]> git.proxmox.com Git - extjs.git/blame - extjs/build/examples/classic/calendar/src/view/AbstractCalendar.js
add extjs 6.0.1 sources
[extjs.git] / extjs / build / examples / classic / calendar / src / view / AbstractCalendar.js
CommitLineData
6527f429
DM
1/**\r
2 * @class Ext.calendar.view.AbstractCalendar\r
3 * @extends Ext.BoxComponent\r
4 * <p>This is an abstract class that serves as the base for other calendar views. This class is not\r
5 * intended to be directly instantiated.</p>\r
6 * <p>When extending this class to create a custom calendar view, you must provide an implementation\r
7 * for the <code>renderItems</code> method, as there is no default implementation for rendering events\r
8 * The rendering logic is totally dependent on how the UI structures its data, which\r
9 * is determined by the underlying UI template (this base class does not have a template).</p>\r
10 * @constructor\r
11 * @param {Object} config The config object\r
12 */\r
13Ext.define('Ext.calendar.view.AbstractCalendar', {\r
14 extend: 'Ext.Component',\r
15 alias: 'widget.calendarview',\r
16 requires: [\r
17 'Ext.calendar.util.Date',\r
18 'Ext.calendar.data.EventMappings'\r
19 ],\r
20 /**\r
21 * @cfg {Number} startDay\r
22 * The 0-based index for the day on which the calendar week begins (0=Sunday, which is the default)\r
23 */\r
24 startDay: 0,\r
25 /**\r
26 * @cfg {Boolean} spansHavePriority\r
27 * Allows switching between two different modes of rendering events that span multiple days. When true,\r
28 * span events are always sorted first, possibly at the expense of start dates being out of order (e.g., \r
29 * a span event that starts at 11am one day and spans into the next day would display before a non-spanning \r
30 * event that starts at 10am, even though they would not be in date order). This can lead to more compact\r
31 * layouts when there are many overlapping events. If false (the default), events will always sort by start date\r
32 * first which can result in a less compact, but chronologically consistent layout.\r
33 */\r
34 spansHavePriority: false,\r
35 /**\r
36 * @cfg {Boolean} trackMouseOver\r
37 * Whether or not the view tracks and responds to the browser mouseover event on contained elements (defaults to\r
38 * true). If you don't need mouseover event highlighting you can disable this.\r
39 */\r
40 trackMouseOver: true,\r
41 /**\r
42 * @cfg {Boolean} enableFx\r
43 * Determines whether or not visual effects for CRUD actions are enabled (defaults to true). If this is false\r
44 * it will override any values for {@link #enableAddFx}, {@link #enableUpdateFx} or {@link enableRemoveFx} and\r
45 * all animations will be disabled.\r
46 */\r
47 enableFx: true,\r
48 /**\r
49 * @cfg {Boolean} enableAddFx\r
50 * True to enable a visual effect on adding a new event (the default), false to disable it. Note that if \r
51 * {@link #enableFx} is false it will override this value. The specific effect that runs is defined in the\r
52 * {@link #doAddFx} method.\r
53 */\r
54 enableAddFx: true,\r
55 /**\r
56 * @cfg {Boolean} enableUpdateFx\r
57 * True to enable a visual effect on updating an event, false to disable it (the default). Note that if \r
58 * {@link #enableFx} is false it will override this value. The specific effect that runs is defined in the\r
59 * {@link #doUpdateFx} method.\r
60 */\r
61 enableUpdateFx: false,\r
62 /**\r
63 * @cfg {Boolean} enableRemoveFx\r
64 * True to enable a visual effect on removing an event (the default), false to disable it. Note that if \r
65 * {@link #enableFx} is false it will override this value. The specific effect that runs is defined in the\r
66 * {@link #doRemoveFx} method.\r
67 */\r
68 enableRemoveFx: true,\r
69 /**\r
70 * @cfg {Boolean} enableDD\r
71 * True to enable drag and drop in the calendar view (the default), false to disable it\r
72 */\r
73 enableDD: true,\r
74 /**\r
75 * @cfg {Boolean} monitorResize\r
76 * True to monitor the browser's resize event (the default), false to ignore it. If the calendar view is rendered\r
77 * into a fixed-size container this can be set to false. However, if the view can change dimensions (e.g., it's in \r
78 * fit layout in a viewport or some other resizable container) it is very important that this config is true so that\r
79 * any resize event propagates properly to all subcomponents and layouts get recalculated properly.\r
80 */\r
81 monitorResize: true,\r
82 /**\r
83 * @cfg {String} ddCreateEventText\r
84 * The text to display inside the drag proxy while dragging over the calendar to create a new event (defaults to \r
85 * 'Create event for {0}' where {0} is a date range supplied by the view)\r
86 */\r
87 ddCreateEventText: 'Create event for {0}',\r
88 /**\r
89 * @cfg {String} ddMoveEventText\r
90 * The text to display inside the drag proxy while dragging an event to reposition it (defaults to \r
91 * 'Move event to {0}' where {0} is the updated event start date/time supplied by the view)\r
92 */\r
93 ddMoveEventText: 'Move event to {0}',\r
94 /**\r
95 * @cfg {String} ddResizeEventText\r
96 * The string displayed to the user in the drag proxy while dragging the resize handle of an event (defaults to \r
97 * 'Update event to {0}' where {0} is the updated event start-end range supplied by the view). Note that \r
98 * this text is only used in views\r
99 * that allow resizing of events.\r
100 */\r
101 ddResizeEventText: 'Update event to {0}',\r
102\r
103 //private properties -- do not override:\r
104 weekCount: 1,\r
105 dayCount: 1,\r
106 eventSelector: '.ext-cal-evt',\r
107 eventOverClass: 'ext-evt-over',\r
108 eventElIdDelimiter: '-evt-',\r
109 dayElIdDelimiter: '-day-',\r
110\r
111 /**\r
112 * Returns a string of HTML template markup to be used as the body portion of the event template created\r
113 * by {@link #getEventTemplate}. This provdes the flexibility to customize what's in the body without\r
114 * having to override the entire XTemplate. This string can include any valid {@link Ext.Template} code, and\r
115 * any data tokens accessible to the containing event template can be referenced in this string.\r
116 * @return {String} The body template string\r
117 */\r
118 getEventBodyMarkup: Ext.emptyFn,\r
119 // must be implemented by a subclass\r
120 /**\r
121 * <p>Returns the XTemplate that is bound to the calendar's event store (it expects records of type\r
122 * {@link Ext.calendar.EventRecord}) to populate the calendar views with events. Internally this method\r
123 * by default generates different markup for browsers that support CSS border radius and those that don't.\r
124 * This method can be overridden as needed to customize the markup generated.</p>\r
125 * <p>Note that this method calls {@link #getEventBodyMarkup} to retrieve the body markup for events separately\r
126 * from the surrounding container markup. This provdes the flexibility to customize what's in the body without\r
127 * having to override the entire XTemplate. If you do override this method, you should make sure that your \r
128 * overridden version also does the same.</p>\r
129 * @return {Ext.XTemplate} The event XTemplate\r
130 */\r
131 getEventTemplate: Ext.emptyFn,\r
132\r
133 /**\r
134 * @event eventsrendered\r
135 * Fires after events are finished rendering in the view\r
136 * @param {Ext.calendar.view.AbstractCalendar} this \r
137 */\r
138\r
139 /**\r
140 * @event eventclick\r
141 * Fires after the user clicks on an event element\r
142 * @param {Ext.calendar.view.AbstractCalendar} this\r
143 * @param {Ext.calendar.EventRecord} rec The {@link Ext.calendar.EventRecord record} for the event that was clicked on\r
144 * @param {HTMLNode} el The DOM node that was clicked on\r
145 */\r
146\r
147 /**\r
148 * @event eventover\r
149 * Fires anytime the mouse is over an event element\r
150 * @param {Ext.calendar.view.AbstractCalendar} this\r
151 * @param {Ext.calendar.EventRecord} rec The {@link Ext.calendar.EventRecord record} for the event that the cursor is over\r
152 * @param {HTMLNode} el The DOM node that is being moused over\r
153 */\r
154\r
155 /**\r
156 * @event eventout\r
157 * Fires anytime the mouse exits an event element\r
158 * @param {Ext.calendar.view.AbstractCalendar} this\r
159 * @param {Ext.calendar.EventRecord} rec The {@link Ext.calendar.EventRecord record} for the event that the cursor exited\r
160 * @param {HTMLNode} el The DOM node that was exited\r
161 */\r
162\r
163 /**\r
164 * @event datechange\r
165 * Fires after the start date of the view changes\r
166 * @param {Ext.calendar.view.AbstractCalendar} this\r
167 * @param {Date} startDate The start date of the view (as explained in {@link #getStartDate}\r
168 * @param {Date} viewStart The first displayed date in the view\r
169 * @param {Date} viewEnd The last displayed date in the view\r
170 */\r
171\r
172 /**\r
173 * @event rangeselect\r
174 * Fires after the user drags on the calendar to select a range of dates/times in which to create an event\r
175 * @param {Ext.calendar.view.AbstractCalendar} this\r
176 * @param {Object} dates An object containing the start (StartDate property) and end (EndDate property) dates selected\r
177 * @param {Function} callback A callback function that MUST be called after the event handling is complete so that\r
178 * the view is properly cleaned up (shim elements are persisted in the view while the user is prompted to handle the\r
179 * range selection). The callback is already created in the proper scope, so it simply needs to be executed as a standard\r
180 * function call (e.g., callback()).\r
181 */\r
182\r
183 /**\r
184 * @event eventmove\r
185 * Fires after an event element is dragged by the user and dropped in a new position\r
186 * @param {Ext.calendar.view.AbstractCalendar} this\r
187 * @param {Ext.calendar.EventRecord} rec The {@link Ext.calendar.EventRecord record} for the event that was moved with\r
188 * updated start and end dates\r
189 */\r
190\r
191 /**\r
192 * @event initdrag\r
193 * Fires when a drag operation is initiated in the view\r
194 * @param {Ext.calendar.view.AbstractCalendar} this\r
195 */\r
196\r
197 /**\r
198 * @event dayover\r
199 * Fires while the mouse is over a day element \r
200 * @param {Ext.calendar.view.AbstractCalendar} this\r
201 * @param {Date} dt The date that is being moused over\r
202 * @param {Ext.core.Element} el The day Element that is being moused over\r
203 */\r
204\r
205 /**\r
206 * @event dayout\r
207 * Fires when the mouse exits a day element \r
208 * @param {Ext.calendar.view.AbstractCalendar} this\r
209 * @param {Date} dt The date that is exited\r
210 * @param {Ext.core.Element} el The day Element that is exited\r
211 */\r
212\r
213 /*\r
214 * @event eventdelete\r
215 * Fires after an event element is deleted by the user. Not currently implemented directly at the view level -- currently \r
216 * deletes only happen from one of the forms.\r
217 * @param {Ext.calendar.view.AbstractCalendar} this\r
218 * @param {Ext.calendar.EventRecord} rec The {@link Ext.calendar.EventRecord record} for the event that was deleted\r
219 */\r
220\r
221 // must be implemented by a subclass\r
222 // private\r
223 initComponent: function() {\r
224 this.setStartDate(this.startDate || new Date());\r
225\r
226 this.callParent(arguments);\r
227 },\r
228\r
229 // private\r
230 afterRender: function() {\r
231 this.callParent(arguments);\r
232\r
233 this.renderTemplate();\r
234\r
235 if (this.store) {\r
236 this.setStore(this.store, true);\r
237 }\r
238\r
239 this.el.on({\r
240 'mouseover': this.onMouseOver,\r
241 'mouseout': this.onMouseOut,\r
242 'click': this.onClick,\r
243 scope: this\r
244 });\r
245\r
246 this.el.unselectable();\r
247\r
248 if (this.enableDD && this.initDD) {\r
249 this.initDD();\r
250 }\r
251\r
252 this.on('eventsrendered', this.forceSize);\r
253 Ext.defer(this.forceSize, 100, this);\r
254\r
255 },\r
256\r
257 // private\r
258 forceSize: function() {\r
259 if (this.el && this.el.down) {\r
260 var hd = this.el.down('.ext-cal-hd-ct'),\r
261 bd = this.el.down('.ext-cal-body-ct');\r
262 \r
263 if (bd==null || hd==null) {\r
264 return;\r
265 }\r
266 \r
267 var headerHeight = hd.getHeight(),\r
268 sz = this.el.parent().getSize();\r
269 \r
270 bd.setHeight(sz.height-headerHeight);\r
271 }\r
272 },\r
273\r
274 refresh: function() {\r
275 this.prepareData();\r
276 this.renderTemplate();\r
277 this.renderItems();\r
278 },\r
279\r
280 getWeekCount: function() {\r
281 var days = Ext.calendar.util.Date.diffDays(this.viewStart, this.viewEnd);\r
282 return Math.ceil(days / this.dayCount);\r
283 },\r
284\r
285 // private\r
286 prepareData: function() {\r
287 var lastInMonth = Ext.Date.getLastDateOfMonth(this.startDate),\r
288 w = 0, d,\r
289 dt = Ext.Date.clone(this.viewStart),\r
290 weeks = this.weekCount < 1 ? 6: this.weekCount;\r
291\r
292 this.eventGrid = [[]];\r
293 this.allDayGrid = [[]];\r
294 this.evtMaxCount = [];\r
295 \r
296 var evtsInView = this.store.queryBy(function(rec) {\r
297 return this.isEventVisible(rec.data);\r
298 },\r
299 this);\r
300\r
301 for (; w < weeks; w++) {\r
302 this.evtMaxCount[w] = 0;\r
303 if (this.weekCount == -1 && dt > lastInMonth) {\r
304 //current week is fully in next month so skip\r
305 break;\r
306 }\r
307 this.eventGrid[w] = this.eventGrid[w] || [];\r
308 this.allDayGrid[w] = this.allDayGrid[w] || [];\r
309\r
310 for (d = 0; d < this.dayCount; d++) {\r
311 if (evtsInView.getCount() > 0) {\r
312 var evts = evtsInView.filterBy(function(rec) {\r
313 var startDt = Ext.Date.clearTime(rec.data[Ext.calendar.data.EventMappings.StartDate.name], true),\r
314 startsOnDate = dt.getTime() == startDt.getTime(),\r
315 spansFromPrevView = (w == 0 && d == 0 && (dt > rec.data[Ext.calendar.data.EventMappings.StartDate.name]));\r
316 \r
317 return startsOnDate || spansFromPrevView;\r
318 },\r
319 this);\r
320\r
321 this.sortEventRecordsForDay(evts);\r
322 this.prepareEventGrid(evts, w, d);\r
323 }\r
324 dt = Ext.calendar.util.Date.add(dt, {days: 1});\r
325 }\r
326 }\r
327 this.currentWeekCount = w;\r
328 },\r
329\r
330 // private\r
331 prepareEventGrid: function(evts, w, d) {\r
332 var me = this,\r
333 row = 0,\r
334 max = me.maxEventsPerDay ? me.maxEventsPerDay: 999;\r
335\r
336 evts.each(function(evt) {\r
337 var M = Ext.calendar.data.EventMappings,\r
338 days = Ext.calendar.util.Date.diffDays(\r
339 Ext.calendar.util.Date.max(me.viewStart, evt.data[M.StartDate.name]),\r
340 Ext.calendar.util.Date.min(me.viewEnd, evt.data[M.EndDate.name])) + 1;\r
341\r
342 if (days > 1 || Ext.calendar.util.Date.diffDays(evt.data[M.StartDate.name], evt.data[M.EndDate.name]) > 1) {\r
343 me.prepareEventGridSpans(evt, me.eventGrid, w, d, days);\r
344 me.prepareEventGridSpans(evt, me.allDayGrid, w, d, days, true);\r
345 } else {\r
346 row = me.findEmptyRowIndex(w, d);\r
347 me.eventGrid[w][d] = me.eventGrid[w][d] || [];\r
348 me.eventGrid[w][d][row] = evt;\r
349\r
350 if (evt.data[M.IsAllDay.name]) {\r
351 row = me.findEmptyRowIndex(w, d, true);\r
352 me.allDayGrid[w][d] = me.allDayGrid[w][d] || [];\r
353 me.allDayGrid[w][d][row] = evt;\r
354 }\r
355 }\r
356\r
357 if (me.evtMaxCount[w] < me.eventGrid[w][d].length) {\r
358 me.evtMaxCount[w] = Math.min(max + 1, me.eventGrid[w][d].length);\r
359 }\r
360 return true;\r
361 });\r
362 },\r
363\r
364 // private\r
365 prepareEventGridSpans: function(evt, grid, w, d, days, allday) {\r
366 // this event spans multiple days/weeks, so we have to preprocess\r
367 // the events and store special span events as placeholders so that\r
368 // the render routine can build the necessary TD spans correctly.\r
369 var w1 = w,\r
370 d1 = d,\r
371 row = this.findEmptyRowIndex(w, d, allday),\r
372 dt = Ext.Date.clone(this.viewStart);\r
373\r
374 var start = {\r
375 event: evt,\r
376 isSpan: true,\r
377 isSpanStart: true,\r
378 spanLeft: false,\r
379 spanRight: (d == 6)\r
380 };\r
381 grid[w][d] = grid[w][d] || [];\r
382 grid[w][d][row] = start;\r
383\r
384 while (--days) {\r
385 dt = Ext.calendar.util.Date.add(dt, {days: 1});\r
386 if (dt > this.viewEnd) {\r
387 break;\r
388 }\r
389 if (++d1 > 6) {\r
390 // reset counters to the next week\r
391 d1 = 0;\r
392 w1++;\r
393 row = this.findEmptyRowIndex(w1, 0);\r
394 }\r
395 grid[w1] = grid[w1] || [];\r
396 grid[w1][d1] = grid[w1][d1] || [];\r
397\r
398 grid[w1][d1][row] = {\r
399 event: evt,\r
400 isSpan: true,\r
401 isSpanStart: (d1 == 0),\r
402 spanLeft: (w1 > w) && (d1 % 7 == 0),\r
403 spanRight: (d1 == 6) && (days > 1)\r
404 };\r
405 }\r
406 },\r
407\r
408 // private\r
409 findEmptyRowIndex: function(w, d, allday) {\r
410 var grid = allday ? this.allDayGrid: this.eventGrid,\r
411 day = grid[w] ? grid[w][d] || [] : [],\r
412 i = 0,\r
413 ln = day.length;\r
414\r
415 for (; i < ln; i++) {\r
416 if (day[i] == null) {\r
417 return i;\r
418 }\r
419 }\r
420 return ln;\r
421 },\r
422\r
423 // private\r
424 renderTemplate: function() {\r
425 if (this.tpl) {\r
426 this.el.select('*').destroy();\r
427 this.tpl.overwrite(this.el, this.getParams());\r
428 this.lastRenderStart = Ext.Date.clone(this.viewStart);\r
429 this.lastRenderEnd = Ext.Date.clone(this.viewEnd);\r
430 }\r
431 },\r
432\r
433 disableStoreEvents: function() {\r
434 this.monitorStoreEvents = false;\r
435 },\r
436\r
437 enableStoreEvents: function(refresh) {\r
438 this.monitorStoreEvents = true;\r
439 if (refresh === true) {\r
440 this.refresh();\r
441 }\r
442 },\r
443\r
444 // private\r
445 onResize: function() {\r
446 this.callParent(arguments);\r
447 this.refresh();\r
448 },\r
449\r
450 // private\r
451 onInitDrag: function() {\r
452 this.fireEvent('initdrag', this);\r
453 },\r
454\r
455 // private\r
456 onEventDrop: function(rec, dt) {\r
457 if (Ext.calendar.util.Date.compare(rec.data[Ext.calendar.data.EventMappings.StartDate.name], dt) === 0) {\r
458 // no changes\r
459 return;\r
460 }\r
461 var diff = dt.getTime() - rec.data[Ext.calendar.data.EventMappings.StartDate.name].getTime();\r
462 rec.set(Ext.calendar.data.EventMappings.StartDate.name, dt);\r
463 rec.set(Ext.calendar.data.EventMappings.EndDate.name, Ext.calendar.util.Date.add(rec.data[Ext.calendar.data.EventMappings.EndDate.name], {millis: diff}));\r
464\r
465 this.fireEvent('eventmove', this, rec);\r
466 },\r
467\r
468 // private\r
469 onCalendarEndDrag: function(start, end, onComplete) {\r
470 if (start && end) {\r
471 // set this flag for other event handlers that might conflict while we're waiting\r
472 this.dragPending = true;\r
473\r
474 // have to wait for the user to save or cancel before finalizing the dd interation\r
475 var o = {};\r
476 o[Ext.calendar.data.EventMappings.StartDate.name] = start;\r
477 o[Ext.calendar.data.EventMappings.EndDate.name] = end;\r
478\r
479 this.fireEvent('rangeselect', this, o, Ext.bind(this.onCalendarEndDragComplete, this, [onComplete]));\r
480 }\r
481 },\r
482\r
483 // private\r
484 onCalendarEndDragComplete: function(onComplete) {\r
485 // callback for the drop zone to clean up\r
486 onComplete();\r
487 // clear flag for other events to resume normally\r
488 this.dragPending = false;\r
489 },\r
490\r
491 // private\r
492 onUpdate: function(ds, rec, operation) {\r
493 if (this.monitorStoreEvents === false) {\r
494 return;\r
495 }\r
496 if (operation == Ext.data.Record.COMMIT) {\r
497 this.refresh();\r
498 if (this.enableFx && this.enableUpdateFx) {\r
499 this.doUpdateFx(this.getEventEls(rec.data[Ext.calendar.data.EventMappings.EventId.name]), {\r
500 scope: this\r
501 });\r
502 }\r
503 }\r
504 },\r
505\r
506\r
507 doUpdateFx: function(els, o) {\r
508 this.highlightEvent(els, null, o);\r
509 },\r
510\r
511 // private\r
512 onAdd: function(ds, records, index) {\r
513 if (this.monitorStoreEvents === false) {\r
514 return;\r
515 }\r
516 var rec = records[0];\r
517 this.tempEventId = rec.id;\r
518 this.refresh();\r
519\r
520 if (this.enableFx && this.enableAddFx) {\r
521 this.doAddFx(this.getEventEls(rec.data[Ext.calendar.data.EventMappings.EventId.name]), {\r
522 scope: this\r
523 });\r
524 }\r
525 },\r
526\r
527 doAddFx: function(els, o) {\r
528 els.fadeIn(Ext.apply(o, {\r
529 duration: 2000\r
530 }));\r
531 },\r
532\r
533 // private\r
534 onRemove: function(ds, recs) {\r
535 var name = Ext.calendar.data.EventMappings.EventId.name,\r
536 i, len, rec, els;\r
537 \r
538 if (this.monitorStoreEvents === false) {\r
539 return;\r
540 }\r
541 \r
542 for (i = 0, len = recs.length; i < len; i++) {\r
543 rec = recs[i];\r
544 \r
545 if (this.enableFx && this.enableRemoveFx) {\r
546 els = this.getEventEls(rec.get(name));\r
547 \r
548 if (els.getCount() > 0) {\r
549 this.doRemoveFx(els, {\r
550 remove: true,\r
551 scope: this,\r
552 callback: this.refresh\r
553 });\r
554 }\r
555 }\r
556 else {\r
557 this.getEventEls(rec.get(name)).remove();\r
558 this.refresh();\r
559 }\r
560 }\r
561 },\r
562\r
563 doRemoveFx: function(els, o) {\r
564 els.fadeOut(o);\r
565 },\r
566\r
567 /**\r
568 * Visually highlights an event using {@link Ext.Fx#highlight} config options.\r
569 * If {@link #highlightEventActions} is false this method will have no effect.\r
570 * @param {Ext.CompositeElement} els The element(s) to highlight\r
571 * @param {Object} color (optional) The highlight color. Should be a 6 char hex \r
572 * color without the leading # (defaults to yellow: 'ffff9c')\r
573 * @param {Object} o (optional) Object literal with any of the {@link Ext.Fx} config \r
574 * options. See {@link Ext.Fx#highlight} for usage examples.\r
575 */\r
576 highlightEvent: function(els, color, o) {\r
577 if (this.enableFx) {\r
578 var c;\r
579 ! (Ext.isIE || Ext.isOpera) ?\r
580 els.highlight(color, o) :\r
581 // Fun IE/Opera handling:\r
582 els.each(function(el) {\r
583 el.highlight(color, Ext.applyIf({\r
584 attr: 'color'\r
585 },\r
586 o));\r
587 c = el.down('.ext-cal-evm');\r
588 if (c) {\r
589 c.highlight(color, o);\r
590 }\r
591 },\r
592 this);\r
593 }\r
594 },\r
595\r
596 /**\r
597 * Retrieve an Event object's id from its corresponding node in the DOM.\r
598 * @param {String/Element/HTMLElement} el An {@link Ext.core.Element}, DOM node or id\r
599 */\r
600 getEventIdFromEl: function(el) {\r
601 el = Ext.get(el);\r
602 var id = el.id.split(this.eventElIdDelimiter)[1],\r
603 lastHypen = id.lastIndexOf('-');\r
604\r
605 // MUST look for last hyphen because autogenned record IDs can contain hyphens\r
606 if (lastHypen > -1) {\r
607 //This id has the index of the week it is rendered in as the suffix.\r
608 //This allows events that span across weeks to still have reproducibly-unique DOM ids.\r
609 id = id.substr(0, lastHypen);\r
610 }\r
611 return id;\r
612 },\r
613\r
614 // private\r
615 getEventId: function(eventId) {\r
616 if (eventId === undefined && this.tempEventId) {\r
617 eventId = this.tempEventId;\r
618 }\r
619 return eventId;\r
620 },\r
621\r
622 /**\r
623 * \r
624 * @param {String} eventId\r
625 * @param {Boolean} forSelect\r
626 * @return {String} The selector class\r
627 */\r
628 getEventSelectorCls: function(eventId, forSelect) {\r
629 var prefix = forSelect ? '.': '';\r
630 return prefix + this.id + this.eventElIdDelimiter + this.getEventId(eventId);\r
631 },\r
632\r
633 /**\r
634 * \r
635 * @param {String} eventId\r
636 * @return {Ext.CompositeElement} The matching CompositeElement of nodes\r
637 * that comprise the rendered event. Any event that spans across a view \r
638 * boundary will contain more than one internal Element.\r
639 */\r
640 getEventEls: function(eventId) {\r
641 var els = Ext.select(this.getEventSelectorCls(this.getEventId(eventId), true), false, this.el.dom);\r
642 return new Ext.CompositeElement(els);\r
643 },\r
644\r
645 /**\r
646 * Returns true if the view is currently displaying today's date, else false.\r
647 * @return {Boolean} True or false\r
648 */\r
649 isToday: function() {\r
650 var today = Ext.Date.clearTime(new Date()).getTime();\r
651 return this.viewStart.getTime() <= today && this.viewEnd.getTime() >= today;\r
652 },\r
653\r
654 // private\r
655 onDataChanged: function(store) {\r
656 this.refresh();\r
657 },\r
658\r
659 // private\r
660 isEventVisible: function(evt) {\r
661 var M = Ext.calendar.data.EventMappings,\r
662 data = evt.data || evt,\r
663 start = this.viewStart.getTime(),\r
664 end = this.viewEnd.getTime(),\r
665 evStart = data[M.StartDate.name].getTime(),\r
666 evEnd = data[M.EndDate.name].getTime();\r
667 evEnd = Ext.calendar.util.Date.add(data[M.EndDate.name], {seconds: -1}).getTime();\r
668\r
669 return this.rangesOverlap(start, end, evStart, evEnd);\r
670 },\r
671 \r
672 rangesOverlap: function(start1, end1, start2, end2) {\r
673 var startsInRange = (start1 >= start2 && start1 <= end2),\r
674 endsInRange = (end1 >= start2 && end1 <= end2),\r
675 spansRange = (start1 <= start2 && end1 >= end2);\r
676 \r
677 return (startsInRange || endsInRange || spansRange);\r
678 },\r
679\r
680 // private\r
681 isOverlapping: function(evt1, evt2) {\r
682 var ev1 = evt1.data ? evt1.data: evt1,\r
683 ev2 = evt2.data ? evt2.data: evt2,\r
684 M = Ext.calendar.data.EventMappings,\r
685 start1 = ev1[M.StartDate.name].getTime(),\r
686 end1 = Ext.calendar.util.Date.add(ev1[M.EndDate.name], {seconds: -1}).getTime(),\r
687 start2 = ev2[M.StartDate.name].getTime(),\r
688 end2 = Ext.calendar.util.Date.add(ev2[M.EndDate.name], {seconds: -1}).getTime();\r
689\r
690 if (end1 < start1) {\r
691 end1 = start1;\r
692 }\r
693 if (end2 < start2) {\r
694 end2 = start2;\r
695 }\r
696\r
697 return (start1 <= end2 && end1 >= start2);\r
698 },\r
699\r
700 getDayEl: function(dt) {\r
701 return Ext.get(this.getDayId(dt));\r
702 },\r
703\r
704 getDayId: function(dt) {\r
705 if (Ext.isDate(dt)) {\r
706 dt = Ext.Date.format(dt, 'Ymd');\r
707 }\r
708 return this.id + this.dayElIdDelimiter + dt;\r
709 },\r
710\r
711 /**\r
712 * Returns the start date of the view, as set by {@link #setStartDate}. Note that this may not \r
713 * be the first date displayed in the rendered calendar -- to get the start and end dates displayed\r
714 * to the user use {@link #getViewBounds}.\r
715 * @return {Date} The start date\r
716 */\r
717 getStartDate: function() {\r
718 return this.startDate;\r
719 },\r
720\r
721 /**\r
722 * Sets the start date used to calculate the view boundaries to display. The displayed view will be the \r
723 * earliest and latest dates that match the view requirements and contain the date passed to this function.\r
724 * @param {Date} dt The date used to calculate the new view boundaries\r
725 */\r
726 setStartDate: function(start, refresh) {\r
727 this.startDate = Ext.Date.clearTime(start);\r
728 this.setViewBounds(start);\r
729 this.store.load({\r
730 params: {\r
731 start: Ext.Date.format(this.viewStart, 'm-d-Y'),\r
732 end: Ext.Date.format(this.viewEnd, 'm-d-Y')\r
733 }\r
734 });\r
735 if (refresh === true) {\r
736 this.refresh();\r
737 }\r
738 this.fireEvent('datechange', this, this.startDate, this.viewStart, this.viewEnd);\r
739 },\r
740\r
741 // private\r
742 setViewBounds: function(startDate) {\r
743 var start = startDate || this.startDate,\r
744 offset = start.getDay() - this.startDay,\r
745 Dt = Ext.calendar.util.Date;\r
746\r
747 switch (this.weekCount) {\r
748 case 0:\r
749 case 1:\r
750 this.viewStart = this.dayCount < 7 ? start: Dt.add(start, {days: -offset, clearTime: true});\r
751 this.viewEnd = Dt.add(this.viewStart, {days: this.dayCount || 7});\r
752 this.viewEnd = Dt.add(this.viewEnd, {seconds: -1});\r
753 return;\r
754\r
755 case - 1:\r
756 // auto by month\r
757 start = Ext.Date.getFirstDateOfMonth(start);\r
758 offset = start.getDay() - this.startDay;\r
759\r
760 this.viewStart = Dt.add(start, {days: -offset, clearTime: true});\r
761\r
762 // start from current month start, not view start:\r
763 var end = Dt.add(start, {months: 1, seconds: -1});\r
764 // fill out to the end of the week:\r
765 this.viewEnd = Dt.add(end, {days: 6 - end.getDay()});\r
766 return;\r
767\r
768 default:\r
769 this.viewStart = Dt.add(start, {days: -offset, clearTime: true});\r
770 this.viewEnd = Dt.add(this.viewStart, {days: this.weekCount * 7, seconds: -1});\r
771 }\r
772 },\r
773\r
774 // private\r
775 getViewBounds: function() {\r
776 return {\r
777 start: this.viewStart,\r
778 end: this.viewEnd\r
779 };\r
780 },\r
781\r
782 /* private\r
783 * Sort events for a single day for display in the calendar. This sorts allday\r
784 * events first, then non-allday events are sorted either based on event start\r
785 * priority or span priority based on the value of {@link #spansHavePriority} \r
786 * (defaults to event start priority).\r
787 * @param {MixedCollection} evts A {@link Ext.util.MixedCollection MixedCollection} \r
788 * of {@link #Ext.calendar.EventRecord EventRecord} objects\r
789 */\r
790 sortEventRecordsForDay: function(evts) {\r
791 if (evts.length < 2) {\r
792 return;\r
793 }\r
794 evts.sortBy(Ext.bind(function(evtA, evtB) {\r
795 var a = evtA.data,\r
796 b = evtB.data,\r
797 M = Ext.calendar.data.EventMappings;\r
798\r
799 // Always sort all day events before anything else\r
800 if (a[M.IsAllDay.name]) {\r
801 return - 1;\r
802 }\r
803 else if (b[M.IsAllDay.name]) {\r
804 return 1;\r
805 }\r
806 if (this.spansHavePriority) {\r
807 // This logic always weights span events higher than non-span events\r
808 // (at the possible expense of start time order). This seems to\r
809 // be the approach used by Google calendar and can lead to a more\r
810 // visually appealing layout in complex cases, but event order is\r
811 // not guaranteed to be consistent.\r
812 var diff = Ext.calendar.util.Date.diffDays;\r
813 if (diff(a[M.StartDate.name], a[M.EndDate.name]) > 0) {\r
814 if (diff(b[M.StartDate.name], b[M.EndDate.name]) > 0) {\r
815 // Both events are multi-day\r
816 if (a[M.StartDate.name].getTime() == b[M.StartDate.name].getTime()) {\r
817 // If both events start at the same time, sort the one\r
818 // that ends later (potentially longer span bar) first\r
819 return b[M.EndDate.name].getTime() - a[M.EndDate.name].getTime();\r
820 }\r
821 return a[M.StartDate.name].getTime() - b[M.StartDate.name].getTime();\r
822 }\r
823 return - 1;\r
824 }\r
825 else if (diff(b[M.StartDate.name], b[M.EndDate.name]) > 0) {\r
826 return 1;\r
827 }\r
828 return a[M.StartDate.name].getTime() - b[M.StartDate.name].getTime();\r
829 }\r
830 else {\r
831 // Doing this allows span and non-span events to intermingle but\r
832 // remain sorted sequentially by start time. This seems more proper\r
833 // but can make for a less visually-compact layout when there are\r
834 // many such events mixed together closely on the calendar.\r
835 return a[M.StartDate.name].getTime() - b[M.StartDate.name].getTime();\r
836 }\r
837 }, this));\r
838 },\r
839\r
840 /**\r
841 * Updates the view to contain the passed date\r
842 * @param {Date} dt The date to display\r
843 * @return {Date} The new view start date\r
844 */\r
845 moveTo: function(dt, noRefresh) {\r
846 if (Ext.isDate(dt)) {\r
847 this.setStartDate(dt);\r
848 if (noRefresh !== false) {\r
849 this.refresh();\r
850 }\r
851 return this.startDate;\r
852 }\r
853 return dt;\r
854 },\r
855\r
856 /**\r
857 * Updates the view to the next consecutive date(s)\r
858 * @return {Date} The new view start date\r
859 */\r
860 moveNext: function(noRefresh) {\r
861 return this.moveTo(Ext.calendar.util.Date.add(this.viewEnd, {days: 1}));\r
862 },\r
863\r
864 /**\r
865 * Updates the view to the previous consecutive date(s)\r
866 * @return {Date} The new view start date\r
867 */\r
868 movePrev: function(noRefresh) {\r
869 var days = Ext.calendar.util.Date.diffDays(this.viewStart, this.viewEnd) + 1;\r
870 return this.moveDays( - days, noRefresh);\r
871 },\r
872\r
873 /**\r
874 * Shifts the view by the passed number of months relative to the currently set date\r
875 * @param {Number} value The number of months (positive or negative) by which to shift the view\r
876 * @return {Date} The new view start date\r
877 */\r
878 moveMonths: function(value, noRefresh) {\r
879 return this.moveTo(Ext.calendar.util.Date.add(this.startDate, {months: value}), noRefresh);\r
880 },\r
881\r
882 /**\r
883 * Shifts the view by the passed number of weeks relative to the currently set date\r
884 * @param {Number} value The number of weeks (positive or negative) by which to shift the view\r
885 * @return {Date} The new view start date\r
886 */\r
887 moveWeeks: function(value, noRefresh) {\r
888 return this.moveTo(Ext.calendar.util.Date.add(this.startDate, {days: value * 7}), noRefresh);\r
889 },\r
890\r
891 /**\r
892 * Shifts the view by the passed number of days relative to the currently set date\r
893 * @param {Number} value The number of days (positive or negative) by which to shift the view\r
894 * @return {Date} The new view start date\r
895 */\r
896 moveDays: function(value, noRefresh) {\r
897 return this.moveTo(Ext.calendar.util.Date.add(this.startDate, {days: value}), noRefresh);\r
898 },\r
899\r
900 /**\r
901 * Updates the view to show today\r
902 * @return {Date} Today's date\r
903 */\r
904 moveToday: function(noRefresh) {\r
905 return this.moveTo(new Date(), noRefresh);\r
906 },\r
907\r
908 /**\r
909 * Sets the event store used by the calendar to display {@link Ext.calendar.EventRecord events}.\r
910 * @param {Ext.data.Store} store\r
911 */\r
912 setStore: function(store, initial) {\r
913 if (!initial && this.store) {\r
914 this.store.un("datachanged", this.onDataChanged, this);\r
915 this.store.un("add", this.onAdd, this);\r
916 this.store.un("remove", this.onRemove, this);\r
917 this.store.un("update", this.onUpdate, this);\r
918 this.store.un("clear", this.refresh, this);\r
919 }\r
920 if (store) {\r
921 store.on("datachanged", this.onDataChanged, this);\r
922 store.on("add", this.onAdd, this);\r
923 store.on("remove", this.onRemove, this);\r
924 store.on("update", this.onUpdate, this);\r
925 store.on("clear", this.refresh, this);\r
926 }\r
927 this.store = store;\r
928 if (store && store.getCount() > 0) {\r
929 this.refresh();\r
930 }\r
931 },\r
932\r
933 getEventRecord: function(id) {\r
934 var idx = this.store.find(Ext.calendar.data.EventMappings.EventId.name, id);\r
935 return this.store.getAt(idx);\r
936 },\r
937\r
938 getEventRecordFromEl: function(el) {\r
939 return this.getEventRecord(this.getEventIdFromEl(el));\r
940 },\r
941\r
942 // private\r
943 getParams: function() {\r
944 return {\r
945 viewStart: this.viewStart,\r
946 viewEnd: this.viewEnd,\r
947 startDate: this.startDate,\r
948 dayCount: this.dayCount,\r
949 weekCount: this.weekCount,\r
950 title: this.getTitle()\r
951 };\r
952 },\r
953\r
954 getTitle: function() {\r
955 return Ext.Date.format(this.startDate, 'F Y');\r
956 },\r
957\r
958 /*\r
959 * Shared click handling. Each specific view also provides view-specific\r
960 * click handling that calls this first. This method returns true if it\r
961 * can handle the click (and so the subclass should ignore it) else false.\r
962 */\r
963 onClick: function(e, t) {\r
964 var el = e.getTarget(this.eventSelector, 5);\r
965 if (el) {\r
966 var id = this.getEventIdFromEl(el);\r
967 this.fireEvent('eventclick', this, this.getEventRecord(id), el);\r
968 return true;\r
969 }\r
970 },\r
971\r
972 // private\r
973 onMouseOver: function(e, t) {\r
974 if (this.trackMouseOver !== false && (this.dragZone == undefined || !this.dragZone.dragging)) {\r
975 if (!this.handleEventMouseEvent(e, t, 'over')) {\r
976 this.handleDayMouseEvent(e, t, 'over');\r
977 }\r
978 }\r
979 },\r
980\r
981 // private\r
982 onMouseOut: function(e, t) {\r
983 if (this.trackMouseOver !== false && (this.dragZone == undefined || !this.dragZone.dragging)) {\r
984 if (!this.handleEventMouseEvent(e, t, 'out')) {\r
985 this.handleDayMouseEvent(e, t, 'out');\r
986 }\r
987 }\r
988 },\r
989\r
990 // private\r
991 handleEventMouseEvent: function(e, t, type) {\r
992 var el = e.getTarget(this.eventSelector, 5, true),\r
993 rel,\r
994 els,\r
995 evtId;\r
996 if (el) {\r
997 rel = Ext.get(e.getRelatedTarget());\r
998 if (el == rel || el.contains(rel)) {\r
999 return true;\r
1000 }\r
1001\r
1002 evtId = this.getEventIdFromEl(el);\r
1003\r
1004 if (this.eventOverClass) {\r
1005 els = this.getEventEls(evtId);\r
1006 els[type == 'over' ? 'addCls': 'removeCls'](this.eventOverClass);\r
1007 }\r
1008 this.fireEvent('event' + type, this, this.getEventRecord(evtId), el);\r
1009 return true;\r
1010 }\r
1011 return false;\r
1012 },\r
1013\r
1014 // private\r
1015 getDateFromId: function(id, delim) {\r
1016 var parts = id.split(delim);\r
1017 return parts[parts.length - 1];\r
1018 },\r
1019\r
1020 // private\r
1021 handleDayMouseEvent: function(e, t, type) {\r
1022 t = e.getTarget('td', 3);\r
1023 if (t) {\r
1024 if (t.id && t.id.indexOf(this.dayElIdDelimiter) > -1) {\r
1025 var dt = this.getDateFromId(t.id, this.dayElIdDelimiter),\r
1026 rel = Ext.get(e.getRelatedTarget()),\r
1027 relTD,\r
1028 relDate;\r
1029\r
1030 if (rel) {\r
1031 relTD = rel.is('td') ? rel: rel.up('td', 3);\r
1032 relDate = relTD && relTD.id ? this.getDateFromId(relTD.id, this.dayElIdDelimiter) : '';\r
1033 }\r
1034 if (!rel || dt != relDate) {\r
1035 var el = this.getDayEl(dt);\r
1036 if (el && this.dayOverClass != '') {\r
1037 el[type == 'over' ? 'addCls': 'removeCls'](this.dayOverClass);\r
1038 }\r
1039 this.fireEvent('day' + type, this, Ext.Date.parseDate(dt, "Ymd"), el);\r
1040 }\r
1041 }\r
1042 }\r
1043 },\r
1044\r
1045 // private\r
1046 renderItems: function() {\r
1047 throw 'This method must be implemented by a subclass';\r
1048 },\r
1049 \r
1050 // private\r
1051 destroy: function(){\r
1052 this.callParent(arguments);\r
1053 \r
1054 if(this.el){\r
1055 this.el.un('contextmenu', this.onContextMenu, this);\r
1056 }\r
1057 Ext.destroy(\r
1058 this.editWin, \r
1059 this.eventMenu,\r
1060 this.dragZone,\r
1061 this.dropZone\r
1062 );\r
1063 }\r
1064});