Merge branch '2015.11' into 2015.11-develop
[tine20] / tine20 / Calendar / js / DaysView.js
1 /* 
2  * Tine 2.0
3  * 
4  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
5  * @author      Cornelius Weiss <c.weiss@metaways.de>
6  * @copyright   Copyright (c) 2009-2015 Metaways Infosystems GmbH (http://www.metaways.de)
7  */
8
9 Ext.ns('Tine.Calendar');
10
11 /**
12  * @namespace   Tine.Calendar
13  * @class       Tine.Calendar.DaysView
14  * @extends     Ext.util.Observable
15  * Calendar view representing each day in a column
16  * @author      Cornelius Weiss <c.weiss@metaways.de>
17  * @constructor
18  * @param {Object} config
19  */
20 Tine.Calendar.DaysView = function(config){
21     Ext.apply(this, config);
22     Tine.Calendar.DaysView.superclass.constructor.call(this);
23     
24     this.printRenderer = Tine.Calendar.Printer.DaysViewRenderer;
25     
26     this.addEvents(
27         /**
28          * @event click
29          * fired if an event got clicked
30          * @param {Tine.Calendar.Model.Event} event
31          * @param {Ext.EventObject} e
32          */
33         'click',
34         /**
35          * @event contextmenu
36          * fired if an event got contextmenu 
37          * @param {Ext.EventObject} e
38          */
39         'contextmenu',
40         /**
41          * @event dblclick
42          * fired if an event got dblclicked
43          * @param {Tine.Calendar.Model.Event} event
44          * @param {Ext.EventObject} e
45          */
46         'dblclick',
47         /**
48          * @event changeView
49          * fired if user wants to change view
50          * @param {String} requested view name
51          * @param {mixed} start param of requested view
52          */
53         'changeView',
54         /**
55          * @event changePeriod
56          * fired when period changed
57          * @param {Object} period
58          */
59         'changePeriod',
60         /**
61          * @event addEvent
62          * fired when a new event got inserted
63          * 
64          * @param {Tine.Calendar.Model.Event} event
65          */
66         'addEvent',
67         /**
68          * @event updateEvent
69          * fired when an event go resised/moved
70          * 
71          * @param {Tine.Calendar.Model.Event} event
72          */
73         'updateEvent',
74         /**
75          * @event onBeforeAllDayScrollerResize
76          * fired when an the allDayArea gets resized
77          *
78          * @param {Tine.Calendar.Model.DaysView} this
79          * @param {number} heigt
80          */
81         'onBeforeAllDayScrollerResize'
82     );
83 };
84
85 Ext.extend(Tine.Calendar.DaysView, Ext.Container, {
86     /**
87      * @cfg {Date} startDate
88      * start date
89      */
90     startDate: new Date(),
91     /**
92      * @cfg {Number} numOfDays
93      * number of days to display
94      */
95     numOfDays: 4,
96     /**
97      * @cfg {String} (H:i) defaultStart
98      * generic scroll start of the (work) day
99      */
100     defaultStart: '08:00',
101     /**
102      * @cfg {String} (H:i) dayStart
103      * generic start of the (work) day
104      */
105     dayStart: '08:00',
106     /**
107      * @cfg {String} (H:i) dayEnd
108      * generic end of the (work) day
109      */
110     dayEnd: '18:00',
111     /**
112      * @cfg {Bool} cropDayTime
113      * crop times before and after dayStart and dayEnd
114      */
115     cropDayTime: false,
116     /**
117      *  @cfg {Integer} wheelIncrement
118      *  the number of pixels to increment on mouse wheel scrolling (defaults to 50)
119      */
120     wheelIncrement: 50,
121     /**
122      * @cfg {String} newEventSummary
123      * _('New Event')
124      */
125     newEventSummary: 'New Event',
126     /**
127      * @cfg {String} dayFormatString
128      * _('{0}, the {1}. of {2}')
129      */
130     dayFormatString: '{0}, the {1}. of {2}',
131     /**
132      * @cfg {Number} timeGranularity
133      * granularity of timegrid in minutes
134      */
135     timeGranularity: 30,
136     /**
137      * @cfg {Number} timeIncrement
138      * time increment for range adds/edits (minutes)
139      */
140     timeIncrement: 15,
141     /**
142      * @cfg {Number} timeVisible
143      * time visible in scrolling area (minutes)
144      */
145     timeVisible: '10:00',
146     /**
147      * @cfg {Boolean} denyDragOnMissingEditGrant
148      * deny drag action if edit grant for event is missing
149      */
150     denyDragOnMissingEditGrant: true,
151
152
153     /**
154      * store holding timescale
155      * @property {Ext.data.Store}
156      * @private
157      */
158     timeScale: null,
159     /**
160      * The amount of space to reserve for the scrollbar (defaults to 19 pixels)
161      * @property {Number}
162      * @private
163      */
164     scrollOffset: 19,
165     /**
166      * The time in milliseconds, a scroll should be delayed after using the mousewheel
167      * @property Number
168      * @private
169      */
170     scrollBuffer: 200,
171     /**
172      * The minmum all day height in px
173      * @property Number
174      * @private
175      */
176     minAllDayScrollerHight: 10,
177     /**
178      * @property {bool} editing
179      * @private
180      */
181     editing: false,
182     /**
183      * currently active event
184      * $type {Tine.Calendar.Model.Event}
185      */
186     activeEvent: null,
187     /**
188      * @property {Ext.data.Store}
189      * @private
190      */
191     ds: null,
192     
193     /**
194      * updates period to display
195      * @param {Array} period
196      */
197     updatePeriod: function(period) {
198         this.startDate = period.from;
199         
200         var tbar = this.findParentBy(function(c) {return c.getTopToolbar()}).getTopToolbar();
201         if (tbar) {
202             tbar.periodPicker.update(this.startDate);
203             this.startDate = tbar.periodPicker.getPeriod().from;
204         }
205         
206         this.endDate = this.startDate.add(Date.DAY, this.numOfDays+1);
207         
208         //this.parallelScrollerEventsRegistry = new Tine.Calendar.ParallelEventsRegistry({dtStart: this.startDate, dtEnd: this.endDate});
209         //this.parallelWholeDayEventsRegistry = new Tine.Calendar.ParallelEventsRegistry({dtStart: this.startDate, dtEnd: this.endDate});
210         //this.store.each(this.removeEvent, this);
211         
212         this.updateDayHeaders();
213         this.onBeforeScroll();
214         
215         this.fireEvent('changePeriod', period);
216     },
217     
218     /**
219      * init this view
220      */
221     initComponent: function() {
222         this.app = Tine.Tinebase.appMgr.get('Calendar');
223         
224         this.newEventSummary      =  this.app.i18n._hidden(this.newEventSummary);
225         this.dayFormatString      =  this.app.i18n._hidden(this.dayFormatString);
226         
227         this.startDate.setHours(0);
228         this.startDate.setMinutes(0);
229         this.startDate.setSeconds(0);
230         
231         this.endDate = this.startDate.add(Date.DAY, this.numOfDays+1);
232         
233         this.boxMinWidth = 60*this.numOfDays;
234         
235         this.parallelScrollerEventsRegistry = new Tine.Calendar.ParallelEventsRegistry({dtStart: this.startDate, dtEnd: this.endDate});
236         this.parallelWholeDayEventsRegistry = new Tine.Calendar.ParallelEventsRegistry({dtStart: this.startDate, dtEnd: this.endDate});
237
238         this.timeIncrement = parseInt(this.app.getRegistry().get('preferences').get('timeIncrement')) || this.timeIncrement;
239
240         this.initData(this.store);
241         
242         this.initTimeScale();
243         this.initTemplates();
244         
245         this.mon(Tine.Tinebase.appMgr, 'activate', this.onAppActivate, this);
246         
247         if (! this.selModel) {
248             this.selModel = this.selModel || new Tine.Calendar.EventSelectionModel();
249         }
250
251         this.onLayout = Function.createBuffered(this.unbufferedOnLayout, 100, this);
252
253         // apply preferences
254         var prefs = this.app.getRegistry().get('preferences'),
255             defaultStartTime = Date.parseDate(prefs.get('daysviewdefaultstarttime'), 'H:i'),
256             startTime = Date.parseDate(prefs.get('daysviewstarttime'), 'H:i'),
257             endTime = Date.parseDate(prefs.get('daysviewendtime'), 'H:i'),
258             timeVisible = Date.parseTimePart(prefs.get('daysviewtimevisible'), 'H:i');
259
260         this.dayStart = Ext.isDate(startTime) ? startTime : Date.parseDate(this.dayStart, 'H:i');
261         this.dayEnd = Ext.isDate(endTime) ? endTime : Date.parseDate(this.dayEnd, 'H:i');
262         // 00:00 in users timezone is a spechial case where the user expects
263         // something like 24:00 and not 00:00
264         if (this.dayEnd.format('H:i') == '00:00') {
265             this.dayEnd = this.dayEnd.add(Date.MINUTE, -1);
266         }
267
268         this.timeVisible = Ext.isDate(timeVisible) ? timeVisible : Date.parseTimePart(this.timeVisible, 'H:i');
269
270         this.cropDayTime = !! Tine.Tinebase.configManager.get('daysviewcroptime', 'Calendar');
271
272         if (this.cropDayTime) {
273             this.defaultStart = Ext.isDate(defaultStartTime) ? defaultStartTime : Date.parseDate(this.defaultStart, 'H:i');
274         } else {
275             this.defaultStart = this.dayStart;
276         }
277
278         this.wheelIncrement = Tine.Tinebase.configManager.get('daysviewwheelincrement', 'Calendar') || this.wheelIncrement;
279
280         Tine.Calendar.DaysView.superclass.initComponent.apply(this, arguments);
281     },
282     
283     /**
284      * @private
285      * @param {Ext.data.Store} ds
286      */
287     initData : function(ds){
288         if(this.store){
289             this.store.un("load", this.onLoad, this);
290             this.store.un("beforeload", this.onBeforeLoad, this);
291             this.store.un("add", this.onAdd, this);
292             this.store.un("remove", this.onRemove, this);
293             this.store.un("update", this.onUpdate, this);
294         }
295         if(ds){
296             ds.on("load", this.onLoad, this);
297             ds.on("beforeload", this.onBeforeLoad, this);
298             ds.on("add", this.onAdd, this);
299             ds.on("remove", this.onRemove, this);
300             ds.on("update", this.onUpdate, this);
301         }
302         this.store = ds;
303     },
304     /**
305      * inits time scale
306      * @private
307      */
308     initTimeScale: function() {
309         var data = [];
310         var scaleSize = Date.msDAY/(this.timeGranularity * Date.msMINUTE);
311         var baseDate = this.startDate.clone();
312         
313         var minutes;
314         for (var i=0; i<scaleSize; i++) {
315             minutes = i * this.timeGranularity;
316             data.push([i, minutes, minutes * Date.msMINUTE, baseDate.add(Date.MINUTE, minutes).format('H:i')]);
317         }
318         
319         this.timeScale = new Ext.data.SimpleStore({
320             fields: ['index', 'minutes', 'milliseconds', 'time'],
321             data: data,
322             id: 'index'
323         });
324     },
325     
326     initDropZone: function() {
327         this.dd = new Ext.dd.DropZone(this.mainWrap.dom, {
328             ddGroup: 'cal-event',
329
330             view: this,
331
332             notifyOver : function(dd, e, data) {
333                 var sourceEl = Ext.fly(data.sourceEl),
334                     sourceView = data.scope;
335
336                 sourceEl.setStyle({'border-style': 'dashed'});
337                 sourceEl.setOpacity(0.5);
338
339                 data.denyDrop = true;
340                 
341                 if (data.event) {
342                     var event = data.event;
343
344                     var targetDateTime = Tine.Calendar.DaysView.prototype.getTargetDateTime.call(data.scope, e);
345                     if (targetDateTime) {
346                         var dtString = targetDateTime.format(targetDateTime.is_all_day_event ? Ext.form.DateField.prototype.format : 'H:i');
347                         if (! event.data.is_all_day_event) {
348                             Ext.fly(dd.proxy.el.query('div[class=cal-daysviewpanel-event-header-inner]')[0]).update(dtString);
349                         }
350
351                         if (event.get('editGrant')) {
352                             data.denyDrop = this.view == sourceView && Math.abs(targetDateTime.getTime() - event.get('dtstart').getTime()) < Date.msMINUTE;
353
354                             if (data.dtstartLimit && targetDateTime.getTimePart() > data.dtstartLimit) {
355                                 data.denyDrop = true;
356                             }
357
358                             return  data.denyDrop ? 'cal-daysviewpanel-event-drop-nodrop' : 'cal-daysviewpanel-event-drop-ok';
359                         }
360                     }
361                 }
362                 
363                 return 'cal-daysviewpanel-event-drop-nodrop';
364             },
365             
366             notifyOut : function() {
367                 //delete this.grid;
368             },
369             
370             notifyDrop : function(dd, e, data) {
371                 var v = data.scope,
372                     targetDate = v.getTargetDateTime(e);
373
374                 if (targetDate) {
375                     var event = data.event,
376                         originalDuration = (event.get('dtend').getTime() - event.get('dtstart').getTime()) / Date.msMINUTE;
377
378                     if (data.denyDrop) {
379                         return false;
380                     }
381
382                     event.beginEdit();
383                     event.set('dtstart', targetDate);
384
385                     if (! event.get('is_all_day_event') && targetDate.is_all_day_event && event.duration < Date.msDAY) {
386                         // draged from scroller -> dropped to allDay and duration less than a day
387                         event.set('dtend', targetDate.add(Date.DAY, 1).add(Date.SECOND, -1));
388                     } else if (event.get('is_all_day_event') && !targetDate.is_all_day_event) {
389                         // draged from allDay -> droped to scroller will be resetted to hone hour
390                         event.set('dtend', targetDate.add(Date.MINUTE, Tine.Calendar.Model.Event.getMeta('defaultEventDuration')));
391                     } else {
392                         event.set('dtend', targetDate.add(Date.MINUTE, originalDuration));
393                     }
394
395                     event.set('is_all_day_event', targetDate.is_all_day_event);
396
397
398                     // change attendee in split view
399                     if (this.view.ownerCt.attendee) {
400                         var attendeeStore = Tine.Calendar.Model.Attender.getAttendeeStore(event.get('attendee')),
401                             sourceAttendee = Tine.Calendar.Model.Attender.getAttendeeStore.getAttenderRecord(attendeeStore, event.view.ownerCt.attendee),
402                             destinationAttendee = Tine.Calendar.Model.Attender.getAttendeeStore.getAttenderRecord(attendeeStore, this.view.ownerCt.attendee);
403
404                         if (! destinationAttendee) {
405                             destinationAttendee = new Tine.Calendar.Model.Attender(this.view.ownerCt.attendee.data);
406
407                             attendeeStore.remove(sourceAttendee);
408                             attendeeStore.add(destinationAttendee);
409
410                             Tine.Calendar.Model.Attender.getAttendeeStore.getData(attendeeStore, event);
411                         }
412                     }
413
414                     event.endEdit();
415                     v.fireEvent('updateEvent', event);
416                 }
417                 
418                 return !!targetDate;
419             }
420         });
421     },
422     
423     /**
424      * @private
425      */
426     initDragZone: function() {
427         this.scroller.ddScrollConfig = {
428             hthresh: -1,
429             frequency: 500
430         };
431         Ext.dd.ScrollManager.register(this.scroller);
432         
433         // init dragables
434         this.dragZone = new Ext.dd.DragZone(this.el, {
435             ddGroup: 'cal-event',
436             view: this,
437             scroll: false,
438             containerScroll: true,
439             
440             getDragData: function(e) {
441                 // adjust scrollConfig
442                 var scrollUnit = this.view.getTimeOffset(this.view.timeIncrement);
443                 this.view.scroller.ddScrollConfig.vthresh = scrollUnit *2;
444                 this.view.scroller.ddScrollConfig.increment =  scrollUnit * 4;
445
446                 var selected = this.view.getSelectionModel().getSelectedEvents();
447                 
448                 var eventEl = e.getTarget('div.cal-daysviewpanel-event', 10);
449                 if (eventEl) {
450                     var parts = eventEl.id.split(':');
451                     var event = this.view.store.getById(parts[1]);
452                     
453                     // don't allow dragging of dirty events
454                     // don't allow dragging with missing edit grant
455                     if (! event || event.dirty || (this.view.denyDragOnMissingEditGrant && ! event.get('editGrant'))) {
456                         return;
457                     }
458                     
459                     // we need to clone an event with summary in
460                     var eventEl = Ext.get(event.ui.domIds[0]),
461                         eventBox = eventEl.getBox(),
462                         d = eventEl.dom.cloneNode(true);
463
464                     d.id = Ext.id();
465
466                     if (event.get('is_all_day_event')) {
467                         Ext.fly(d).setTop(-2);
468                         Ext.fly(d).setLeft(0);
469                         Ext.fly(d).setWidth(eventBox.width);
470                     } else {
471                         var width = (Ext.fly(this.view.dayCols[0]).getWidth() * 0.9);
472                         Ext.fly(d).setTop(0);
473                         Ext.fly(d).setWidth(width);
474                         Ext.fly(d).setHeight(eventBox.height);
475                     }
476
477                     return {
478                         scope: this.view,
479                         sourceEl: eventEl,
480                         event: event,
481                         ddel: d,
482                         selections: this.view.getSelectionModel().getSelectedEvents(),
483                         dtstartLimit: this.view.cropDayTime ? this.view.dayEnd.getTimePart() - event.duration : false
484                     }
485                 }
486             },
487             
488             getRepairXY: function(e, dd) {
489                 Ext.fly(this.dragData.sourceEl).setStyle({'border-style': 'solid'});
490                 Ext.fly(this.dragData.sourceEl).setOpacity(1, 1);
491                 
492                 return Ext.fly(this.dragData.sourceEl).getXY();
493             }
494         });
495     },
496     
497     /**
498      * renders the view
499      */
500     onRender: function(container, position) {
501         Tine.Calendar.DaysView.superclass.onRender.apply(this, arguments);
502         
503         this.templates.master.append(this.el.dom, {
504             header: this.templates.header.applyTemplate({
505                 daysHeader: this.getDayHeaders(),
506                 wholeDayCols: this.getWholeDayCols()
507             }),
508             body: this.templates.body.applyTemplate({
509                 timeRows: this.getTimeRows(),
510                 dayColumns: this.getDayColumns()
511             })
512         });
513         
514         this.initElements();
515         this.getSelectionModel().init(this);
516     },
517
518     /**
519      * fill the events into the view
520      */
521     afterRender: function() {
522         Tine.Calendar.DaysView.superclass.afterRender.apply(this, arguments);
523         
524         this.mon(this.el, 'click', this.onClick, this);
525         this.mon(this.el, 'dblclick', this.onDblClick, this);
526         this.mon(this.el, 'contextmenu', this.onContextMenu, this);
527         this.mon(this.el, 'mousedown', this.onMouseDown, this);
528         this.mon(this.el, 'mouseup', this.onMouseUp, this);
529         this.mon(this.el, 'keydown', this.onKeyDown, this);
530         
531         this.initDropZone();
532         this.initDragZone();
533         
534         this.updatePeriod({from: this.startDate});
535
536         // apply os specific scrolling space
537         Ext.fly(this.innerHd.firstChild.firstChild).setStyle('margin-right', Ext.getScrollBarWidth() + 'px');
538         
539         // crop daytime
540         if (this.cropDayTime) {
541             this.cropper.setStyle('overflow', 'hidden');
542             this.scroller.addClass('cal-daysviewpanel-body-cropDayTime');
543         }
544
545         if (this.store.getCount()) {
546             this.onLoad.defer(100, this);
547         }
548
549         // scrollTo initial position
550         this.isScrolling = true;
551
552         try {
553             this.scrollTo.defer(500, this, [this.defaultStart]);
554         } catch (e) {
555             this.scrollTo();
556         }
557
558         this.rendered = true;
559     },
560     
561     scrollTo: function(time) {
562         time = Ext.isDate(time) ? time : new Date();
563         
564         var scrollTop = this.getTimeOffset(time);
565         if (this.cropDayTime) {
566             scrollTop = scrollTop - this.getTimeOffset(this.dayStart);
567         }
568
569         this.scroller.dom.scrollTop = scrollTop;
570     },
571
572     onMouseWheel: function(e) {
573         var d = e.getWheelDelta()*this.wheelIncrement*-1;
574         e.stopEvent();
575
576         var scrollTop = this.scroller.dom.scrollTop,
577             newTop = scrollTop + d,
578             sh = this.scroller.dom.scrollHeight-this.scroller.dom.clientHeight;
579
580         var s = Math.max(0, Math.min(sh, newTop));
581         if(s != scrollTop){
582             this.scroller.scrollTo('top', s);
583         }
584     },
585
586     onBeforeScroll: function() {
587         if (! this.isScrolling) {
588             this.isScrolling = true;
589
590             var topOffset = this.getHeightMinutes(this.scroller.dom.scrollTop);
591             if (topOffset) {
592                 this.lastScrollTime = this.dayStart.clearTime(true).add(Date.MINUTE, topOffset);
593             }
594
595             // walk all cols an hide hints
596             Ext.each(this.dayCols, function(dayCol, idx) {
597                 this.aboveHints.item(idx).setDisplayed(false);
598                 this.belowHints.item(idx).setDisplayed(false);
599             }, this);
600         }
601     },
602     
603     /**
604      * add hint if events are outside visible area
605      * 
606      * @param {} e
607      * @param {} t
608      * @param {} o
609      */
610     onScroll: function(e, t, o) {
611         // no arguments means programatic scroll (show/hide/...)
612         if (! arguments.length) {
613             var topTime = this.lastScrollTime || this.defaultStart,
614                 topOffset = this.getTimeOffset(topTime);
615
616             if (topOffset) {
617                 this.scroller.dom.scrollTop = this.getTimeOffset(topTime);
618             }
619         }
620
621         var visibleHeight = this.scroller.dom.clientHeight,
622             visibleStart  = this.scroller.dom.scrollTop - this.mainBody.dom.offsetTop,
623             visibleEnd    = visibleStart + visibleHeight,
624             vStartMinutes = this.getHeightMinutes(visibleStart),
625             vEndMinutes   = this.getHeightMinutes(visibleEnd);
626
627         Ext.each(this.dayCols, function(dayCol, idx) {
628             var dayColEl    = Ext.get(dayCol),
629                 dayStart    = this.startDate.add(Date.DAY, idx),
630                 aboveEvents = this.parallelScrollerEventsRegistry.getEvents(dayStart, dayStart.add(Date.MINUTE, vStartMinutes)),
631                 belowEvents = this.parallelScrollerEventsRegistry.getEvents(dayStart.add(Date.MINUTE, vEndMinutes), dayStart.add(Date.DAY, 1));
632
633             if (aboveEvents.length) {
634                 var aboveHint = this.aboveHints.item(idx);
635                 aboveHint.setTop(visibleStart + 5);
636                 if (!aboveHint.isVisible()) {
637                     aboveHint.fadeIn({duration: 1.6});
638                 }
639             }
640
641             if (belowEvents.length) {
642                 var belowHint = this.belowHints.item(idx);
643                 belowHint.setTop(visibleEnd - 14);
644                 if (!belowHint.isVisible()) {
645                     belowHint.fadeIn({duration: 1.6});
646                 }
647             }
648         }, this);
649
650         this.isScrolling = false;
651     },
652
653     /**
654      * renders a single event into this daysview
655      * @param {Tine.Calendar.Model.Event} event
656      * 
657      * @todo Add support vor Events spanning over a day boundary
658      */
659     insertEvent: function(event) {
660         event.ui = new Tine.Calendar.DaysViewEventUI(event);
661         event.ui.render(this);
662     },
663     
664     /**
665      * removes all events
666      */
667     removeAllEvents: function() {
668         this.store.each(function(event) {
669             if (event.ui) {
670                 event.ui.remove();
671             }
672         });
673     },
674     
675     /**
676      * removes a event from the dom
677      * @param {Tine.Calendar.Model.Event} event
678      */
679     removeEvent: function(event) {
680         if (event == this.activeEvent) {
681             this.activeEvent = null;
682         }
683
684         if(this.editing) {
685             this.abortCreateEvent(event);
686         }
687
688         if (event.ui) {
689             event.ui.remove();
690         }
691     },
692     
693     /**
694      * sets currentlcy active event
695      * 
696      * NOTE: active != selected
697      * @param {Tine.Calendar.Model.Event} event
698      */
699     setActiveEvent: function(event) {
700         this.activeEvent = event || null;
701     },
702     
703     /**
704      * gets currentlcy active event
705      * 
706      * @return {Tine.Calendar.Model.Event} event
707      */
708     getActiveEvent: function() {
709         return this.activeEvent;
710     },
711     
712     /**
713      * returns the selectionModel of the active panel
714      * @return {}
715      */
716     getSelectionModel: function() {
717         return this.selModel;
718     },
719     
720     /**
721      * creates a new event directly from this view
722      * @param {} event
723      */
724     createEvent: function(e, event) {
725         // only add range events if mouse is down long enough
726         if (this.editing || (event.isRangeAdd && ! this.mouseDown)) {
727             return;
728         }
729         
730         // insert event silently into store
731         this.editing = event;
732         this.store.suspendEvents();
733         this.store.add(event);
734         this.store.resumeEvents();
735         
736         // draw event
737         var registry = event.get('is_all_day_event') ? this.parallelWholeDayEventsRegistry : this.parallelScrollerEventsRegistry;
738         registry.register(event);
739         this.insertEvent(event);
740         //this.setActiveEvent(event);
741         this.onLayout();
742         
743         //var eventEls = event.ui.getEls();
744         //eventEls[0].setStyle({'border-style': 'dashed'});
745         //eventEls[0].setOpacity(0.5);
746         
747         // start sizing for range adds
748         if (event.isRangeAdd) {
749             // don't create events with very small duration
750             event.ui.resizeable.on('resize', function() {
751                 if (event.get('is_all_day_event')) {
752                     var keep = true;
753                 } else {
754                     var keep = (event.get('dtend').getTime() - event.get('dtstart').getTime()) / Date.msMINUTE >= this.timeIncrement;
755                 }
756                 
757                 if (keep) {
758                     this.startEditSummary(event);
759                 } else {
760                     this.abortCreateEvent(event);
761                 }
762             }, this);
763             
764             var rzPos = event.get('is_all_day_event') ? 'east' : 'south';
765             
766             if (Ext.isIE) {
767                 e.browserEvent = {type: 'mousedown'};
768             }
769             
770             event.ui.resizeable[rzPos].onMouseDown.call(event.ui.resizeable[rzPos], e);
771             //event.ui.resizeable.startSizing.defer(2000, event.ui.resizeable, [e, event.ui.resizeable[rzPos]]);
772         } else {
773             this.startEditSummary(event);
774         }
775     },
776     
777     abortCreateEvent: function(event) {
778         this.store.remove(event);
779         this.editing = false;
780     },
781     
782     startEditSummary: function(event) {
783         if (event.summaryEditor) {
784             return false;
785         }
786         
787         var eventEls = event.ui.getEls();
788         
789         var bodyCls = event.get('is_all_day_event') ? 'cal-daysviewpanel-wholedayevent-body' : 'cal-daysviewpanel-event-body';
790         event.summaryEditor = new Ext.form.TextArea({
791             event: event,
792             renderTo: eventEls[0].down('div[class=' + bodyCls + ']'),
793             width: event.ui.getEls()[0].getWidth() -12,
794             height: Math.max(12, event.ui.getEls()[0].getHeight() -18),
795             style: 'background-color: transparent; background: 0: border: 0; position: absolute; top: 0px;',
796             value: this.newEventSummary,
797             maxLength: 255,
798             maxLengthText: this.app.i18n._('The summary must not be longer than 255 characters.'),
799             minLength: 1,
800             minLengthText: this.app.i18n._('The summary must have at least 1 character.'),
801             enableKeyEvents: true,
802             listeners: {
803                 scope: this,
804                 render: function(field) {
805                     field.focus(true, 100);
806                 },
807                 blur: this.endEditSummary,
808                 specialkey: this.endEditSummary,
809                 keydown: this.endEditSummary
810             }
811             
812         });
813     },
814     
815     endEditSummary: function(field, e) {
816         var event   = field.event;
817         var summary = field.getValue();
818
819         if (! this.editing || this.validateMsg || !Ext.isDefined(e)) {
820             return;
821         }
822
823         // abort edit on ESC key
824         if (e && (e.getKey() == e.ESC)) {
825             this.abortCreateEvent(event);
826             return;
827         }
828
829         // only commit edit on Enter & blur
830         if (e && e.getKey() != e.ENTER) {
831             return;
832         }
833         
834         // Validate Summary maxLength
835         if (summary.length > field.maxLength) {
836             field.markInvalid();
837             this.validateMsg = Ext.Msg.alert(this.app.i18n._('Summary too Long'), field.maxLengthText, function(){
838                 field.focus();
839                 this.validateMsg = false;
840                 }, this);
841             return;
842         }
843
844         // Validate Summary minLength
845         if (!summary || summary.match(/^\s{1,}$/) || summary.length < field.minLength) {
846             field.markInvalid();
847             this.validateMsg = Ext.Msg.alert(this.app.i18n._('Summary too Short'), field.minLengthText, function(){
848                 field.focus();
849                 this.validateMsg = false;
850                 }, this);
851             return;
852         }
853
854         this.editing = false;
855         event.summaryEditor = false;
856         
857         event.set('summary', summary);
858         
859         this.store.suspendEvents();
860         this.store.remove(event);
861         this.store.resumeEvents();
862         
863         var registry = event.get('is_all_day_event') ? this.parallelWholeDayEventsRegistry : this.parallelScrollerEventsRegistry;
864         registry.unregister(event);
865         this.removeEvent(event);
866         
867         event.dirty = true;
868         this.store.add(event);
869         this.fireEvent('addEvent', event);
870     },
871     
872     onAppActivate: function(app) {
873         if (app === this.app) {
874             this.redrawWholeDayEvents();
875         }
876     },
877     
878     onResize: function() {
879         Tine.Calendar.DaysView.superclass.onResize.apply(this, arguments);
880         
881         this.updateDayHeaders();
882         this.redrawWholeDayEvents.defer(50, this);
883
884         this.unbufferedOnLayout();
885     },
886     
887     redrawWholeDayEvents: function() {
888         this.store.each(function(event) {
889             // check if event is currently visible by looking into ui.domIds
890             if (event.ui && event.ui.domIds.length > 0 && event.get('is_all_day_event')) {
891                 this.removeEvent(event);
892                 this.insertEvent(event);
893             }
894         }, this);
895     },
896     
897     onClick: function(e) {
898         // check for hint clicks first
899         var hint = e.getTarget('img[class^=cal-daysviewpanel-body-daycolumn-hint-]', 10, true);
900         if (hint) {
901             this.scroller.scroll(hint.hasClass('cal-daysviewpanel-body-daycolumn-hint-above') ? 't' : 'b', 10000, true);
902             return;
903         }
904         
905         var event = this.getTargetEvent(e);
906         if (event) {
907             this.fireEvent('click', event, e);
908         }
909     },
910     
911     onContextMenu: function(e) {
912         this.fireEvent('contextmenu', e);
913     },
914     
915     onKeyDown : function(e){
916         this.fireEvent("keydown", e);
917     },
918     
919     /**
920      * @private
921      */
922     onDblClick: function(e, target) {
923         e.stopEvent();
924         var event = this.getTargetEvent(e);
925         var dtStart = this.getTargetDateTime(e);
926         
927         if (event) {
928             this.fireEvent('dblclick', event, e);
929         } else if (dtStart && !this.editing) {
930             var newId = 'cal-daysviewpanel-new-' + Ext.id();
931             var dtend = dtStart.add(Date.MINUTE, Tine.Calendar.Model.Event.getMeta('defaultEventDuration'));
932             if (dtStart.is_all_day_event) {
933                 dtend = dtend.add(Date.HOUR, 23).add(Date.SECOND, -1);
934             }
935             
936             // do not create an event exceeding the crop day time limit
937             else if (this.cropDayTime) {
938                 var format = 'Hms';
939                 if (dtStart.format(format) >= this.dayEnd.format(format)) {
940                     return false;
941                 }
942                 
943                 if (dtend.format(format) >= this.dayEnd.format(format)) {
944                     dtend.setHours(this.dayEnd.getHours());
945                     dtend.setMinutes(this.dayEnd.getMinutes());
946                     dtend.setSeconds(this.dayEnd.getSeconds());
947                 }
948             }
949             
950             var event = new Tine.Calendar.Model.Event(Ext.apply(Tine.Calendar.Model.Event.getDefaultData(), {
951                 id: newId,
952                 dtstart: dtStart, 
953                 dtend: dtend,
954                 is_all_day_event: dtStart.is_all_day_event
955             }), newId);
956             
957             this.createEvent(e, event);
958             event.dirty = true;
959         } else if (target.className == 'cal-daysviewpanel-dayheader-day'){
960             var dayHeaders = Ext.DomQuery.select('div[class=cal-daysviewpanel-dayheader-day]', this.innerHd);
961             var date = this.startDate.add(Date.DAY, dayHeaders.indexOf(target));
962             this.fireEvent('changeView', 'day', date);
963         }
964     },
965     
966     /**
967      * @private
968      */
969     onMouseDown: function(e) {
970         // only care for left mouse button
971         if (e.button !== 0) {
972             return;
973         }
974         
975         if (! this.editing) {
976             this.focusEl.focus();
977         }
978         this.mouseDown = true;
979         
980         var targetEvent = this.getTargetEvent(e);
981         if (this.editing && this.editing.summaryEditor && (targetEvent != this.editing)) {
982             this.editing.summaryEditor.fireEvent('blur', this.editing.summaryEditor, null);
983             return;
984         }
985
986         var sm = this.getSelectionModel();
987         sm.select(targetEvent);
988         
989         var dtStart = this.getTargetDateTime(e),
990             dtEnd = this.getTargetDateTime(e, this.timeIncrement, 's');
991
992         if (dtStart) {
993             var newId = 'cal-daysviewpanel-new-' + Ext.id();
994             var event = new Tine.Calendar.Model.Event(Ext.apply(Tine.Calendar.Model.Event.getDefaultData(), {
995                 id: newId,
996                 dtstart: dtStart,
997                 dtend: dtStart.is_all_day_event ? dtStart.add(Date.HOUR, 24).add(Date.SECOND, -1) : dtEnd,
998                 is_all_day_event: dtStart.is_all_day_event
999             }), newId);
1000             event.isRangeAdd = true;
1001             event.dirty = true;
1002             
1003             e.stopEvent();
1004             this.createEvent.defer(100, this, [e, event]);
1005         }
1006     },
1007     
1008     /**
1009      * @private
1010      */
1011     onMouseUp: function() {
1012         this.mouseDown = false;
1013     },
1014     
1015     /**
1016      * @private
1017      */
1018     onBeforeEventResize: function(rz, e) {
1019         var parts = rz.el.id.split(':');
1020         var event = this.store.getById(parts[1]);
1021
1022         // @TODO compute max minutes also
1023         var maxHeight = 10000;
1024
1025         if (this.cropDayTime) {
1026             var maxMinutes = (this.dayEnd.getTimePart() - event.get('dtstart').getTimePart()) / Date.msMINUTE;
1027             maxHeight = this.getTimeOffset(maxMinutes);
1028         }
1029
1030         rz.heightIncrement = this.getTimeOffset(this.timeIncrement);
1031         rz.maxHeight = maxHeight,
1032
1033         rz.event = event;
1034         rz.originalHeight = rz.el.getHeight();
1035         rz.originalWidth  = rz.el.getWidth();
1036
1037         // NOTE: ext dosn't support move events via api
1038         rz.onMouseMove = rz.onMouseMove.createSequence(function() {
1039             var event = this.event;
1040             if (! event) {
1041                 //event already gone -> late event / busy brower?
1042                 return;
1043             }
1044             var ui = event.ui;
1045             var rzInfo = ui.getRzInfo(this);
1046             
1047             this.durationEl.update(rzInfo.dtend.format(event.get('is_all_day_event') ? Ext.form.DateField.prototype.format : 'H:i'));
1048         }, rz);
1049         
1050         event.ui.markDirty();
1051         
1052         // NOTE: Ext keeps proxy if element is not destroyed (diff !=0)
1053         if (! rz.durationEl) {
1054             rz.durationEl = rz.el.insertFirst({
1055                 'class': 'cal-daysviewpanel-event-rzduration',
1056                 'style': 'position: absolute; bottom: 3px; right: 2px; z-index: 1000;'
1057             });
1058         }
1059         rz.durationEl.update(event.get('dtend').format(event.get('is_all_day_event') ? Ext.form.DateField.prototype.format : 'H:i'));
1060         
1061         if (event) {
1062             this.getSelectionModel().select(event);
1063         } else {
1064             this.getSelectionModel().clearSelections();
1065         }
1066     },
1067     
1068     /**
1069      * @private
1070      */
1071     onEventResize: function(rz, width, height) {
1072         var event = rz.event;
1073         
1074         if (! event) {
1075             //event already gone -> late event / busy brower?
1076             return;
1077         }
1078         
1079         var rzInfo = event.ui.getRzInfo(rz, width, height);
1080         
1081         if (rzInfo.diff != 0) {
1082             if (rzInfo.duration > 0) {
1083                 event.set('dtend', rzInfo.dtend);
1084             } else {
1085                 // force event length to at least 1 minute
1086                 var date = new Date(event.get('dtstart').getTime());
1087                 date.setMinutes(date.getMinutes() + 1);
1088                 event.set('dtend', date);
1089             }
1090         }
1091         
1092         if (event.summaryEditor) {
1093             event.summaryEditor.setHeight(event.ui.getEls()[0].getHeight() -18);
1094         }
1095         
1096         // don't fire update events on rangeAdd
1097         if (rzInfo.diff != 0 && event != this.editing && ! event.isRangeAdd) {
1098             this.fireEvent('updateEvent', event);
1099         } else {
1100             event.ui.clearDirty();
1101         }
1102     },
1103     
1104     /**
1105      * @private
1106      */
1107     onUpdate : function(ds, event){
1108         // don't update events while being created
1109         if (event.get('id').match(/new/)) {
1110             return;
1111         }
1112         
1113         // relayout original context
1114         var originalRegistry = (event.modified.hasOwnProperty('is_all_day_event') ? event.modified.is_all_day_event : event.get('is_all_day_event')) ? 
1115             this.parallelWholeDayEventsRegistry : 
1116             this.parallelScrollerEventsRegistry;
1117
1118         var registry = event.get('is_all_day_event') ? this.parallelWholeDayEventsRegistry : this.parallelScrollerEventsRegistry;
1119         var originalDtstart = event.modified.hasOwnProperty('dtstart') ? event.modified.dtstart : event.get('dtstart');
1120         var originalDtend = event.modified.hasOwnProperty('dtend') ? event.modified.dtend : event.get('dtend');
1121
1122         var originalParallels = originalRegistry.getEvents(originalDtstart, originalDtend);
1123         for (var j=0; j<originalParallels.length; j++) {
1124             this.removeEvent(originalParallels[j]);
1125         }
1126         originalRegistry.unregister(event);
1127         
1128         var originalParallels = originalRegistry.getEvents(originalDtstart, originalDtend);
1129         for (var j=0; j<originalParallels.length; j++) {
1130             this.insertEvent(originalParallels[j]);
1131         }
1132         
1133         // relayout actual context
1134         var parallelEvents = registry.getEvents(event.get('dtstart'), event.get('dtend'));
1135         for (var j=0; j<parallelEvents.length; j++) {
1136             this.removeEvent(parallelEvents[j]);
1137         }
1138         
1139         registry.register(event);
1140         var parallelEvents = registry.getEvents(event.get('dtstart'), event.get('dtend'));
1141         for (var j=0; j<parallelEvents.length; j++) {
1142             this.insertEvent(parallelEvents[j]);
1143         }
1144         
1145         this.setActiveEvent(this.getActiveEvent());
1146         this.onLayout();
1147     },
1148
1149     /**
1150      * @private
1151      */
1152     onAdd : function(ds, records, index){
1153         for (var i=0; i<records.length; i++) {
1154             var event = records[i];
1155             
1156             var registry = event.get('is_all_day_event') ? this.parallelWholeDayEventsRegistry : this.parallelScrollerEventsRegistry;
1157             registry.register(event);
1158             
1159             var parallelEvents = registry.getEvents(event.get('dtstart'), event.get('dtend'));
1160             
1161             for (var j=0; j<parallelEvents.length; j++) {
1162                 this.removeEvent(parallelEvents[j]);
1163                 this.insertEvent(parallelEvents[j]);
1164             }
1165             
1166             //this.setActiveEvent(event);
1167         }
1168         
1169         this.onLayout();
1170     },
1171
1172     /**
1173      * @private
1174      */
1175     onRemove : function(ds, event, index, isUpdate) {
1176         if (!event || index == -1) {
1177             return;
1178         }
1179         
1180         if(isUpdate !== true){
1181             //this.fireEvent("beforeeventremoved", this, index, record);
1182         }
1183         var registry = event.get('is_all_day_event') ? this.parallelWholeDayEventsRegistry : this.parallelScrollerEventsRegistry;
1184         registry.unregister(event);
1185         this.removeEvent(event);
1186         this.getSelectionModel().unselect(event);
1187         this.onLayout();
1188     },
1189     
1190     onBeforeLoad: function(store, options) {
1191         if (! options.refresh) {
1192             this.store.each(this.removeEvent, this);
1193             this.transitionEvents = [];
1194         } else {
1195             this.transitionEvents = this.store.data.items;
1196         }
1197     },
1198     
1199     /**
1200      * @private
1201      */
1202     onLoad : function() {
1203         if(! this.rendered){
1204             return;
1205         }
1206         
1207         // remove old events
1208         Ext.each(this.transitionEvents, this.removeEvent, this);
1209         
1210         // setup registry
1211         this.parallelScrollerEventsRegistry = new Tine.Calendar.ParallelEventsRegistry({dtStart: this.startDate, dtEnd: this.endDate});
1212         this.parallelWholeDayEventsRegistry = new Tine.Calendar.ParallelEventsRegistry({dtStart: this.startDate, dtEnd: this.endDate});
1213         
1214         // todo: sort generic?
1215         this.store.fields = Tine.Calendar.Model.Event.prototype.fields;
1216         this.store.sortInfo = {field: 'dtstart', direction: 'ASC'};
1217         this.store.applySort();
1218         
1219         this.store.each(function(event) {
1220             var registry = event.get('is_all_day_event') ? this.parallelWholeDayEventsRegistry : this.parallelScrollerEventsRegistry;
1221             registry.register(event);
1222         }, this);
1223         
1224         // put the events in
1225         this.store.each(this.insertEvent, this);
1226         
1227         this.onLayout();
1228     },
1229     
1230     /**
1231      * print wrapper
1232      */
1233     print: function(printMode) {
1234         var renderer = new this.printRenderer({printMode: printMode});
1235         renderer.print(this);
1236     },
1237     
1238     hex2dec: function(hex) {
1239         var dec = 0;
1240         hex = hex.toString();
1241         var length = hex.length, multiplier, digit;
1242         for (var i=0; i<length; i++) {
1243             
1244             multiplier = Math.pow(16, (Math.abs(i - hex.length)-1));
1245             digit = parseInt(hex.toString().charAt([i]), 10);
1246             if (isNaN(digit)) {
1247                 switch (hex.toString().charAt([i]).toUpperCase()) {
1248                     case 'A': digit = 10;  break;
1249                     case 'B': digit = 11;  break;
1250                     case 'C': digit = 12;  break;
1251                     case 'D': digit = 13;  break;
1252                     case 'E': digit = 14;  break;
1253                     case 'F': digit = 15;  break;
1254                     default: return NaN;
1255                 }
1256             }
1257             dec = dec + (multiplier * digit);
1258         }
1259         
1260         return dec;
1261     },
1262     
1263     getPeriod: function() {
1264         return {
1265             from: this.startDate,
1266             until: this.startDate.add(Date.DAY, this.numOfDays)
1267         };
1268     },
1269     
1270     /**
1271      * get date of a (event) target
1272      * 
1273      * @param {Ext.EventObject} e
1274      * @param {number} graticule
1275      * @param {char} graticuleEdge one of n (north) s, (south)
1276      * @return {Date}
1277      */
1278     getTargetDateTime: function(e, graticule, graticuleHandle) {
1279         var target = e.getTarget('div[class^=cal-daysviewpanel-datetime]');
1280         
1281         if (target && target.id.match(/^ext-gen\d+:\d+/)) {
1282             var parts = target.id.split(':');
1283             
1284             var date = this.startDate.add(Date.DAY, parseInt(parts[1], 10));
1285             date.is_all_day_event = true;
1286             
1287             if (parts[2] ) {
1288                 var timePart = this.timeScale.getAt(parts[2]),
1289                     eventXY = e.getXY(),
1290                     mainBodyXY = this.mainBody.getXY(),
1291                     offsetPx = eventXY[1] - mainBodyXY[1],
1292                     offsetMinutes = this.getHeightMinutes(offsetPx),
1293                     graticule = graticule ? graticule : this.timeGranularity,
1294                     graticuleHandle = graticuleHandle ? graticuleHandle : 'n',
1295                     graticuleFn = graticuleHandle == 'n' ? Math.floor : Math.ceil;
1296
1297                 // constraint to graticule
1298                 offsetMinutes = graticuleFn(offsetMinutes/graticule) * graticule;
1299
1300                 date = date.add(Date.MINUTE, offsetMinutes);
1301                 date.is_all_day_event = false;
1302             }
1303             
1304             return date;
1305         }
1306     },
1307     
1308     /**
1309      * gets event el of target
1310      * 
1311      * @param {Ext.EventObject} e
1312      * @return {Tine.Calendar.Model.Event}
1313      */
1314     getTargetEvent: function(e) {
1315         var target = e.getTarget();
1316         var el = Ext.fly(target);
1317         
1318         if (el.hasClass('cal-daysviewpanel-event') || (el = el.up('[id*=event:]', 10))) {
1319             var parts = el.dom.id.split(':');
1320             
1321             return this.store.getById(parts[1]);
1322         }
1323     },
1324
1325     /**
1326      * get offset in px for the time part of given date (with current scaling)
1327      *
1328      * @param {date} date
1329      * @returns {number}
1330      */
1331     getTimeOffset: function(date) {
1332         if (this.mainBody) {
1333             var minutes = Ext.isDate(date) ? date.getTimePart() / Date.msMINUTE : date,
1334                 d = this.mainBody.getHeight() / (24 * 60);
1335
1336             return Math.round(d * minutes);
1337         }
1338     },
1339
1340     /**
1341      * get offset in % for the time part of given date
1342      *
1343      * @param {date|number} date
1344      * @returns {number}
1345      */
1346     getTimeOffsetPct: function(date) {
1347         var minutes = Ext.isDate(date) ? date.getTimePart() / Date.msMINUTE : date;
1348
1349         return 100 * ((Date.msMINUTE * minutes) / Date.msDAY);
1350     },
1351
1352     /**
1353      * get height in px of the diff for the given dates (with current scaling)
1354      *
1355      * @param {date} dtStart
1356      * @param {date} dtEnd
1357      * @returns {number}
1358      */
1359     getTimeHeight: function(dtStart, dtEnd) {
1360         if (this.mainBody) {
1361             var d = this.mainBody.getHeight() / (24 * 60);
1362             return Math.round(d * ((dtEnd.getTime() - dtStart.getTime()) / Date.msMINUTE));
1363         }
1364     },
1365
1366     /**
1367      * get height in % of the diff for the given dates
1368      *
1369      * @param {date} dtStart
1370      * @param {date} dtEnd
1371      * @returns {number}
1372      */
1373     getTimeHeightPct: function(dtStart, dtEnd) {
1374         return 100 * ((dtEnd.getTime() - dtStart.getTime()) / Date.msDAY);
1375     },
1376
1377     /**
1378      * get number of minutes represented by height in px (current scaleing)
1379      *
1380      * @param {number} height
1381      * @returns {number}
1382      */
1383     getHeightMinutes: function(height) {
1384         var d = (24 * 60) / this.mainBody.getHeight();
1385         return Math.round(d * height);
1386     },
1387
1388
1389     /**
1390      * fetches elements from our generated dom
1391      */
1392     initElements : function(){
1393         var E = Ext.Element;
1394
1395         var cs = this.el.dom.firstChild.childNodes;
1396
1397         this.mainWrap = new E(cs[0]);
1398         this.mainHd = new E(this.mainWrap.dom.firstChild);
1399
1400         this.innerHd = this.mainHd.dom.firstChild;
1401         
1402         this.wholeDayScroller = new E(this.innerHd.firstChild.childNodes[1]);
1403         this.wholeDayArea = this.wholeDayScroller.dom.firstChild;
1404         
1405         this.scroller = new E(this.mainWrap.dom.childNodes[1]);
1406         this.scroller.setStyle('overflow-x', 'hidden');
1407
1408
1409         this.mon(this.scroller, 'mousewheel', this.onMouseWheel, this);
1410         this.mon(this.scroller, 'scroll', this.onBeforeScroll, this);
1411         this.mon(this.scroller, 'scroll', this.onScroll, this, {buffer: 200});
1412
1413
1414         this.cropper = new E(this.scroller.dom.firstChild);
1415         this.mainBody = new E(this.cropper.dom.firstChild);
1416         this.dayCols = this.mainBody.dom.lastChild.childNodes;
1417
1418         this.focusEl = new E(this.el.dom.lastChild.lastChild);
1419         this.focusEl.swallowEvent("click", true);
1420         this.focusEl.swallowEvent("dblclick", true);
1421         this.focusEl.swallowEvent("contextmenu", true);
1422         
1423         this.aboveHints   = this.mainBody.select('img[class=cal-daysviewpanel-body-daycolumn-hint-above]');
1424         this.belowHints   = this.mainBody.select('img[class=cal-daysviewpanel-body-daycolumn-hint-below]');
1425     },
1426     
1427     /**
1428      * @TODO this returns wrong cols on DST boundaries:
1429      *  e.g. on DST switch form +2 to +1 an all day event is 25 hrs. long
1430      * 
1431      * @param {} date
1432      * @return {}
1433      */
1434     getColumnNumber: function(date) {
1435         return Math.floor((date.add(Date.SECOND, 1).getTime() - this.startDate.getTime()) / Date.msDAY);
1436     },
1437     
1438     getDateColumnEl: function(pos) {
1439         return this.dayCols[pos];
1440     },
1441     
1442     checkWholeDayEls: function() {
1443         var freeIdxs = [];
1444         for (var i=0; i<this.wholeDayArea.childNodes.length-1; i++) {
1445             if(this.wholeDayArea.childNodes[i].childNodes.length === 1) {
1446                 freeIdxs.push(i);
1447             }
1448         }
1449         
1450         for (var i=1; i<freeIdxs.length; i++) {
1451             Ext.fly(this.wholeDayArea.childNodes[freeIdxs[i]]).remove();
1452         }
1453     },
1454
1455     /**
1456      * buffered version of this.unbufferedOnLayout
1457      * @see this.initComponent
1458      */
1459     onLayout: Ext.emptyFn,
1460
1461     /**
1462      * layouts the view
1463      */
1464     unbufferedOnLayout: function() {
1465         Tine.Calendar.DaysView.superclass.onLayout.apply(this, arguments);
1466         if(!this.mainBody){
1467             return; // not rendered
1468         }
1469         
1470         var csize = this.container.getSize(true);
1471         var vw = csize.width;
1472         
1473         this.el.setSize(csize.width, csize.height);
1474         
1475         // layout whole day area -> take one third of the available height maximum
1476         var wholeDayAreaEl = Ext.get(this.wholeDayArea),
1477             wholeDayAreaHeight = this.computeAllDayAreaHeight(),
1478             wholeDayScrollerHeight = wholeDayAreaHeight,
1479             maxAllowedHeight = Math.round(csize.height/3),
1480             resizeEvent = {
1481                 wholeDayAreaHeight: wholeDayAreaHeight,
1482                 wholeDayScrollerHeight: wholeDayScrollerHeight,
1483                 maxAllowedHeight: maxAllowedHeight
1484             };
1485
1486         wholeDayAreaEl.setHeight(Math.min(wholeDayAreaHeight, maxAllowedHeight));
1487         this.fireEvent('onBeforeAllDayScrollerResize', this, resizeEvent);
1488
1489         this.wholeDayScroller.setHeight(resizeEvent.wholeDayScrollerHeight);
1490         
1491         var hdHeight = this.mainHd.getHeight();
1492         var vh = csize.height - (hdHeight);
1493         
1494         this.scroller.setSize(vw, vh);
1495
1496         // resize mainBody for visibleMinutes to fit
1497         var timeToDisplay = this.timeVisible.getTime() / Date.msMINUTE,
1498             scrollerHeight = this.scroller.getHeight(),
1499             height = scrollerHeight * (24 * 60)/timeToDisplay;
1500
1501         this.mainBody.setHeight(height);
1502
1503         if (this.cropDayTime) {
1504             var cropHeightPx = this.getTimeHeight(this.dayStart, this.dayEnd),
1505                 cropStartPx = this.getTimeOffset(this.dayStart);
1506
1507             this.cropper.setStyle('height', cropHeightPx + 'px');
1508             this.cropper.dom.scrollTop = cropStartPx;
1509         }
1510
1511         // force positioning on scroll hints
1512         this.onBeforeScroll.defer(50, this);
1513         this.onScroll.defer(100, this);
1514     },
1515
1516     computeAllDayAreaHeight: function() {
1517         var wholeDayAreaEl = Ext.get(this.wholeDayArea);
1518         for (var i=0, bottom = wholeDayAreaEl.getTop(); i<this.wholeDayArea.childNodes.length -1; i++) {
1519             bottom = Math.max(parseInt(Ext.get(this.wholeDayArea.childNodes[i]).getBottom(), 10), bottom);
1520         }
1521
1522         // take one third of the available height maximum
1523         return bottom - wholeDayAreaEl.getTop() + this.minAllDayScrollerHight;
1524     },
1525
1526     onDestroy: function() {
1527         this.removeAllEvents();
1528         this.initData(false);
1529         this.purgeListeners();
1530         
1531         Tine.Calendar.DaysView.superclass.onDestroy.apply(this, arguments);
1532     },
1533     
1534     /**
1535      * returns HTML frament of the day headers
1536      */
1537     getDayHeaders: function() {
1538         var html = '';
1539         var width = 100/this.numOfDays;
1540         
1541         for (var i=0, date; i<this.numOfDays; i++) {
1542             var day = this.startDate.add(Date.DAY, i);
1543             html += this.templates.dayHeader.applyTemplate({
1544                 day: String.format(this.dayFormatString, day.format('l'), day.format('j'), day.format('F')),
1545                 height: '20px',
1546                 width: width + '%',
1547                 left: i * width + '%'
1548             });
1549         }
1550         return html;
1551     },
1552     
1553     /**
1554      * updates HTML of day headers
1555      */
1556     updateDayHeaders: function() {
1557         if (! this.rendered) {
1558             return;
1559         }
1560         var dayHeaders = Ext.DomQuery.select('div[class=cal-daysviewpanel-dayheader-day]', this.innerHd),
1561             dayWidth = Ext.get(dayHeaders[0]).getWidth(),
1562             headerString;
1563             
1564         for (var i=0, date, isToDay, headerEl, dayColEl; i<dayHeaders.length; i++) {
1565             
1566             date = this.startDate.add(Date.DAY, i);
1567             isToDay = date.getTime() == new Date().clearTime().getTime();
1568             
1569             headerEl = Ext.get(dayHeaders[i]);
1570             
1571             if (dayWidth > 150) {
1572                 headerString = String.format(this.dayFormatString, date.format('l'), date.format('j'), date.format('F'));
1573             } else if (dayWidth > 60){
1574                 headerString = date.format('D') + ', ' + date.format('j') + '.' + date.format('n');
1575             } else {
1576                 headerString = date.format('j') + '.' + date.format('n');
1577             }
1578             
1579             headerEl.update(headerString);
1580             headerEl.parent()[(isToDay ? 'add' : 'remove') + 'Class']('cal-daysviewpanel-dayheader-today');
1581             Ext.get(this.dayCols[i])[(isToDay ? 'add' : 'remove') + 'Class']('cal-daysviewpanel-body-daycolumn-today');
1582         }
1583     },
1584     
1585     /**
1586      * returns HTML fragment of the whole day cols
1587      */
1588     getWholeDayCols: function() {
1589         var html = '';
1590         var width = 100/this.numOfDays;
1591         
1592         var baseId = Ext.id();
1593         for (var i=0; i<this.numOfDays; i++) {
1594             html += this.templates.wholeDayCol.applyTemplate({
1595                 //day: date.get('dateString'),
1596                 id: baseId + ':' + i,
1597                 width: width + '%',
1598                 left: i * width + '%'
1599             });
1600         };
1601         
1602         return html;
1603     },
1604     
1605     /**
1606      * gets HTML fragment of the horizontal time rows
1607      */
1608     getTimeRows: function() {
1609         var html = '';
1610         this.timeScale.each(function(time){
1611             var index = time.get('index');
1612
1613             html += this.templates.timeRow.applyTemplate({
1614                 cls: time.get('minutes')%60 ? 'cal-daysviewpanel-timeRow-off' : 'cal-daysviewpanel-timeRow-on',
1615                 height: 100/(24 * 60 / this.timeGranularity) + '%',
1616                 time: time.get('minutes')%60 ? '' : time.get('time')
1617             });
1618         }, this);
1619         
1620         return html;
1621     },
1622     
1623     /**
1624      * gets HTML fragment of the day columns
1625      */
1626     getDayColumns: function() {
1627         var html = '';
1628         var width = 100/this.numOfDays;
1629         
1630         for (var i=0; i<this.numOfDays; i++) {
1631             html += this.templates.dayColumn.applyTemplate({
1632                 width: width + '%',
1633                 left: i * width + '%',
1634                 overRows: this.getOverRows(i)
1635             });
1636         }
1637         
1638         return html;
1639     },
1640     
1641     /**
1642      * gets HTML fragment of the time over rows
1643      */
1644     getOverRows: function(dayIndex) {
1645         var html = '',
1646             baseId = Ext.id();
1647         
1648         this.timeScale.each(function(time, index, totalCount) {
1649             var cls = 'cal-daysviewpanel-daycolumn-row-' + (time.get('minutes')%60 ? 'off' : 'on');
1650             if (index+1 == totalCount) {
1651                 cls += ' cal-daysviewpanel-daycolumn-row-last';
1652             }
1653
1654             html += this.templates.overRow.applyTemplate({
1655                 id: baseId + ':' + dayIndex + ':' + index,
1656                 cls: cls,
1657                 height: 100/(24 * 60 / this.timeGranularity) + '%',
1658                 time: time.get('time')
1659             });
1660         }, this);
1661         
1662         return html;
1663     },
1664     
1665     /**
1666      * inits all tempaltes of this view
1667      */
1668     initTemplates: function() {
1669         var ts = this.templates || {};
1670     
1671         ts.master = new Ext.XTemplate(
1672             '<div class="cal-daysviewpanel" hidefocus="true">',
1673                 '<div class="cal-daysviewpanel-viewport">',
1674                     '<div class="cal-daysviewpanel-header">',
1675                         '<div class="cal-daysviewpanel-header-inner">',
1676                             '<div class="cal-daysviewpanel-header-offset">{header}</div>',
1677                         '</div>',
1678                     '<div class="x-clear"></div>',
1679                 '</div>',
1680                 '<div class="cal-daysviewpanel-scroller">',
1681                     '<div class="cal-daysviewpanel-cropper">{body}</div>',
1682                 '</div>',
1683             '</div>',
1684             '<a href="#" class="cal-daysviewpanel-focus" tabIndex="-1"></a>'
1685         );
1686         
1687         ts.header = new Ext.XTemplate(
1688             '<div class="cal-daysviewpanel-daysheader">{daysHeader}</div>',
1689             '<div class="cal-daysviewpanel-wholedayheader-scroller">',
1690                 '<div class="cal-daysviewpanel-wholedayheader">',
1691                     '<div class="cal-daysviewpanel-wholedayheader-daycols">{wholeDayCols}</div>',
1692                 '</div>',
1693             '</div>'
1694         );
1695         
1696         ts.dayHeader = new Ext.XTemplate(
1697             '<div class="cal-daysviewpanel-dayheader" style="height: {height}; width: {width}; left: {left};">' + 
1698                 '<div class="cal-daysviewpanel-dayheader-day-wrap">' +
1699                     '<div class="cal-daysviewpanel-dayheader-day">{day}</div>' +
1700                 '</div>',
1701             '</div>'
1702         );
1703         
1704         ts.wholeDayCol = new Ext.XTemplate(
1705             '<div class="cal-daysviewpanel-body-wholedaycolumn" style="left: {left}; width: {width};">' +
1706                 '<div id="{id}" class="cal-daysviewpanel-datetime cal-daysviewpanel-body-wholedaycolumn-over">&#160;</div>' +
1707             '</div>'
1708         );
1709         
1710         ts.body = new Ext.XTemplate(
1711             '<div class="cal-daysviewpanel-body">',
1712                 '<div class="cal-daysviewpanel-body-timecolumn">{timeRows}</div>',
1713                 '<div class="cal-daysviewpanel-body-daycolumns">{dayColumns}</div>',
1714             '</div>'
1715         );
1716         
1717         ts.timeRow = new Ext.XTemplate(
1718             '<div class="{cls}" style="height: {height}; top: {top};">',
1719                 '<div class="cal-daysviewpanel-timeRow-time">{time}</div>',
1720             '</div>'
1721         );
1722         
1723         ts.dayColumn = new Ext.XTemplate(
1724             '<div class="cal-daysviewpanel-body-daycolumn" style="left: {left}; width: {width}; height: 100%">',
1725                 '<div class="cal-daysviewpanel-body-daycolumn-inner">&#160;</div>',
1726                 '{overRows}',
1727                 '<img src="', Ext.BLANK_IMAGE_URL, '" class="cal-daysviewpanel-body-daycolumn-hint-above" />',
1728                 '<img src="', Ext.BLANK_IMAGE_URL, '" class="cal-daysviewpanel-body-daycolumn-hint-below" />',
1729             '</div>'
1730         );
1731         
1732         ts.overRow = new Ext.XTemplate(
1733             '<div id="{id}" class="cal-daysviewpanel-datetime cal-daysviewpanel-daycolumn-row" style="height: {height};">' +
1734                 '<div class="{cls}" >{time}</div>'+
1735             '</div>'
1736         );
1737         
1738         ts.event = new Ext.XTemplate(
1739             '<div id="{id}" class="cal-daysviewpanel-event {extraCls}" style="width: {width}; height: {height}; left: {left}; top: {top}; z-index: {zIndex}; background-color: {bgColor}; border-color: {color};">',
1740                 '<div class="cal-daysviewpanel-event-header" style="background-color: {color};">',
1741                     '<div class="cal-daysviewpanel-event-header-inner" style="color: {textColor}; background-color: {color}; z-index: {zIndex};">{startTime}</div>',
1742                     '<div class="cal-daysviewpanel-event-header-icons">',
1743                         '<tpl for="statusIcons">',
1744                             '<img src="', Ext.BLANK_IMAGE_URL, '" class="cal-status-icon {status}-{[parent.textColor == \'#FFFFFF\' ? \'white\' : \'black\']}" ext:qtip="{[this.encode(values.text)]}" />',
1745                         '</tpl>',
1746                     '</div>',
1747                 '</div>',
1748                 '<div class="cal-daysviewpanel-event-body">{[Ext.util.Format.nl2br(Ext.util.Format.htmlEncode(values.summary))]}</div>',
1749                 '<div class="cal-daysviewpanel-event-tags">{tagsHtml}</div>',
1750             '</div>',
1751             {
1752                 encode: function(v) { return Tine.Tinebase.common.doubleEncode(v); }
1753             }
1754         );
1755         
1756         ts.wholeDayEvent = new Ext.XTemplate(
1757             '<div id="{id}" class="cal-daysviewpanel-event {extraCls}" style="width: {width}; height: {height}; left: {left}; top: {top}; z-index: {zIndex}; background-color: {bgColor}; border-color: {color};">' +
1758             '<div class="cal-daysviewpanel-wholedayevent-tags">{tagsHtml}</div>' +
1759                 '<div class="cal-daysviewpanel-wholedayevent-body">{[Ext.util.Format.nl2br(Ext.util.Format.htmlEncode(values.summary))]}</div>' +
1760                 '<div class="cal-daysviewpanel-event-header-icons" style="background-color: {bgColor};" >' +
1761                     '<tpl for="statusIcons">' +
1762                         '<img src="', Ext.BLANK_IMAGE_URL, '" class="cal-status-icon {status}-black" ext:qtip="{[this.encode(values.text)]}" />',
1763                     '</tpl>' +
1764                 '</div>' +
1765             '</div>',
1766             {
1767                 encode: function(v) { return Tine.Tinebase.common.doubleEncode(v); }
1768             }
1769         );
1770         
1771         for(var k in ts){
1772             var t = ts[k];
1773             if(t && typeof t.compile == 'function' && !t.compiled){
1774                 t.disableFormats = true;
1775                 t.compile();
1776             }
1777         }
1778
1779         this.templates = ts;
1780     }
1781 });
1782
1783 Ext.reg('Tine.Calendar.DaysView', Tine.Calendar.DaysView);