3 * Copyright(c) 2006-2010 Sencha Inc.
5 * http://www.sencha.com/license
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
17 * @param {Object} config The config object
\r
19 Ext.calendar.CalendarView = Ext.extend(Ext.BoxComponent, {
\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
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
34 spansHavePriority: false,
\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
40 trackMouseOver: true,
\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
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
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
61 enableUpdateFx: false,
\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
68 enableRemoveFx: true,
\r
70 * @cfg {Boolean} enableDD
\r
71 * True to enable drag and drop in the calendar view (the default), false to disable it
\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
81 monitorResize: true,
\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
87 ddCreateEventText: 'Create event for {0}',
\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
93 ddMoveEventText: 'Move event to {0}',
\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
101 ddResizeEventText: 'Update event to {0}',
\r
103 //private properties -- do not override:
\r
106 eventSelector: '.ext-cal-evt',
\r
107 eventOverClass: 'ext-evt-over',
\r
108 eventElIdDelimiter: '-evt-',
\r
109 dayElIdDelimiter: '-day-',
\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
118 getEventBodyMarkup: Ext.emptyFn,
\r
119 // must be implemented by a subclass
\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
131 getEventTemplate: Ext.emptyFn,
\r
132 // must be implemented by a subclass
\r
134 initComponent: function() {
\r
135 this.setStartDate(this.startDate || new Date());
\r
137 Ext.calendar.CalendarView.superclass.initComponent.call(this);
\r
141 * @event eventsrendered
\r
142 * Fires after events are finished rendering in the view
\r
143 * @param {Ext.calendar.CalendarView} this
\r
145 eventsrendered: true,
\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
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
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
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
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
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
200 * Fires when a drag operation is initiated in the view
\r
201 * @param {Ext.calendar.CalendarView} this
\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
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
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
227 //eventdelete: true
\r
232 afterRender: function() {
\r
233 Ext.calendar.CalendarView.superclass.afterRender.call(this);
\r
235 this.renderTemplate();
\r
238 this.setStore(this.store, true);
\r
242 'mouseover': this.onMouseOver,
\r
243 'mouseout': this.onMouseOut,
\r
244 'click': this.onClick,
\r
245 'resize': this.onResize,
\r
249 this.el.unselectable();
\r
251 if (this.enableDD && this.initDD) {
\r
255 this.on('eventsrendered', this.forceSize);
\r
256 this.forceSize.defer(100, this);
\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
266 if (bd == null || hd == null) return;
\r
268 var headerHeight = hd.getHeight(),
\r
269 sz = this.el.parent().getSize();
\r
271 bd.setHeight(sz.height - headerHeight);
\r
275 refresh: function() {
\r
276 this.prepareData();
\r
277 this.renderTemplate();
\r
278 this.renderItems();
\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
287 prepareData: function() {
\r
288 var lastInMonth = this.startDate.getLastDateOfMonth(),
\r
291 dt = this.viewStart.clone(),
\r
292 weeks = this.weekCount < 1 ? 6: this.weekCount;
\r
294 this.eventGrid = [[]];
\r
295 this.allDayGrid = [[]];
\r
296 this.evtMaxCount = [];
\r
298 var evtsInView = this.store.queryBy(function(rec) {
\r
299 return this.isEventVisible(rec.data);
\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
309 this.eventGrid[w] = this.eventGrid[w] || [];
\r
310 this.allDayGrid[w] = this.allDayGrid[w] || [];
\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
321 this.sortEventRecordsForDay(evts);
\r
322 this.prepareEventGrid(evts, w, d);
\r
324 dt = dt.add(Date.DAY, 1);
\r
327 this.currentWeekCount = w;
\r
331 prepareEventGrid: function(evts, w, d) {
\r
333 dt = this.viewStart.clone(),
\r
334 max = this.maxEventsPerDay ? this.maxEventsPerDay: 999;
\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
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
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
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
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
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
372 row = this.findEmptyRowIndex(w, d, allday),
\r
373 dt = this.viewStart.clone();
\r
380 spanRight: (d == 6)
\r
382 grid[w][d] = grid[w][d] || [];
\r
383 grid[w][d][row] = start;
\r
386 dt = dt.add(Date.DAY, 1);
\r
387 if (dt > this.viewEnd) {
\r
391 // reset counters to the next week
\r
394 row = this.findEmptyRowIndex(w1, 0);
\r
396 grid[w1] = grid[w1] || [];
\r
397 grid[w1][d1] = grid[w1][d1] || [];
\r
399 grid[w1][d1][row] = {
\r
402 isSpanStart: (d1 == 0),
\r
403 spanLeft: (w1 > w) && (d1 % 7 == 0),
\r
404 spanRight: (d1 == 6) && (days > 1)
\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
416 for (; i < ln; i++) {
\r
417 if (day[i] == null) {
\r
425 renderTemplate: function() {
\r
427 this.tpl.overwrite(this.el, this.getParams());
\r
428 this.lastRenderStart = this.viewStart.clone();
\r
429 this.lastRenderEnd = this.viewEnd.clone();
\r
433 disableStoreEvents: function() {
\r
434 this.monitorStoreEvents = false;
\r
437 enableStoreEvents: function(refresh) {
\r
438 this.monitorStoreEvents = true;
\r
439 if (refresh === true) {
\r
445 onResize: function() {
\r
450 onInitDrag: function() {
\r
451 this.fireEvent('initdrag', this);
\r
455 onEventDrop: function(rec, dt) {
\r
456 if (Ext.calendar.Date.compare(rec.data[Ext.calendar.EventMappings.StartDate.name], dt) === 0) {
\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
464 this.fireEvent('eventmove', this, rec);
\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
472 // have to wait for the user to save or cancel before finalizing the dd interation
\r
474 o[Ext.calendar.EventMappings.StartDate.name] = start;
\r
475 o[Ext.calendar.EventMappings.EndDate.name] = end;
\r
477 this.fireEvent('rangeselect', this, o, this.onCalendarEndDragComplete.createDelegate(this, [onComplete]));
\r
481 onCalendarEndDragComplete: function(onComplete) {
\r
482 // callback for the drop zone to clean up
\r
484 // clear flag for other events to resume normally
\r
485 this.dragPending = false;
\r
489 onUpdate: function(ds, rec, operation) {
\r
490 if (this.monitorStoreEvents === false) {
\r
493 if (operation == Ext.data.Record.COMMIT) {
\r
495 if (this.enableFx && this.enableUpdateFx) {
\r
496 this.doUpdateFx(this.getEventEls(rec.data[Ext.calendar.EventMappings.EventId.name]), {
\r
504 doUpdateFx: function(els, o) {
\r
505 this.highlightEvent(els, null, o);
\r
509 onAdd: function(ds, records, index) {
\r
510 if (this.monitorStoreEvents === false) {
\r
513 var rec = records[0];
\r
514 this.tempEventId = rec.id;
\r
517 if (this.enableFx && this.enableAddFx) {
\r
518 this.doAddFx(this.getEventEls(rec.data[Ext.calendar.EventMappings.EventId.name]), {
\r
524 doAddFx: function(els, o) {
\r
525 els.fadeIn(Ext.apply(o, {
\r
531 onRemove: function(ds, rec) {
\r
532 if (this.monitorStoreEvents === false) {
\r
535 if (this.enableFx && this.enableRemoveFx) {
\r
536 this.doRemoveFx(this.getEventEls(rec.data[Ext.calendar.EventMappings.EventId.name]), {
\r
539 callback: this.refresh
\r
543 this.getEventEls(rec.data[Ext.calendar.EventMappings.EventId.name]).remove();
\r
548 doRemoveFx: function(els, o) {
\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
561 highlightEvent: function(els, color, o) {
\r
562 if (this.enableFx) {
\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
572 c = el.child('.ext-cal-evm');
\r
574 c.highlight(color, o);
\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
585 getEventIdFromEl: function(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
597 getEventId: function(eventId) {
\r
598 if (eventId === undefined && this.tempEventId) {
\r
599 eventId = this.tempEventId;
\r
606 * @param {String} eventId
\r
607 * @param {Boolean} forSelect
\r
608 * @return {String} The selector class
\r
610 getEventSelectorCls: function(eventId, forSelect) {
\r
611 var prefix = forSelect ? '.': '';
\r
612 return prefix + this.id + this.eventElIdDelimiter + this.getEventId(eventId);
\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
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
628 * Returns true if the view is currently displaying today's date, else false.
\r
629 * @return {Boolean} True or false
\r
631 isToday: function() {
\r
632 var today = new Date().clearTime().getTime();
\r
633 return this.viewStart.getTime() <= today && this.viewEnd.getTime() >= today;
\r
637 onDataChanged: function(store) {
\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
649 startsInRange = (evStart >= start && evStart <= end),
\r
650 endsInRange = (evEnd >= start && evEnd <= end),
\r
651 spansRange = (evStart < start && evEnd > end);
\r
653 return (startsInRange || endsInRange || spansRange);
\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
666 if (end1 < start1) {
\r
669 if (end2 < start2) {
\r
673 var ev1startsInEv2 = (start1 >= start2 && start1 <= end2),
\r
674 ev1EndsInEv2 = (end1 >= start2 && end1 <= end2),
\r
675 ev1SpansEv2 = (start1 < start2 && end1 > end2);
\r
677 return (ev1startsInEv2 || ev1EndsInEv2 || ev1SpansEv2);
\r
680 getDayEl: function(dt) {
\r
681 return Ext.get(this.getDayId(dt));
\r
684 getDayId: function(dt) {
\r
685 if (Ext.isDate(dt)) {
\r
686 dt = dt.format('Ymd');
\r
688 return this.id + this.dayElIdDelimiter + dt;
\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
697 getStartDate: function() {
\r
698 return this.startDate;
\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
706 setStartDate: function(start, refresh) {
\r
707 this.startDate = start.clearTime();
\r
708 this.setViewBounds(start);
\r
711 start: this.viewStart.format('m-d-Y'),
\r
712 end: this.viewEnd.format('m-d-Y')
\r
715 if (refresh === true) {
\r
718 this.fireEvent('datechange', this, this.startDate, this.viewStart, this.viewEnd);
\r
722 setViewBounds: function(startDate) {
\r
723 var start = startDate || this.startDate,
\r
724 offset = start.getDay() - this.startDay;
\r
726 switch (this.weekCount) {
\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
735 start = start.getFirstDateOfMonth();
\r
736 offset = start.getDay() - this.startDay;
\r
740 this.viewStart = start.add(Date.DAY, -offset).clearTime(true);
\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
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
755 getViewBounds: function() {
\r
757 start: this.viewStart,
\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
770 sortEventRecordsForDay: function(evts) {
\r
771 if (evts.length < 2) {
\r
775 function(evtA, evtB) {
\r
778 M = Ext.calendar.EventMappings;
\r
780 // Always sort all day events before anything else
\r
781 if (a[M.IsAllDay.name]) {
\r
784 else if (b[M.IsAllDay.name]) {
\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
802 return a[M.StartDate.name].getTime() - b[M.StartDate.name].getTime();
\r
806 else if (diff(b[M.StartDate.name], b[M.EndDate.name]) > 0) {
\r
809 return a[M.StartDate.name].getTime() - b[M.StartDate.name].getTime();
\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
818 }.createDelegate(this));
\r
822 * Updates the view to contain the passed date
\r
823 * @param {Date} dt The date to display
\r
825 moveTo: function(dt, noRefresh) {
\r
826 if (Ext.isDate(dt)) {
\r
827 this.setStartDate(dt);
\r
828 if (noRefresh !== false) {
\r
831 return this.startDate;
\r
837 * Updates the view to the next consecutive date(s)
\r
839 moveNext: function(noRefresh) {
\r
840 return this.moveTo(this.viewEnd.add(Date.DAY, 1));
\r
844 * Updates the view to the previous consecutive date(s)
\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
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
855 moveMonths: function(value, noRefresh) {
\r
856 return this.moveTo(this.startDate.add(Date.MONTH, value), noRefresh);
\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
863 moveWeeks: function(value, noRefresh) {
\r
864 return this.moveTo(this.startDate.add(Date.DAY, value * 7), noRefresh);
\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
871 moveDays: function(value, noRefresh) {
\r
872 return this.moveTo(this.startDate.add(Date.DAY, value), noRefresh);
\r
876 * Updates the view to show today
\r
878 moveToday: function(noRefresh) {
\r
879 return this.moveTo(new Date(), noRefresh);
\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
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
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
901 this.store = store;
\r
902 if (store && store.getCount() > 0) {
\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
912 getEventRecordFromEl: function(el) {
\r
913 return this.getEventRecord(this.getEventIdFromEl(el));
\r
917 getParams: function() {
\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
928 getTitle: function() {
\r
929 return this.startDate.format('F Y');
\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
937 onClick: function(e, t) {
\r
938 var el = e.getTarget(this.eventSelector, 5);
\r
940 var id = this.getEventIdFromEl(el);
\r
941 this.fireEvent('eventclick', this, this.getEventRecord(id), el);
\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
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
965 handleEventMouseEvent: function(e, t, type) {
\r
966 var el = e.getTarget(this.eventSelector, 5, true),
\r
971 rel = Ext.get(e.getRelatedTarget());
\r
972 if (el == rel || el.contains(rel)) {
\r
976 evtId = this.getEventIdFromEl(el);
\r
978 if (this.eventOverClass != '') {
\r
979 els = this.getEventEls(evtId);
\r
980 els[type == 'over' ? 'addClass': 'removeClass'](this.eventOverClass);
\r
982 this.fireEvent('event' + type, this, this.getEventRecord(evtId), el);
\r
989 getDateFromId: function(id, delim) {
\r
990 var parts = id.split(delim);
\r
991 return parts[parts.length - 1];
\r
995 handleDayMouseEvent: function(e, t, type) {
\r
996 t = e.getTarget('td', 3);
\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
1005 relTD = rel.is('td') ? rel: rel.up('td', 3);
\r
1006 relDate = relTD && relTD.id ? this.getDateFromId(relTD.id, this.dayElIdDelimiter) : '';
\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
1013 this.fireEvent('day' + type, this, Date.parseDate(dt, "Ymd"), el);
\r
1020 renderItems: function() {
\r
1021 throw 'This method must be implemented by a subclass';
\r