Upgrade to ExtJS 3.3.1 - Released 11/30/2010
[extjs.git] / examples / calendar / src / views / CalendarView.js
1 /*!
2  * Ext JS Library 3.3.1
3  * Copyright(c) 2006-2010 Sencha Inc.
4  * licensing@sencha.com
5  * http://www.sencha.com/license
6  */
7 /**\r
8  * @class Ext.calendar.CalendarView\r
9  * @extends Ext.BoxComponent\r
10  * <p>This is an abstract class that serves as the base for other calendar views. This class is not\r
11  * intended to be directly instantiated.</p>\r
12  * <p>When extending this class to create a custom calendar view, you must provide an implementation\r
13  * for the <code>renderItems</code> method, as there is no default implementation for rendering events\r
14  * The rendering logic is totally dependent on how the UI structures its data, which\r
15  * is determined by the underlying UI template (this base class does not have a template).</p>\r
16  * @constructor\r
17  * @param {Object} config The config object\r
18  */\r
19 Ext.calendar.CalendarView = Ext.extend(Ext.BoxComponent, {\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     // must be implemented by a subclass\r
133     // private\r
134     initComponent: function() {\r
135         this.setStartDate(this.startDate || new Date());\r
136 \r
137         Ext.calendar.CalendarView.superclass.initComponent.call(this);\r
138 \r
139         this.addEvents({\r
140             /**\r
141              * @event eventsrendered\r
142              * Fires after events are finished rendering in the view\r
143              * @param {Ext.calendar.CalendarView} this \r
144              */\r
145             eventsrendered: true,\r
146             /**\r
147              * @event eventclick\r
148              * Fires after the user clicks on an event element\r
149              * @param {Ext.calendar.CalendarView} this\r
150              * @param {Ext.calendar.EventRecord} rec The {@link Ext.calendar.EventRecord record} for the event that was clicked on\r
151              * @param {HTMLNode} el The DOM node that was clicked on\r
152              */\r
153             eventclick: true,\r
154             /**\r
155              * @event eventover\r
156              * Fires anytime the mouse is over an event element\r
157              * @param {Ext.calendar.CalendarView} this\r
158              * @param {Ext.calendar.EventRecord} rec The {@link Ext.calendar.EventRecord record} for the event that the cursor is over\r
159              * @param {HTMLNode} el The DOM node that is being moused over\r
160              */\r
161             eventover: true,\r
162             /**\r
163              * @event eventout\r
164              * Fires anytime the mouse exits an event element\r
165              * @param {Ext.calendar.CalendarView} this\r
166              * @param {Ext.calendar.EventRecord} rec The {@link Ext.calendar.EventRecord record} for the event that the cursor exited\r
167              * @param {HTMLNode} el The DOM node that was exited\r
168              */\r
169             eventout: true,\r
170             /**\r
171              * @event datechange\r
172              * Fires after the start date of the view changes\r
173              * @param {Ext.calendar.CalendarView} this\r
174              * @param {Date} startDate The start date of the view (as explained in {@link #getStartDate}\r
175              * @param {Date} viewStart The first displayed date in the view\r
176              * @param {Date} viewEnd The last displayed date in the view\r
177              */\r
178             datechange: true,\r
179             /**\r
180              * @event rangeselect\r
181              * Fires after the user drags on the calendar to select a range of dates/times in which to create an event\r
182              * @param {Ext.calendar.CalendarView} this\r
183              * @param {Object} dates An object containing the start (StartDate property) and end (EndDate property) dates selected\r
184              * @param {Function} callback A callback function that MUST be called after the event handling is complete so that\r
185              * the view is properly cleaned up (shim elements are persisted in the view while the user is prompted to handle the\r
186              * range selection). The callback is already created in the proper scope, so it simply needs to be executed as a standard\r
187              * function call (e.g., callback()).\r
188              */\r
189             rangeselect: true,\r
190             /**\r
191              * @event eventmove\r
192              * Fires after an event element is dragged by the user and dropped in a new position\r
193              * @param {Ext.calendar.CalendarView} this\r
194              * @param {Ext.calendar.EventRecord} rec The {@link Ext.calendar.EventRecord record} for the event that was moved with\r
195              * updated start and end dates\r
196              */\r
197             eventmove: true,\r
198             /**\r
199              * @event initdrag\r
200              * Fires when a drag operation is initiated in the view\r
201              * @param {Ext.calendar.CalendarView} this\r
202              */\r
203             initdrag: true,\r
204             /**\r
205              * @event dayover\r
206              * Fires while the mouse is over a day element \r
207              * @param {Ext.calendar.CalendarView} this\r
208              * @param {Date} dt The date that is being moused over\r
209              * @param {Ext.Element} el The day Element that is being moused over\r
210              */\r
211             dayover: true,\r
212             /**\r
213              * @event dayout\r
214              * Fires when the mouse exits a day element \r
215              * @param {Ext.calendar.CalendarView} this\r
216              * @param {Date} dt The date that is exited\r
217              * @param {Ext.Element} el The day Element that is exited\r
218              */\r
219             dayout: true\r
220             /*\r
221              * @event eventdelete\r
222              * Fires after an event element is deleted by the user. Not currently implemented directly at the view level -- currently \r
223              * deletes only happen from one of the forms.\r
224              * @param {Ext.calendar.CalendarView} this\r
225              * @param {Ext.calendar.EventRecord} rec The {@link Ext.calendar.EventRecord record} for the event that was deleted\r
226              */\r
227             //eventdelete: true\r
228         });\r
229     },\r
230 \r
231     // private\r
232     afterRender: function() {\r
233         Ext.calendar.CalendarView.superclass.afterRender.call(this);\r
234 \r
235         this.renderTemplate();\r
236 \r
237         if (this.store) {\r
238             this.setStore(this.store, true);\r
239         }\r
240 \r
241         this.el.on({\r
242             'mouseover': this.onMouseOver,\r
243             'mouseout': this.onMouseOut,\r
244             'click': this.onClick,\r
245             'resize': this.onResize,\r
246             scope: this\r
247         });\r
248 \r
249         this.el.unselectable();\r
250 \r
251         if (this.enableDD && this.initDD) {\r
252             this.initDD();\r
253         }\r
254 \r
255         this.on('eventsrendered', this.forceSize);\r
256         this.forceSize.defer(100, this);\r
257 \r
258     },\r
259 \r
260     // private\r
261     forceSize: function() {\r
262         if (this.el && this.el.child) {\r
263             var hd = this.el.child('.ext-cal-hd-ct'),\r
264             bd = this.el.child('.ext-cal-body-ct');\r
265 \r
266             if (bd == null || hd == null) return;\r
267 \r
268             var headerHeight = hd.getHeight(),\r
269             sz = this.el.parent().getSize();\r
270 \r
271             bd.setHeight(sz.height - headerHeight);\r
272         }\r
273     },\r
274 \r
275     refresh: function() {\r
276         this.prepareData();\r
277         this.renderTemplate();\r
278         this.renderItems();\r
279     },\r
280 \r
281     getWeekCount: function() {\r
282         var days = Ext.calendar.Date.diffDays(this.viewStart, this.viewEnd);\r
283         return Math.ceil(days / this.dayCount);\r
284     },\r
285 \r
286     // private\r
287     prepareData: function() {\r
288         var lastInMonth = this.startDate.getLastDateOfMonth(),\r
289         w = 0,\r
290         row = 0,\r
291         dt = this.viewStart.clone(),\r
292         weeks = this.weekCount < 1 ? 6: this.weekCount;\r
293 \r
294         this.eventGrid = [[]];\r
295         this.allDayGrid = [[]];\r
296         this.evtMaxCount = [];\r
297 \r
298         var evtsInView = this.store.queryBy(function(rec) {\r
299             return this.isEventVisible(rec.data);\r
300         },\r
301         this);\r
302 \r
303         for (; w < weeks; w++) {\r
304             this.evtMaxCount[w] = 0;\r
305             if (this.weekCount == -1 && dt > lastInMonth) {\r
306                 //current week is fully in next month so skip\r
307                 break;\r
308             }\r
309             this.eventGrid[w] = this.eventGrid[w] || [];\r
310             this.allDayGrid[w] = this.allDayGrid[w] || [];\r
311 \r
312             for (d = 0; d < this.dayCount; d++) {\r
313                 if (evtsInView.getCount() > 0) {\r
314                     var evts = evtsInView.filterBy(function(rec) {\r
315                         var startsOnDate = (dt.getTime() == rec.data[Ext.calendar.EventMappings.StartDate.name].clearTime(true).getTime());\r
316                         var spansFromPrevView = (w == 0 && d == 0 && (dt > rec.data[Ext.calendar.EventMappings.StartDate.name]));\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 = dt.add(Date.DAY, 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 row = 0,\r
333         dt = this.viewStart.clone(),\r
334         max = this.maxEventsPerDay ? this.maxEventsPerDay: 999;\r
335 \r
336         evts.each(function(evt) {\r
337             var M = Ext.calendar.EventMappings,\r
338             days = Ext.calendar.Date.diffDays(\r
339             Ext.calendar.Date.max(this.viewStart, evt.data[M.StartDate.name]),\r
340             Ext.calendar.Date.min(this.viewEnd, evt.data[M.EndDate.name])) + 1;\r
341 \r
342             if (days > 1 || Ext.calendar.Date.diffDays(evt.data[M.StartDate.name], evt.data[M.EndDate.name]) > 1) {\r
343                 this.prepareEventGridSpans(evt, this.eventGrid, w, d, days);\r
344                 this.prepareEventGridSpans(evt, this.allDayGrid, w, d, days, true);\r
345             } else {\r
346                 row = this.findEmptyRowIndex(w, d);\r
347                 this.eventGrid[w][d] = this.eventGrid[w][d] || [];\r
348                 this.eventGrid[w][d][row] = evt;\r
349 \r
350                 if (evt.data[M.IsAllDay.name]) {\r
351                     row = this.findEmptyRowIndex(w, d, true);\r
352                     this.allDayGrid[w][d] = this.allDayGrid[w][d] || [];\r
353                     this.allDayGrid[w][d][row] = evt;\r
354                 }\r
355             }\r
356 \r
357             if (this.evtMaxCount[w] < this.eventGrid[w][d].length) {\r
358                 this.evtMaxCount[w] = Math.min(max + 1, this.eventGrid[w][d].length);\r
359             }\r
360             return true;\r
361         },\r
362         this);\r
363     },\r
364 \r
365     // private\r
366     prepareEventGridSpans: function(evt, grid, w, d, days, allday) {\r
367         // this event spans multiple days/weeks, so we have to preprocess\r
368         // the events and store special span events as placeholders so that\r
369         // the render routine can build the necessary TD spans correctly.\r
370         var w1 = w,\r
371         d1 = d,\r
372         row = this.findEmptyRowIndex(w, d, allday),\r
373         dt = this.viewStart.clone();\r
374 \r
375         var start = {\r
376             event: evt,\r
377             isSpan: true,\r
378             isSpanStart: true,\r
379             spanLeft: false,\r
380             spanRight: (d == 6)\r
381         };\r
382         grid[w][d] = grid[w][d] || [];\r
383         grid[w][d][row] = start;\r
384 \r
385         while (--days) {\r
386             dt = dt.add(Date.DAY, 1);\r
387             if (dt > this.viewEnd) {\r
388                 break;\r
389             }\r
390             if (++d1 > 6) {\r
391                 // reset counters to the next week\r
392                 d1 = 0;\r
393                 w1++;\r
394                 row = this.findEmptyRowIndex(w1, 0);\r
395             }\r
396             grid[w1] = grid[w1] || [];\r
397             grid[w1][d1] = grid[w1][d1] || [];\r
398 \r
399             grid[w1][d1][row] = {\r
400                 event: evt,\r
401                 isSpan: true,\r
402                 isSpanStart: (d1 == 0),\r
403                 spanLeft: (w1 > w) && (d1 % 7 == 0),\r
404                 spanRight: (d1 == 6) && (days > 1)\r
405             };\r
406         }\r
407     },\r
408 \r
409     // private\r
410     findEmptyRowIndex: function(w, d, allday) {\r
411         var grid = allday ? this.allDayGrid: this.eventGrid,\r
412         day = grid[w] ? grid[w][d] || [] : [],\r
413         i = 0,\r
414         ln = day.length;\r
415 \r
416         for (; i < ln; i++) {\r
417             if (day[i] == null) {\r
418                 return i;\r
419             }\r
420         }\r
421         return ln;\r
422     },\r
423 \r
424     // private\r
425     renderTemplate: function() {\r
426         if (this.tpl) {\r
427             this.tpl.overwrite(this.el, this.getParams());\r
428             this.lastRenderStart = this.viewStart.clone();\r
429             this.lastRenderEnd = this.viewEnd.clone();\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.refresh();\r
447     },\r
448 \r
449     // private\r
450     onInitDrag: function() {\r
451         this.fireEvent('initdrag', this);\r
452     },\r
453 \r
454     // private\r
455     onEventDrop: function(rec, dt) {\r
456         if (Ext.calendar.Date.compare(rec.data[Ext.calendar.EventMappings.StartDate.name], dt) === 0) {\r
457             // no changes\r
458             return;\r
459         }\r
460         var diff = dt.getTime() - rec.data[Ext.calendar.EventMappings.StartDate.name].getTime();\r
461         rec.set(Ext.calendar.EventMappings.StartDate.name, dt);\r
462         rec.set(Ext.calendar.EventMappings.EndDate.name, rec.data[Ext.calendar.EventMappings.EndDate.name].add(Date.MILLI, diff));\r
463 \r
464         this.fireEvent('eventmove', this, rec);\r
465     },\r
466 \r
467     // private\r
468     onCalendarEndDrag: function(start, end, onComplete) {\r
469         // set this flag for other event handlers that might conflict while we're waiting\r
470         this.dragPending = true;\r
471 \r
472         // have to wait for the user to save or cancel before finalizing the dd interation\r
473         var o = {};\r
474         o[Ext.calendar.EventMappings.StartDate.name] = start;\r
475         o[Ext.calendar.EventMappings.EndDate.name] = end;\r
476 \r
477         this.fireEvent('rangeselect', this, o, this.onCalendarEndDragComplete.createDelegate(this, [onComplete]));\r
478     },\r
479 \r
480     // private\r
481     onCalendarEndDragComplete: function(onComplete) {\r
482         // callback for the drop zone to clean up\r
483         onComplete();\r
484         // clear flag for other events to resume normally\r
485         this.dragPending = false;\r
486     },\r
487 \r
488     // private\r
489     onUpdate: function(ds, rec, operation) {\r
490         if (this.monitorStoreEvents === false) {\r
491             return;\r
492         }\r
493         if (operation == Ext.data.Record.COMMIT) {\r
494             this.refresh();\r
495             if (this.enableFx && this.enableUpdateFx) {\r
496                 this.doUpdateFx(this.getEventEls(rec.data[Ext.calendar.EventMappings.EventId.name]), {\r
497                     scope: this\r
498                 });\r
499             }\r
500         }\r
501     },\r
502 \r
503 \r
504     doUpdateFx: function(els, o) {\r
505         this.highlightEvent(els, null, o);\r
506     },\r
507 \r
508     // private\r
509     onAdd: function(ds, records, index) {\r
510         if (this.monitorStoreEvents === false) {\r
511             return;\r
512         }\r
513         var rec = records[0];\r
514         this.tempEventId = rec.id;\r
515         this.refresh();\r
516 \r
517         if (this.enableFx && this.enableAddFx) {\r
518             this.doAddFx(this.getEventEls(rec.data[Ext.calendar.EventMappings.EventId.name]), {\r
519                 scope: this\r
520             });\r
521         };\r
522     },\r
523 \r
524     doAddFx: function(els, o) {\r
525         els.fadeIn(Ext.apply(o, {\r
526             duration: 2\r
527         }));\r
528     },\r
529 \r
530     // private\r
531     onRemove: function(ds, rec) {\r
532         if (this.monitorStoreEvents === false) {\r
533             return;\r
534         }\r
535         if (this.enableFx && this.enableRemoveFx) {\r
536             this.doRemoveFx(this.getEventEls(rec.data[Ext.calendar.EventMappings.EventId.name]), {\r
537                 remove: true,\r
538                 scope: this,\r
539                 callback: this.refresh\r
540             });\r
541         }\r
542         else {\r
543             this.getEventEls(rec.data[Ext.calendar.EventMappings.EventId.name]).remove();\r
544             this.refresh();\r
545         }\r
546     },\r
547 \r
548     doRemoveFx: function(els, o) {\r
549         els.fadeOut(o);\r
550     },\r
551 \r
552     /**\r
553      * Visually highlights an event using {@link Ext.Fx#highlight} config options.\r
554      * If {@link #highlightEventActions} is false this method will have no effect.\r
555      * @param {Ext.CompositeElement} els The element(s) to highlight\r
556      * @param {Object} color (optional) The highlight color. Should be a 6 char hex \r
557      * color without the leading # (defaults to yellow: 'ffff9c')\r
558      * @param {Object} o (optional) Object literal with any of the {@link Ext.Fx} config \r
559      * options. See {@link Ext.Fx#highlight} for usage examples.\r
560      */\r
561     highlightEvent: function(els, color, o) {\r
562         if (this.enableFx) {\r
563             var c;\r
564             ! (Ext.isIE || Ext.isOpera) ?\r
565             els.highlight(color, o) :\r
566             // Fun IE/Opera handling:\r
567             els.each(function(el) {\r
568                 el.highlight(color, Ext.applyIf({\r
569                     attr: 'color'\r
570                 },\r
571                 o));\r
572                 c = el.child('.ext-cal-evm');\r
573                 if (c) {\r
574                     c.highlight(color, o);\r
575                 }\r
576             },\r
577             this);\r
578         }\r
579     },\r
580 \r
581     /**\r
582      * Retrieve an Event object's id from its corresponding node in the DOM.\r
583      * @param {String/Element/HTMLElement} el An {@link Ext.Element}, DOM node or id\r
584      */\r
585     getEventIdFromEl: function(el) {\r
586         el = Ext.get(el);\r
587         var id = el.id.split(this.eventElIdDelimiter)[1];\r
588         if (id.indexOf('-') > -1) {\r
589             //This id has the index of the week it is rendered in as the suffix.\r
590             //This allows events that span across weeks to still have reproducibly-unique DOM ids.\r
591             id = id.split('-')[0];\r
592         }\r
593         return id;\r
594     },\r
595 \r
596     // private\r
597     getEventId: function(eventId) {\r
598         if (eventId === undefined && this.tempEventId) {\r
599             eventId = this.tempEventId;\r
600         }\r
601         return eventId;\r
602     },\r
603 \r
604     /**\r
605      * \r
606      * @param {String} eventId\r
607      * @param {Boolean} forSelect\r
608      * @return {String} The selector class\r
609      */\r
610     getEventSelectorCls: function(eventId, forSelect) {\r
611         var prefix = forSelect ? '.': '';\r
612         return prefix + this.id + this.eventElIdDelimiter + this.getEventId(eventId);\r
613     },\r
614 \r
615     /**\r
616      * \r
617      * @param {String} eventId\r
618      * @return {Ext.CompositeElement} The matching CompositeElement of nodes\r
619      * that comprise the rendered event.  Any event that spans across a view \r
620      * boundary will contain more than one internal Element.\r
621      */\r
622     getEventEls: function(eventId) {\r
623         var els = Ext.select(this.getEventSelectorCls(this.getEventId(eventId), true), false, this.el.id);\r
624         return new Ext.CompositeElement(els);\r
625     },\r
626 \r
627     /**\r
628      * Returns true if the view is currently displaying today's date, else false.\r
629      * @return {Boolean} True or false\r
630      */\r
631     isToday: function() {\r
632         var today = new Date().clearTime().getTime();\r
633         return this.viewStart.getTime() <= today && this.viewEnd.getTime() >= today;\r
634     },\r
635 \r
636     // private\r
637     onDataChanged: function(store) {\r
638         this.refresh();\r
639     },\r
640 \r
641     // private\r
642     isEventVisible: function(evt) {\r
643         var start = this.viewStart.getTime(),\r
644         end = this.viewEnd.getTime(),\r
645         M = Ext.calendar.EventMappings,\r
646         evStart = (evt.data ? evt.data[M.StartDate.name] : evt[M.StartDate.name]).getTime(),\r
647         evEnd = (evt.data ? evt.data[M.EndDate.name] : evt[M.EndDate.name]).add(Date.SECOND, -1).getTime(),\r
648 \r
649         startsInRange = (evStart >= start && evStart <= end),\r
650         endsInRange = (evEnd >= start && evEnd <= end),\r
651         spansRange = (evStart < start && evEnd > end);\r
652 \r
653         return (startsInRange || endsInRange || spansRange);\r
654     },\r
655 \r
656     // private\r
657     isOverlapping: function(evt1, evt2) {\r
658         var ev1 = evt1.data ? evt1.data: evt1,\r
659         ev2 = evt2.data ? evt2.data: evt2,\r
660         M = Ext.calendar.EventMappings,\r
661         start1 = ev1[M.StartDate.name].getTime(),\r
662         end1 = ev1[M.EndDate.name].add(Date.SECOND, -1).getTime(),\r
663         start2 = ev2[M.StartDate.name].getTime(),\r
664         end2 = ev2[M.EndDate.name].add(Date.SECOND, -1).getTime();\r
665 \r
666         if (end1 < start1) {\r
667             end1 = start1;\r
668         }\r
669         if (end2 < start2) {\r
670             end2 = start2;\r
671         }\r
672 \r
673         var ev1startsInEv2 = (start1 >= start2 && start1 <= end2),\r
674         ev1EndsInEv2 = (end1 >= start2 && end1 <= end2),\r
675         ev1SpansEv2 = (start1 < start2 && end1 > end2);\r
676 \r
677         return (ev1startsInEv2 || ev1EndsInEv2 || ev1SpansEv2);\r
678     },\r
679 \r
680     getDayEl: function(dt) {\r
681         return Ext.get(this.getDayId(dt));\r
682     },\r
683 \r
684     getDayId: function(dt) {\r
685         if (Ext.isDate(dt)) {\r
686             dt = dt.format('Ymd');\r
687         }\r
688         return this.id + this.dayElIdDelimiter + dt;\r
689     },\r
690 \r
691     /**\r
692      * Returns the start date of the view, as set by {@link #setStartDate}. Note that this may not \r
693      * be the first date displayed in the rendered calendar -- to get the start and end dates displayed\r
694      * to the user use {@link #getViewBounds}.\r
695      * @return {Date} The start date\r
696      */\r
697     getStartDate: function() {\r
698         return this.startDate;\r
699     },\r
700 \r
701     /**\r
702      * Sets the start date used to calculate the view boundaries to display. The displayed view will be the \r
703      * earliest and latest dates that match the view requirements and contain the date passed to this function.\r
704      * @param {Date} dt The date used to calculate the new view boundaries\r
705      */\r
706     setStartDate: function(start, refresh) {\r
707         this.startDate = start.clearTime();\r
708         this.setViewBounds(start);\r
709         this.store.load({\r
710             params: {\r
711                 start: this.viewStart.format('m-d-Y'),\r
712                 end: this.viewEnd.format('m-d-Y')\r
713             }\r
714         });\r
715         if (refresh === true) {\r
716             this.refresh();\r
717         }\r
718         this.fireEvent('datechange', this, this.startDate, this.viewStart, this.viewEnd);\r
719     },\r
720 \r
721     // private\r
722     setViewBounds: function(startDate) {\r
723         var start = startDate || this.startDate,\r
724         offset = start.getDay() - this.startDay;\r
725 \r
726         switch (this.weekCount) {\r
727         case 0:\r
728         case 1:\r
729             this.viewStart = this.dayCount < 7 ? start: start.add(Date.DAY, -offset).clearTime(true);\r
730             this.viewEnd = this.viewStart.add(Date.DAY, this.dayCount || 7).add(Date.SECOND, -1);\r
731             return;\r
732 \r
733         case - 1:\r
734             // auto by month\r
735             start = start.getFirstDateOfMonth();\r
736             offset = start.getDay() - this.startDay;\r
737             if (offset < 0) {\r
738                 offset += 7;\r
739             }\r
740             this.viewStart = start.add(Date.DAY, -offset).clearTime(true);\r
741 \r
742             // start from current month start, not view start:\r
743             var end = start.add(Date.MONTH, 1).add(Date.SECOND, -1);\r
744             // fill out to the end of the week:\r
745             this.viewEnd = end.add(Date.DAY, 6 - end.getDay());\r
746             return;\r
747 \r
748         default:\r
749             this.viewStart = start.add(Date.DAY, -offset).clearTime(true);\r
750             this.viewEnd = this.viewStart.add(Date.DAY, this.weekCount * 7).add(Date.SECOND, -1);\r
751         }\r
752     },\r
753 \r
754     // private\r
755     getViewBounds: function() {\r
756         return {\r
757             start: this.viewStart,\r
758             end: this.viewEnd\r
759         };\r
760     },\r
761 \r
762     /* private\r
763      * Sort events for a single day for display in the calendar.  This sorts allday\r
764      * events first, then non-allday events are sorted either based on event start\r
765      * priority or span priority based on the value of {@link #spansHavePriority} \r
766      * (defaults to event start priority).\r
767      * @param {MixedCollection} evts A {@link Ext.util.MixedCollection MixedCollection}  \r
768      * of {@link #Ext.calendar.EventRecord EventRecord} objects\r
769      */\r
770     sortEventRecordsForDay: function(evts) {\r
771         if (evts.length < 2) {\r
772             return;\r
773         }\r
774         evts.sort('ASC',\r
775         function(evtA, evtB) {\r
776             var a = evtA.data,\r
777             b = evtB.data,\r
778             M = Ext.calendar.EventMappings;\r
779 \r
780             // Always sort all day events before anything else\r
781             if (a[M.IsAllDay.name]) {\r
782                 return - 1;\r
783             }\r
784             else if (b[M.IsAllDay.name]) {\r
785                 return 1;\r
786             }\r
787             if (this.spansHavePriority) {\r
788                 // This logic always weights span events higher than non-span events\r
789                 // (at the possible expense of start time order). This seems to\r
790                 // be the approach used by Google calendar and can lead to a more\r
791                 // visually appealing layout in complex cases, but event order is\r
792                 // not guaranteed to be consistent.\r
793                 var diff = Ext.calendar.Date.diffDays;\r
794                 if (diff(a[M.StartDate.name], a[M.EndDate.name]) > 0) {\r
795                     if (diff(b[M.StartDate.name], b[M.EndDate.name]) > 0) {\r
796                         // Both events are multi-day\r
797                         if (a[M.StartDate.name].getTime() == b[M.StartDate.name].getTime()) {\r
798                             // If both events start at the same time, sort the one\r
799                             // that ends later (potentially longer span bar) first\r
800                             return b[M.EndDate.name].getTime() - a[M.EndDate.name].getTime();\r
801                         }\r
802                         return a[M.StartDate.name].getTime() - b[M.StartDate.name].getTime();\r
803                     }\r
804                     return - 1;\r
805                 }\r
806                 else if (diff(b[M.StartDate.name], b[M.EndDate.name]) > 0) {\r
807                     return 1;\r
808                 }\r
809                 return a[M.StartDate.name].getTime() - b[M.StartDate.name].getTime();\r
810             }\r
811             else {\r
812                 // Doing this allows span and non-span events to intermingle but\r
813                 // remain sorted sequentially by start time. This seems more proper\r
814                 // but can make for a less visually-compact layout when there are\r
815                 // many such events mixed together closely on the calendar.\r
816                 return a[M.StartDate.name].getTime() - b[M.StartDate.name].getTime();\r
817             }\r
818         }.createDelegate(this));\r
819     },\r
820 \r
821     /**\r
822      * Updates the view to contain the passed date\r
823      * @param {Date} dt The date to display\r
824      */\r
825     moveTo: function(dt, noRefresh) {\r
826         if (Ext.isDate(dt)) {\r
827             this.setStartDate(dt);\r
828             if (noRefresh !== false) {\r
829                 this.refresh();\r
830             }\r
831             return this.startDate;\r
832         }\r
833         return dt;\r
834     },\r
835 \r
836     /**\r
837      * Updates the view to the next consecutive date(s)\r
838      */\r
839     moveNext: function(noRefresh) {\r
840         return this.moveTo(this.viewEnd.add(Date.DAY, 1));\r
841     },\r
842 \r
843     /**\r
844      * Updates the view to the previous consecutive date(s)\r
845      */\r
846     movePrev: function(noRefresh) {\r
847         var days = Ext.calendar.Date.diffDays(this.viewStart, this.viewEnd) + 1;\r
848         return this.moveDays( - days, noRefresh);\r
849     },\r
850 \r
851     /**\r
852      * Shifts the view by the passed number of months relative to the currently set date\r
853      * @param {Number} value The number of months (positive or negative) by which to shift the view\r
854      */\r
855     moveMonths: function(value, noRefresh) {\r
856         return this.moveTo(this.startDate.add(Date.MONTH, value), noRefresh);\r
857     },\r
858 \r
859     /**\r
860      * Shifts the view by the passed number of weeks relative to the currently set date\r
861      * @param {Number} value The number of weeks (positive or negative) by which to shift the view\r
862      */\r
863     moveWeeks: function(value, noRefresh) {\r
864         return this.moveTo(this.startDate.add(Date.DAY, value * 7), noRefresh);\r
865     },\r
866 \r
867     /**\r
868      * Shifts the view by the passed number of days relative to the currently set date\r
869      * @param {Number} value The number of days (positive or negative) by which to shift the view\r
870      */\r
871     moveDays: function(value, noRefresh) {\r
872         return this.moveTo(this.startDate.add(Date.DAY, value), noRefresh);\r
873     },\r
874 \r
875     /**\r
876      * Updates the view to show today\r
877      */\r
878     moveToday: function(noRefresh) {\r
879         return this.moveTo(new Date(), noRefresh);\r
880     },\r
881 \r
882     /**\r
883      * Sets the event store used by the calendar to display {@link Ext.calendar.EventRecord events}.\r
884      * @param {Ext.data.Store} store\r
885      */\r
886     setStore: function(store, initial) {\r
887         if (!initial && this.store) {\r
888             this.store.un("datachanged", this.onDataChanged, this);\r
889             this.store.un("add", this.onAdd, this);\r
890             this.store.un("remove", this.onRemove, this);\r
891             this.store.un("update", this.onUpdate, this);\r
892             this.store.un("clear", this.refresh, this);\r
893         }\r
894         if (store) {\r
895             store.on("datachanged", this.onDataChanged, this);\r
896             store.on("add", this.onAdd, this);\r
897             store.on("remove", this.onRemove, this);\r
898             store.on("update", this.onUpdate, this);\r
899             store.on("clear", this.refresh, this);\r
900         }\r
901         this.store = store;\r
902         if (store && store.getCount() > 0) {\r
903             this.refresh();\r
904         }\r
905     },\r
906 \r
907     getEventRecord: function(id) {\r
908         var idx = this.store.find(Ext.calendar.EventMappings.EventId.name, id);\r
909         return this.store.getAt(idx);\r
910     },\r
911 \r
912     getEventRecordFromEl: function(el) {\r
913         return this.getEventRecord(this.getEventIdFromEl(el));\r
914     },\r
915 \r
916     // private\r
917     getParams: function() {\r
918         return {\r
919             viewStart: this.viewStart,\r
920             viewEnd: this.viewEnd,\r
921             startDate: this.startDate,\r
922             dayCount: this.dayCount,\r
923             weekCount: this.weekCount,\r
924             title: this.getTitle()\r
925         };\r
926     },\r
927 \r
928     getTitle: function() {\r
929         return this.startDate.format('F Y');\r
930     },\r
931 \r
932     /*\r
933      * Shared click handling.  Each specific view also provides view-specific\r
934      * click handling that calls this first.  This method returns true if it\r
935      * can handle the click (and so the subclass should ignore it) else false.\r
936      */\r
937     onClick: function(e, t) {\r
938         var el = e.getTarget(this.eventSelector, 5);\r
939         if (el) {\r
940             var id = this.getEventIdFromEl(el);\r
941             this.fireEvent('eventclick', this, this.getEventRecord(id), el);\r
942             return true;\r
943         }\r
944     },\r
945 \r
946     // private\r
947     onMouseOver: function(e, t) {\r
948         if (this.trackMouseOver !== false && (this.dragZone == undefined || !this.dragZone.dragging)) {\r
949             if (!this.handleEventMouseEvent(e, t, 'over')) {\r
950                 this.handleDayMouseEvent(e, t, 'over');\r
951             }\r
952         }\r
953     },\r
954 \r
955     // private\r
956     onMouseOut: function(e, t) {\r
957         if (this.trackMouseOver !== false && (this.dragZone == undefined || !this.dragZone.dragging)) {\r
958             if (!this.handleEventMouseEvent(e, t, 'out')) {\r
959                 this.handleDayMouseEvent(e, t, 'out');\r
960             }\r
961         }\r
962     },\r
963 \r
964     // private\r
965     handleEventMouseEvent: function(e, t, type) {\r
966         var el = e.getTarget(this.eventSelector, 5, true),\r
967             rel,\r
968             els,\r
969             evtId;\r
970         if (el) {\r
971             rel = Ext.get(e.getRelatedTarget());\r
972             if (el == rel || el.contains(rel)) {\r
973                 return true;\r
974             }\r
975 \r
976             evtId = this.getEventIdFromEl(el);\r
977 \r
978             if (this.eventOverClass != '') {\r
979                 els = this.getEventEls(evtId);\r
980                 els[type == 'over' ? 'addClass': 'removeClass'](this.eventOverClass);\r
981             }\r
982             this.fireEvent('event' + type, this, this.getEventRecord(evtId), el);\r
983             return true;\r
984         }\r
985         return false;\r
986     },\r
987 \r
988     // private\r
989     getDateFromId: function(id, delim) {\r
990         var parts = id.split(delim);\r
991         return parts[parts.length - 1];\r
992     },\r
993 \r
994     // private\r
995     handleDayMouseEvent: function(e, t, type) {\r
996         t = e.getTarget('td', 3);\r
997         if (t) {\r
998             if (t.id && t.id.indexOf(this.dayElIdDelimiter) > -1) {\r
999                 var dt = this.getDateFromId(t.id, this.dayElIdDelimiter),\r
1000                 rel = Ext.get(e.getRelatedTarget()),\r
1001                 relTD,\r
1002                 relDate;\r
1003 \r
1004                 if (rel) {\r
1005                     relTD = rel.is('td') ? rel: rel.up('td', 3);\r
1006                     relDate = relTD && relTD.id ? this.getDateFromId(relTD.id, this.dayElIdDelimiter) : '';\r
1007                 }\r
1008                 if (!rel || dt != relDate) {\r
1009                     var el = this.getDayEl(dt);\r
1010                     if (el && this.dayOverClass != '') {\r
1011                         el[type == 'over' ? 'addClass': 'removeClass'](this.dayOverClass);\r
1012                     }\r
1013                     this.fireEvent('day' + type, this, Date.parseDate(dt, "Ymd"), el);\r
1014                 }\r
1015             }\r
1016         }\r
1017     },\r
1018 \r
1019     // private\r
1020     renderItems: function() {\r
1021         throw 'This method must be implemented by a subclass';\r
1022     }\r
1023 });\r