Upgrade to ExtJS 3.3.0 - Released 10/06/2010
[extjs.git] / examples / calendar / src / views / DayBodyView.js
1 /*!
2  * Ext JS Library 3.3.0
3  * Copyright(c) 2006-2010 Ext JS, Inc.
4  * licensing@extjs.com
5  * http://www.extjs.com/license
6  */
7 /**S
8  * @class Ext.calendar.DayBodyView
9  * @extends Ext.calendar.CalendarView
10  * <p>This is the scrolling container within the day and week views where non-all-day events are displayed.
11  * Normally you should not need to use this class directly -- instead you should use {@link Ext.calendar.DayView DayView}
12  * which aggregates this class and the {@link Ext.calendar.DayHeaderView DayHeaderView} into the single unified view
13  * presented by {@link Ext.calendar.CalendarPanel CalendarPanel}.</p>
14  * @constructor
15  * @param {Object} config The config object
16  */
17 Ext.calendar.DayBodyView = Ext.extend(Ext.calendar.CalendarView, {
18     //private
19     dayColumnElIdDelimiter: '-day-col-',
20
21     //private
22     initComponent: function() {
23         Ext.calendar.DayBodyView.superclass.initComponent.call(this);
24
25         this.addEvents({
26             /**
27              * @event eventresize
28              * Fires after the user drags the resize handle of an event to resize it
29              * @param {Ext.calendar.DayBodyView} this
30              * @param {Ext.calendar.EventRecord} rec The {@link Ext.calendar.EventRecord record} for the event that was resized
31              * containing the updated start and end dates
32              */
33             eventresize: true,
34             /**
35              * @event dayclick
36              * Fires after the user clicks within the day view container and not on an event element
37              * @param {Ext.calendar.DayBodyView} this
38              * @param {Date} dt The date/time that was clicked on
39              * @param {Boolean} allday True if the day clicked on represents an all-day box, else false. Clicks within the 
40              * DayBodyView always return false for this param.
41              * @param {Ext.Element} el The Element that was clicked on
42              */
43             dayclick: true
44         });
45     },
46
47     //private
48     initDD: function() {
49         var cfg = {
50             createText: this.ddCreateEventText,
51             moveText: this.ddMoveEventText,
52             resizeText: this.ddResizeEventText
53         };
54
55         this.el.ddScrollConfig = {
56             // scrolling is buggy in IE/Opera for some reason.  A larger vthresh
57             // makes it at least functional if not perfect
58             vthresh: Ext.isIE || Ext.isOpera ? 100: 40,
59             hthresh: -1,
60             frequency: 50,
61             increment: 100,
62             ddGroup: 'DayViewDD'
63         };
64         this.dragZone = new Ext.calendar.DayViewDragZone(this.el, Ext.apply({
65             view: this,
66             containerScroll: true
67         },
68         cfg));
69
70         this.dropZone = new Ext.calendar.DayViewDropZone(this.el, Ext.apply({
71             view: this
72         },
73         cfg));
74     },
75
76     //private
77     refresh: function() {
78         var top = this.el.getScroll().top;
79         this.prepareData();
80         this.renderTemplate();
81         this.renderItems();
82
83         // skip this if the initial render scroll position has not yet been set.
84         // necessary since IE/Opera must be deferred, so the first refresh will
85         // override the initial position by default and always set it to 0.
86         if (this.scrollReady) {
87             this.scrollTo(top);
88         }
89     },
90
91     /**
92      * Scrolls the container to the specified vertical position. If the view is large enough that
93      * there is no scroll overflow then this method will have no affect.
94      * @param {Number} y The new vertical scroll position in pixels 
95      * @param {Boolean} defer (optional) <p>True to slightly defer the call, false to execute immediately.</p> 
96      * <p>This method will automatically defer itself for IE and Opera (even if you pass false) otherwise
97      * the scroll position will not update in those browsers. You can optionally pass true, however, to
98      * force the defer in all browsers, or use your own custom conditions to determine whether this is needed.</p>
99      * <p>Note that this method should not generally need to be called directly as scroll position is managed internally.</p>
100      */
101     scrollTo: function(y, defer) {
102         defer = defer || (Ext.isIE || Ext.isOpera);
103         if (defer) {
104             (function() {
105                 this.el.scrollTo('top', y);
106                 this.scrollReady = true;
107             }).defer(10, this);
108         }
109         else {
110             this.el.scrollTo('top', y);
111             this.scrollReady = true;
112         }
113     },
114
115     // private
116     afterRender: function() {
117         if (!this.tpl) {
118             this.tpl = new Ext.calendar.DayBodyTemplate({
119                 id: this.id,
120                 dayCount: this.dayCount,
121                 showTodayText: this.showTodayText,
122                 todayText: this.todayText,
123                 showTime: this.showTime
124             });
125         }
126         this.tpl.compile();
127
128         this.addClass('ext-cal-body-ct');
129
130         Ext.calendar.DayBodyView.superclass.afterRender.call(this);
131
132         // default scroll position to 7am:
133         this.scrollTo(7 * 42);
134     },
135
136     // private
137     forceSize: Ext.emptyFn,
138
139     // private
140     onEventResize: function(rec, data) {
141         var D = Ext.calendar.Date,
142         start = Ext.calendar.EventMappings.StartDate.name,
143         end = Ext.calendar.EventMappings.EndDate.name;
144
145         if (D.compare(rec.data[start], data.StartDate) === 0 &&
146         D.compare(rec.data[end], data.EndDate) === 0) {
147             // no changes
148             return;
149         }
150         rec.set(start, data.StartDate);
151         rec.set(end, data.EndDate);
152
153         this.fireEvent('eventresize', this, rec);
154     },
155
156     // inherited docs
157     getEventBodyMarkup: function() {
158         if (!this.eventBodyMarkup) {
159             this.eventBodyMarkup = ['{Title}',
160             '<tpl if="_isReminder">',
161             '<i class="ext-cal-ic ext-cal-ic-rem">&nbsp;</i>',
162             '</tpl>',
163             '<tpl if="_isRecurring">',
164             '<i class="ext-cal-ic ext-cal-ic-rcr">&nbsp;</i>',
165             '</tpl>'
166             //                '<tpl if="spanLeft">',
167             //                    '<i class="ext-cal-spl">&nbsp;</i>',
168             //                '</tpl>',
169             //                '<tpl if="spanRight">',
170             //                    '<i class="ext-cal-spr">&nbsp;</i>',
171             //                '</tpl>'
172             ].join('');
173         }
174         return this.eventBodyMarkup;
175     },
176
177     // inherited docs
178     getEventTemplate: function() {
179         if (!this.eventTpl) {
180             this.eventTpl = !(Ext.isIE || Ext.isOpera) ?
181             new Ext.XTemplate(
182             '<div id="{_elId}" class="{_selectorCls} {_colorCls} ext-cal-evt ext-cal-evr" style="left: {_left}%; width: {_width}%; top: {_top}px; height: {_height}px;">',
183             '<div class="ext-evt-bd">', this.getEventBodyMarkup(), '</div>',
184             '<div class="ext-evt-rsz"><div class="ext-evt-rsz-h">&nbsp;</div></div>',
185             '</div>'
186             )
187             : new Ext.XTemplate(
188             '<div id="{_elId}" class="ext-cal-evt {_selectorCls} {_colorCls}-x" style="left: {_left}%; width: {_width}%; top: {_top}px;">',
189             '<div class="ext-cal-evb">&nbsp;</div>',
190             '<dl style="height: {_height}px;" class="ext-cal-evdm">',
191             '<dd class="ext-evt-bd">',
192             this.getEventBodyMarkup(),
193             '</dd>',
194             '<div class="ext-evt-rsz"><div class="ext-evt-rsz-h">&nbsp;</div></div>',
195             '</dl>',
196             '<div class="ext-cal-evb">&nbsp;</div>',
197             '</div>'
198             );
199             this.eventTpl.compile();
200         }
201         return this.eventTpl;
202     },
203
204     /**
205      * <p>Returns the XTemplate that is bound to the calendar's event store (it expects records of type
206      * {@link Ext.calendar.EventRecord}) to populate the calendar views with <strong>all-day</strong> events. 
207      * Internally this method by default generates different markup for browsers that support CSS border radius 
208      * and those that don't. This method can be overridden as needed to customize the markup generated.</p>
209      * <p>Note that this method calls {@link #getEventBodyMarkup} to retrieve the body markup for events separately
210      * from the surrounding container markup.  This provdes the flexibility to customize what's in the body without
211      * having to override the entire XTemplate. If you do override this method, you should make sure that your 
212      * overridden version also does the same.</p>
213      * @return {Ext.XTemplate} The event XTemplate
214      */
215     getEventAllDayTemplate: function() {
216         if (!this.eventAllDayTpl) {
217             var tpl,
218             body = this.getEventBodyMarkup();
219
220             tpl = !(Ext.isIE || Ext.isOpera) ?
221             new Ext.XTemplate(
222             '<div id="{_elId}" class="{_selectorCls} {_colorCls} {values.spanCls} ext-cal-evt ext-cal-evr" style="left: {_left}%; width: {_width}%; top: {_top}px; height: {_height}px;">',
223             body,
224             '</div>'
225             )
226             : new Ext.XTemplate(
227             '<div id="{_elId}" class="ext-cal-evt" style="left: {_left}%; width: {_width}%; top: {_top}px; height: {_height}px;">',
228             '<div class="{_selectorCls} {values.spanCls} {_colorCls} ext-cal-evo">',
229             '<div class="ext-cal-evm">',
230             '<div class="ext-cal-evi">',
231             body,
232             '</div>',
233             '</div>',
234             '</div></div>'
235             );
236             tpl.compile();
237             this.eventAllDayTpl = tpl;
238         }
239         return this.eventAllDayTpl;
240     },
241
242     // private
243     getTemplateEventData: function(evt) {
244         var selector = this.getEventSelectorCls(evt[Ext.calendar.EventMappings.EventId.name]),
245         data = {},
246         M = Ext.calendar.EventMappings;
247
248         this.getTemplateEventBox(evt);
249
250         data._selectorCls = selector;
251         data._colorCls = 'ext-color-' + evt[M.CalendarId.name] + (evt._renderAsAllDay ? '-ad': '');
252         data._elId = selector + (evt._weekIndex ? '-' + evt._weekIndex: '');
253         data._isRecurring = evt.Recurrence && evt.Recurrence != '';
254         data._isReminder = evt[M.Reminder.name] && evt[M.Reminder.name] != '';
255         var title = evt[M.Title.name];
256         data.Title = (evt[M.IsAllDay.name] ? '': evt[M.StartDate.name].format('g:ia ')) + (!title || title.length == 0 ? '(No title)': title);
257
258         return Ext.applyIf(data, evt);
259     },
260
261     // private
262     getTemplateEventBox: function(evt) {
263         var heightFactor = 0.7,
264             start = evt[Ext.calendar.EventMappings.StartDate.name],
265             end = evt[Ext.calendar.EventMappings.EndDate.name],
266             startMins = start.getHours() * 60 + start.getMinutes(),
267             endMins = end.getHours() * 60 + end.getMinutes(),
268             diffMins = endMins - startMins;
269
270         evt._left = 0;
271         evt._width = 100;
272         evt._top = Math.round(startMins * heightFactor) + 1;
273         evt._height = Math.max((diffMins * heightFactor) - 2, 15);
274     },
275
276     // private
277     renderItems: function() {
278         var day = 0,
279             evts = [],
280             ev,
281             d,
282             ct,
283             item,
284             i,
285             j,
286             l,
287             overlapCols,
288             prevCol,
289             colWidth,
290             evtWidth,
291             markup,
292             target;
293         for (; day < this.dayCount; day++) {
294             ev = emptyCells = skipped = 0;
295             d = this.eventGrid[0][day];
296             ct = d ? d.length: 0;
297
298             for (; ev < ct; ev++) {
299                 evt = d[ev];
300                 if (!evt) {
301                     continue;
302                 }
303                 item = evt.data || evt.event.data;
304                 if (item._renderAsAllDay) {
305                     continue;
306                 }
307                 Ext.apply(item, {
308                     cls: 'ext-cal-ev',
309                     _positioned: true
310                 });
311                 evts.push({
312                     data: this.getTemplateEventData(item),
313                     date: this.viewStart.add(Date.DAY, day)
314                 });
315             }
316         }
317
318         // overlapping event pre-processing loop
319         i = j = overlapCols = prevCol = 0;
320         l = evts.length;
321         for (; i < l; i++) {
322             evt = evts[i].data;
323             evt2 = null;
324             prevCol = overlapCols;
325             for (j = 0; j < l; j++) {
326                 if (i == j) {
327                     continue;
328                 }
329                 evt2 = evts[j].data;
330                 if (this.isOverlapping(evt, evt2)) {
331                     evt._overlap = evt._overlap == undefined ? 1: evt._overlap + 1;
332                     if (i < j) {
333                         if (evt._overcol === undefined) {
334                             evt._overcol = 0;
335                         }
336                         evt2._overcol = evt._overcol + 1;
337                         overlapCols = Math.max(overlapCols, evt2._overcol);
338                     }
339                 }
340             }
341         }
342
343         // rendering loop
344         for (i = 0; i < l; i++) {
345             evt = evts[i].data;
346             if (evt._overlap !== undefined) {
347                 colWidth = 100 / (overlapCols + 1);
348                 evtWidth = 100 - (colWidth * evt._overlap);
349
350                 evt._width = colWidth;
351                 evt._left = colWidth * evt._overcol;
352             }
353             markup = this.getEventTemplate().apply(evt);
354             target = this.id + '-day-col-' + evts[i].date.format('Ymd');
355
356             Ext.DomHelper.append(target, markup);
357         }
358
359         this.fireEvent('eventsrendered', this);
360     },
361
362     // private
363     getDayEl: function(dt) {
364         return Ext.get(this.getDayId(dt));
365     },
366
367     // private
368     getDayId: function(dt) {
369         if (Ext.isDate(dt)) {
370             dt = dt.format('Ymd');
371         }
372         return this.id + this.dayColumnElIdDelimiter + dt;
373     },
374
375     // private
376     getDaySize: function() {
377         var box = this.el.child('.ext-cal-day-col-inner').getBox();
378         return {
379             height: box.height,
380             width: box.width
381         };
382     },
383
384     // private
385     getDayAt: function(x, y) {
386         var sel = '.ext-cal-body-ct',
387         xoffset = this.el.child('.ext-cal-day-times').getWidth(),
388         viewBox = this.el.getBox(),
389         daySize = this.getDaySize(false),
390         relX = x - viewBox.x - xoffset,
391         dayIndex = Math.floor(relX / daySize.width),
392         // clicked col index
393         scroll = this.el.getScroll(),
394         row = this.el.child('.ext-cal-bg-row'),
395         // first avail row, just to calc size
396         rowH = row.getHeight() / 2,
397         // 30 minute increment since a row is 60 minutes
398         relY = y - viewBox.y - rowH + scroll.top,
399         rowIndex = Math.max(0, Math.ceil(relY / rowH)),
400         mins = rowIndex * 30,
401         dt = this.viewStart.add(Date.DAY, dayIndex).add(Date.MINUTE, mins),
402         el = this.getDayEl(dt),
403         timeX = x;
404
405         if (el) {
406             timeX = el.getLeft();
407         }
408
409         return {
410             date: dt,
411             el: el,
412             // this is the box for the specific time block in the day that was clicked on:
413             timeBox: {
414                 x: timeX,
415                 y: (rowIndex * 21) + viewBox.y - scroll.top,
416                 width: daySize.width,
417                 height: rowH
418             }
419         };
420     },
421
422     // private
423     onClick: function(e, t) {
424         if (this.dragPending || Ext.calendar.DayBodyView.superclass.onClick.apply(this, arguments)) {
425             // The superclass handled the click already so exit
426             return;
427         }
428         if (e.getTarget('.ext-cal-day-times', 3) !== null) {
429             // ignore clicks on the times-of-day gutter
430             return;
431         }
432         var el = e.getTarget('td', 3);
433         if (el) {
434             if (el.id && el.id.indexOf(this.dayElIdDelimiter) > -1) {
435                 var dt = this.getDateFromId(el.id, this.dayElIdDelimiter);
436                 this.fireEvent('dayclick', this, Date.parseDate(dt, 'Ymd'), true, Ext.get(this.getDayId(dt, true)));
437                 return;
438             }
439         }
440         var day = this.getDayAt(e.xy[0], e.xy[1]);
441         if (day && day.date) {
442             this.fireEvent('dayclick', this, day.date, false, null);
443         }
444     }
445 });
446
447 Ext.reg('daybodyview', Ext.calendar.DayBodyView);