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 Metaways Infosystems GmbH (http://www.metaways.de)
9 Ext.ns('Tine.Calendar');
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>
18 * @param {Object} config
20 Tine.Calendar.DaysView = function(config){
21 Ext.apply(this, config);
22 Tine.Calendar.DaysView.superclass.constructor.call(this);
24 this.printRenderer = Tine.Calendar.Printer.DaysViewRenderer;
29 * fired if an event got clicked
30 * @param {Tine.Calendar.Model.Event} event
31 * @param {Ext.EventObject} e
36 * fired if an event got contextmenu
37 * @param {Ext.EventObject} e
42 * fired if an event got dblclicked
43 * @param {Tine.Calendar.Model.Event} event
44 * @param {Ext.EventObject} e
49 * fired if user wants to change view
50 * @param {String} requested view name
51 * @param {mixed} start param of requested view
56 * fired when period changed
57 * @param {Object} period
62 * fired when a new event got inserted
64 * @param {Tine.Calendar.Model.Event} event
69 * fired when an event go resised/moved
71 * @param {Tine.Calendar.Model.Event} event
77 Ext.extend(Tine.Calendar.DaysView, Ext.Container, {
79 * @cfg {Date} startDate
82 startDate: new Date(),
84 * @cfg {Number} numOfDays
85 * number of days to display
89 * @cfg {String} (H:i) dayStart
90 * generic start of the (work) day
94 * @cfg {String} (H:i) dayEnd
95 * generic end of the (work) day
99 * @cfg {Bool} cropDayTime
100 * crop times before and after dayStart and dayEnd
104 * @cfg {String} newEventSummary
107 newEventSummary: 'New Event',
109 * @cfg {String} dayFormatString
110 * _('{0}, the {1}. of {2}')
112 dayFormatString: '{0}, the {1}. of {2}',
114 * @cfg {Number} timeGranularity
115 * granularity of timegrid in minutes
119 * @cfg {Number} granularityUnitHeights
120 * heights in px of a granularity unit
122 granularityUnitHeights: 18,
124 * @cfg {Boolean} denyDragOnMissingEditGrant
125 * deny drag action if edit grant for event is missing
127 denyDragOnMissingEditGrant: true,
129 * store holding timescale
130 * @type {Ext.data.Store}
134 * The amount of space to reserve for the scrollbar (defaults to 19 pixels)
140 * The time in milliseconds, a scroll should be delayed after using the mousewheel
147 * @property {bool} editing
152 * currently active event
153 * $type {Tine.Calendar.Model.Event}
157 * @property {Ext.data.Store}
163 * updates period to display
164 * @param {Array} period
166 updatePeriod: function(period) {
167 this.startDate = period.from;
169 var tbar = this.findParentBy(function(c) {return c.getTopToolbar()}).getTopToolbar();
171 tbar.periodPicker.update(this.startDate);
172 this.startDate = tbar.periodPicker.getPeriod().from;
175 this.endDate = this.startDate.add(Date.DAY, this.numOfDays+1);
177 //this.parallelScrollerEventsRegistry = new Tine.Calendar.ParallelEventsRegistry({dtStart: this.startDate, dtEnd: this.endDate});
178 //this.parallelWholeDayEventsRegistry = new Tine.Calendar.ParallelEventsRegistry({dtStart: this.startDate, dtEnd: this.endDate});
179 //this.store.each(this.removeEvent, this);
181 this.updateDayHeaders();
182 this.onBeforeScroll();
184 this.fireEvent('changePeriod', period);
190 initComponent: function() {
191 this.app = Tine.Tinebase.appMgr.get('Calendar');
193 this.newEventSummary = this.app.i18n._hidden(this.newEventSummary);
194 this.dayFormatString = this.app.i18n._hidden(this.dayFormatString);
196 this.startDate.setHours(0);
197 this.startDate.setMinutes(0);
198 this.startDate.setSeconds(0);
200 this.endDate = this.startDate.add(Date.DAY, this.numOfDays+1);
202 this.boxMinWidth = 60*this.numOfDays;
204 this.parallelScrollerEventsRegistry = new Tine.Calendar.ParallelEventsRegistry({dtStart: this.startDate, dtEnd: this.endDate});
205 this.parallelWholeDayEventsRegistry = new Tine.Calendar.ParallelEventsRegistry({dtStart: this.startDate, dtEnd: this.endDate});
207 this.initData(this.store);
209 this.initTimeScale();
210 this.initTemplates();
212 this.on('beforehide', this.onBeforeHide, this);
213 this.on('show', this.onShow, this);
215 this.mon(Tine.Tinebase.appMgr, 'activate', this.onAppActivate, this);
217 if (! this.selModel) {
218 this.selModel = this.selModel || new Tine.Calendar.EventSelectionModel();
221 this.onLayout = Function.createBuffered(this.onLayout, 100, this);
224 var prefs = this.app.getRegistry().get('preferences'),
225 startTime = Date.parseDate(prefs.get('daysviewstarttime'), 'H:i'),
226 endTime = Date.parseDate(prefs.get('daysviewendtime'), 'H:i');
228 this.dayStart = Ext.isDate(startTime) ? startTime : Date.parseDate(this.dayStart, 'H:i');
229 this.dayEnd = Ext.isDate(endTime) ? endTime : Date.parseDate(this.dayEnd, 'H:i');
230 // 00:00 in users timezone is a spechial case where the user expects
231 // something like 24:00 and not 00:00
232 if (this.dayEnd.format('H:i') == '00:00') {
233 this.dayEnd = this.dayEnd.add(Date.MINUTE, -1);
235 this.dayEndPx = this.getTimeOffset(this.dayEnd);
237 this.cropDayTime = !! Tine.Tinebase.configManager.get('daysviewcroptime', 'Calendar') && !(!this.getTimeOffset(this.dayStart) && !this.getTimeOffset(this.dayEnd));
239 Tine.Calendar.DaysView.superclass.initComponent.apply(this, arguments);
244 * @param {Ext.data.Store} ds
246 initData : function(ds){
248 this.store.un("load", this.onLoad, this);
249 this.store.un("beforeload", this.onBeforeLoad, this);
250 this.store.un("add", this.onAdd, this);
251 this.store.un("remove", this.onRemove, this);
252 this.store.un("update", this.onUpdate, this);
255 ds.on("load", this.onLoad, this);
256 ds.on("beforeload", this.onBeforeLoad, this);
257 ds.on("add", this.onAdd, this);
258 ds.on("remove", this.onRemove, this);
259 ds.on("update", this.onUpdate, this);
267 initTimeScale: function() {
269 var scaleSize = Date.msDAY/(this.timeGranularity * Date.msMINUTE);
270 var baseDate = this.startDate.clone();
273 for (var i=0; i<scaleSize; i++) {
274 minutes = i * this.timeGranularity;
275 data.push([i, minutes, minutes * Date.msMINUTE, baseDate.add(Date.MINUTE, minutes).format('H:i')]);
278 this.timeScale = new Ext.data.SimpleStore({
279 fields: ['index', 'minutes', 'milliseconds', 'time'],
285 initDropZone: function() {
286 this.dd = new Ext.dd.DropZone(this.mainWrap.dom, {
287 ddGroup: 'cal-event',
289 notifyOver : function(dd, e, data) {
290 var sourceEl = Ext.fly(data.sourceEl);
291 sourceEl.setStyle({'border-style': 'dashed'});
292 sourceEl.setOpacity(0.5);
295 var event = data.event;
297 var targetDateTime = Tine.Calendar.DaysView.prototype.getTargetDateTime.call(data.scope, e);
298 if (targetDateTime) {
299 var dtString = targetDateTime.format(targetDateTime.is_all_day_event ? Ext.form.DateField.prototype.format : 'H:i');
300 if (! event.data.is_all_day_event) {
301 Ext.fly(dd.proxy.el.query('div[class=cal-daysviewpanel-event-header-inner]')[0]).update(dtString);
304 if (event.get('editGrant')) {
305 return Math.abs(targetDateTime.getTime() - event.get('dtstart').getTime()) < Date.msMINUTE ? 'cal-daysviewpanel-event-drop-nodrop' : 'cal-daysviewpanel-event-drop-ok';
310 return 'cal-daysviewpanel-event-drop-nodrop';
313 notifyOut : function() {
314 //console.log('notifyOut');
318 notifyDrop : function(dd, e, data) {
321 var targetDate = v.getTargetDateTime(e);
324 var event = data.event;
326 var originalDuration = (event.get('dtend').getTime() - event.get('dtstart').getTime()) / Date.msMINUTE;
328 // Get the new endDate to ensure it's not out of croptimes
329 var copyTargetDate = targetDate;
330 var newEnd = copyTargetDate.add(Date.MINUTE, originalDuration);
332 v.dayEnd.setDate(newEnd.getDate());
334 // deny drop for missing edit grant or no time change
335 if (! event.get('editGrant') || Math.abs(targetDate.getTime() - event.get('dtstart').getTime()) < Date.msMINUTE
336 || ((v.cropDayTime == true) && (newEnd > v.dayEnd))) {
342 event.set('dtstart', targetDate);
344 if (! event.get('is_all_day_event') && targetDate.is_all_day_event && event.duration < Date.msDAY) {
345 // draged from scroller -> dropped to allDay and duration less than a day
346 event.set('dtend', targetDate.add(Date.DAY, 1).add(Date.SECOND, -1));
347 } else if (event.get('is_all_day_event') && !targetDate.is_all_day_event) {
348 // draged from allDay -> droped to scroller will be resetted to hone hour
349 event.set('dtend', targetDate.add(Date.HOUR, 1));
351 event.set('dtend', targetDate.add(Date.MINUTE, originalDuration));
354 event.set('is_all_day_event', targetDate.is_all_day_event);
357 v.fireEvent('updateEvent', event);
368 initDragZone: function() {
369 this.scroller.ddScrollConfig = {
370 vthresh: this.granularityUnitHeights * 2,
371 increment: this.granularityUnitHeights * 4,
375 Ext.dd.ScrollManager.register(this.scroller);
378 this.dragZone = new Ext.dd.DragZone(this.el, {
379 ddGroup: 'cal-event',
382 containerScroll: true,
384 getDragData: function(e) {
385 var selected = this.view.getSelectionModel().getSelectedEvents();
387 var eventEl = e.getTarget('div.cal-daysviewpanel-event', 10);
389 var parts = eventEl.id.split(':');
390 var event = this.view.store.getById(parts[1]);
392 // don't allow dragging of dirty events
393 // don't allow dragging with missing edit grant
394 if (! event || event.dirty || (this.view.denyDragOnMissingEditGrant && ! event.get('editGrant'))) {
398 // we need to clone an event with summary in
399 var d = Ext.get(event.ui.domIds[0]).dom.cloneNode(true);
402 if (event.get('is_all_day_event')) {
403 Ext.fly(d).setLeft(0);
405 var width = (Ext.fly(this.view.dayCols[0]).getWidth() * 0.9);
406 Ext.fly(d).setTop(0);
407 Ext.fly(d).setWidth(width);
408 Ext.fly(d).setHeight(this.view.getTimeHeight.call(this.view, event.get('dtstart'), event.get('dtend')));
416 selections: this.view.getSelectionModel().getSelectedEvents()
421 getRepairXY: function(e, dd) {
422 Ext.fly(this.dragData.sourceEl).setStyle({'border-style': 'solid'});
423 Ext.fly(this.dragData.sourceEl).setOpacity(1, 1);
425 return Ext.fly(this.dragData.sourceEl).getXY();
433 onRender: function(container, position) {
434 Tine.Calendar.DaysView.superclass.onRender.apply(this, arguments);
436 this.templates.master.append(this.el.dom, {
437 header: this.templates.header.applyTemplate({
438 daysHeader: this.getDayHeaders(),
439 wholeDayCols: this.getWholeDayCols()
441 body: this.templates.body.applyTemplate({
442 timeRows: this.getTimeRows(),
443 dayColumns: this.getDayColumns()
448 this.getSelectionModel().init(this);
452 * fill the events into the view
454 afterRender: function() {
455 Tine.Calendar.DaysView.superclass.afterRender.apply(this, arguments);
457 this.mon(this.el, 'click', this.onClick, this);
458 this.mon(this.el, 'dblclick', this.onDblClick, this);
459 this.mon(this.el, 'contextmenu', this.onContextMenu, this);
460 this.mon(this.el, 'mousedown', this.onMouseDown, this);
461 this.mon(this.el, 'mouseup', this.onMouseUp, this);
462 this.mon(this.el, 'keydown', this.onKeyDown, this);
467 this.updatePeriod({from: this.startDate});
469 if (this.store.getCount()) {
470 this.onLoad.apply(this);
473 // apply os specific scrolling space
474 Ext.fly(this.innerHd.firstChild.firstChild).setStyle('margin-right', Ext.getScrollBarWidth() + 'px');
477 if (this.cropDayTime) {
478 var cropStartPx = this.getTimeOffset(this.dayStart),
479 cropHeightPx = this.getTimeOffset(this.dayEnd) +2;
481 this.mainBody.setStyle('margin-top', '-' + cropStartPx + 'px');
482 this.mainBody.setStyle('height', cropHeightPx + 'px');
483 this.mainBody.setStyle('overflow', 'hidden');
484 this.scroller.addClass('cal-daysviewpanel-body-cropDayTime');
487 // scrollTo initial position
488 this.isScrolling = true;
490 this.scrollTo(this.dayStart);
496 this.rendered = true;
499 scrollTo: function(time) {
500 time = Ext.isDate(time) ? time : new Date();
501 this.scroller.dom.scrollTop = this.getTimeOffset(time);
504 onBeforeScroll: function() {
505 if (! this.isScrolling) {
506 this.isScrolling = true;
508 // walk all cols an hide hints
509 Ext.each(this.dayCols, function(dayCol, idx) {
510 this.aboveHints.item(idx).setDisplayed(false);
511 this.belowHints.item(idx).setDisplayed(false);
517 * add hint if events are outside visible area
523 onScroll: function(e, t, o) {
524 var visibleHeight = this.scroller.dom.clientHeight,
525 visibleStart = this.scroller.dom.scrollTop - this.mainBody.dom.offsetTop,
526 visibleEnd = visibleStart + visibleHeight,
527 vStartMinutes = this.getHeightMinutes(visibleStart),
528 vEndMinutes = this.getHeightMinutes(visibleEnd);
531 Ext.each(this.dayCols, function(dayCol, idx) {
532 var dayColEl = Ext.get(dayCol),
533 dayStart = this.startDate.add(Date.DAY, idx),
534 aboveEvents = this.parallelScrollerEventsRegistry.getEvents(dayStart, dayStart.add(Date.MINUTE, vStartMinutes)),
535 belowEvents = this.parallelScrollerEventsRegistry.getEvents(dayStart.add(Date.MINUTE, vEndMinutes), dayStart.add(Date.DAY, 1));
537 if (aboveEvents.length) {
538 var aboveHint = this.aboveHints.item(idx);
539 aboveHint.setTop(visibleStart + 5);
540 if (!aboveHint.isVisible()) {
541 aboveHint.fadeIn({duration: 1.6});
545 if (belowEvents.length) {
546 var belowHint = this.belowHints.item(idx);
547 belowHint.setTop(visibleEnd - 14);
548 if (!belowHint.isVisible()) {
549 belowHint.fadeIn({duration: 1.6});
554 this.isScrolling = false;
559 this.scroller.dom.scrollTop = this.lastScrollPos || this.getTimeOffset(new Date());
562 onBeforeHide: function() {
563 this.lastScrollPos = this.scroller.dom.scrollTop;
567 * renders a single event into this daysview
568 * @param {Tine.Calendar.Model.Event} event
570 * @todo Add support vor Events spanning over a day boundary
572 insertEvent: function(event) {
573 event.ui = new Tine.Calendar.DaysViewEventUI(event);
574 event.ui.render(this);
580 removeAllEvents: function() {
581 this.store.each(function(event) {
589 * removes a event from the dom
590 * @param {Tine.Calendar.Model.Event} event
592 removeEvent: function(event) {
593 if (event == this.activeEvent) {
594 this.activeEvent = null;
598 this.abortCreateEvent(event);
607 * sets currentlcy active event
609 * NOTE: active != selected
610 * @param {Tine.Calendar.Model.Event} event
612 setActiveEvent: function(event) {
613 this.activeEvent = event || null;
617 * gets currentlcy active event
619 * @return {Tine.Calendar.Model.Event} event
621 getActiveEvent: function() {
622 return this.activeEvent;
626 * returns the selectionModel of the active panel
629 getSelectionModel: function() {
630 return this.selModel;
634 * creates a new event directly from this view
637 createEvent: function(e, event) {
639 // only add range events if mouse is down long enough
640 if (this.editing || (event.isRangeAdd && ! this.mouseDown)) {
644 // insert event silently into store
645 this.editing = event;
646 this.store.suspendEvents();
647 this.store.add(event);
648 this.store.resumeEvents();
651 var registry = event.get('is_all_day_event') ? this.parallelWholeDayEventsRegistry : this.parallelScrollerEventsRegistry;
652 registry.register(event);
653 this.insertEvent(event);
654 //this.setActiveEvent(event);
657 //var eventEls = event.ui.getEls();
658 //eventEls[0].setStyle({'border-style': 'dashed'});
659 //eventEls[0].setOpacity(0.5);
661 // start sizing for range adds
662 if (event.isRangeAdd) {
663 // don't create events with very small duration
664 event.ui.resizeable.on('resize', function() {
665 if (event.get('is_all_day_event')) {
668 var keep = (event.get('dtend').getTime() - event.get('dtstart').getTime()) / Date.msMINUTE >= this.timeGranularity;
672 this.startEditSummary(event);
674 this.abortCreateEvent(event);
678 var rzPos = event.get('is_all_day_event') ? 'east' : 'south';
681 e.browserEvent = {type: 'mousedown'};
684 event.ui.resizeable[rzPos].onMouseDown.call(event.ui.resizeable[rzPos], e);
685 //event.ui.resizeable.startSizing.defer(2000, event.ui.resizeable, [e, event.ui.resizeable[rzPos]]);
687 this.startEditSummary(event);
691 abortCreateEvent: function(event) {
692 this.store.remove(event);
693 this.editing = false;
696 startEditSummary: function(event) {
697 if (event.summaryEditor) {
701 var eventEls = event.ui.getEls();
703 var bodyCls = event.get('is_all_day_event') ? 'cal-daysviewpanel-wholedayevent-body' : 'cal-daysviewpanel-event-body';
704 event.summaryEditor = new Ext.form.TextArea({
706 renderTo: eventEls[0].down('div[class=' + bodyCls + ']'),
707 width: event.ui.getEls()[0].getWidth() -12,
708 height: Math.max(12, event.ui.getEls()[0].getHeight() -18),
709 style: 'background-color: transparent; background: 0: border: 0; position: absolute; top: 0px;',
710 value: this.newEventSummary,
712 maxLengthText: this.app.i18n._('The summary must not be longer than 255 characters.'),
714 minLengthText: this.app.i18n._('The summary must have at least 1 character.'),
715 enableKeyEvents: true,
718 render: function(field) {
719 field.focus(true, 100);
721 blur: this.endEditSummary,
722 specialkey: this.endEditSummary,
723 keydown: this.endEditSummary
729 endEditSummary: function(field, e) {
730 var event = field.event;
731 var summary = field.getValue();
733 if (! this.editing || this.validateMsg || !Ext.isDefined(e)) {
737 // abort edit on ESC key
738 if (e && (e.getKey() == e.ESC)) {
739 this.abortCreateEvent(event);
743 // only commit edit on Enter & blur
744 if (e && e.getKey() != e.ENTER) {
748 // Validate Summary maxLength
749 if (summary.length > field.maxLength) {
751 this.validateMsg = Ext.Msg.alert(this.app.i18n._('Summary too Long'), field.maxLengthText, function(){
753 this.validateMsg = false;
758 // Validate Summary minLength
759 if (!summary || summary.match(/^\s{1,}$/) || summary.length < field.minLength) {
761 this.validateMsg = Ext.Msg.alert(this.app.i18n._('Summary too Short'), field.minLengthText, function(){
763 this.validateMsg = false;
768 this.editing = false;
769 event.summaryEditor = false;
771 event.set('summary', summary);
773 this.store.suspendEvents();
774 this.store.remove(event);
775 this.store.resumeEvents();
777 var registry = event.get('is_all_day_event') ? this.parallelWholeDayEventsRegistry : this.parallelScrollerEventsRegistry;
778 registry.unregister(event);
779 this.removeEvent(event);
782 this.store.add(event);
783 this.fireEvent('addEvent', event);
786 onAppActivate: function(app) {
787 if (app === this.app) {
788 this.redrawWholeDayEvents();
792 onResize: function() {
793 Tine.Calendar.DaysView.superclass.onResize.apply(this, arguments);
795 this.updateDayHeaders();
796 this.redrawWholeDayEvents.defer(50, this);
799 redrawWholeDayEvents: function() {
800 this.store.each(function(event) {
801 if (event.ui && event.get('is_all_day_event')) {
802 this.removeEvent(event);
803 this.insertEvent(event);
808 onClick: function(e) {
809 // check for hint clicks first
810 var hint = e.getTarget('img[class^=cal-daysviewpanel-body-daycolumn-hint-]', 10, true);
812 this.scroller.scroll(hint.hasClass('cal-daysviewpanel-body-daycolumn-hint-above') ? 't' : 'b', 10000, true);
816 var event = this.getTargetEvent(e);
818 this.fireEvent('click', event, e);
822 onContextMenu: function(e) {
823 this.fireEvent('contextmenu', e);
826 onKeyDown : function(e){
827 this.fireEvent("keydown", e);
833 onDblClick: function(e, target) {
835 var event = this.getTargetEvent(e);
836 var dtStart = this.getTargetDateTime(e);
839 this.fireEvent('dblclick', event, e);
840 } else if (dtStart && !this.editing) {
841 var newId = 'cal-daysviewpanel-new-' + Ext.id();
842 var dtend = dtStart.add(Date.HOUR, 1);
843 if (dtStart.is_all_day_event) {
844 dtend = dtend.add(Date.HOUR, 23).add(Date.SECOND, -1);
847 // do not create an event exceeding the crop day time limit
848 else if (this.cropDayTime) {
850 if (dtStart.format(format) >= this.dayEnd.format(format)) {
854 if (dtend.format(format) >= this.dayEnd.format(format)) {
855 dtend.setHours(this.dayEnd.getHours());
856 dtend.setMinutes(this.dayEnd.getMinutes());
857 dtend.setSeconds(this.dayEnd.getSeconds());
861 var event = new Tine.Calendar.Model.Event(Ext.apply(Tine.Calendar.Model.Event.getDefaultData(), {
865 is_all_day_event: dtStart.is_all_day_event
868 this.createEvent(e, event);
870 } else if (target.className == 'cal-daysviewpanel-dayheader-day'){
871 var dayHeaders = Ext.DomQuery.select('div[class=cal-daysviewpanel-dayheader-day]', this.innerHd);
872 var date = this.startDate.add(Date.DAY, dayHeaders.indexOf(target));
873 this.fireEvent('changeView', 'day', date);
880 onMouseDown: function(e) {
881 // only care for left mouse button
882 if (e.button !== 0) {
886 if (! this.editing) {
887 this.focusEl.focus();
889 this.mouseDown = true;
891 var targetEvent = this.getTargetEvent(e);
892 if (this.editing && this.editing.summaryEditor && (targetEvent != this.editing)) {
893 this.editing.summaryEditor.fireEvent('blur', this.editing.summaryEditor, null);
897 var sm = this.getSelectionModel();
898 sm.select(targetEvent);
900 var dtStart = this.getTargetDateTime(e);
902 var newId = 'cal-daysviewpanel-new-' + Ext.id();
903 var event = new Tine.Calendar.Model.Event(Ext.apply(Tine.Calendar.Model.Event.getDefaultData(), {
906 dtend: dtStart.is_all_day_event ? dtStart.add(Date.HOUR, 24).add(Date.SECOND, -1) : dtStart.add(Date.MINUTE, 2*this.timeGranularity/2),
907 is_all_day_event: dtStart.is_all_day_event
909 event.isRangeAdd = true;
913 this.createEvent.defer(100, this, [e, event]);
920 onMouseUp: function() {
921 this.mouseDown = false;
927 onBeforeEventResize: function(rz, e) {
928 var parts = rz.el.id.split(':');
929 var event = this.store.getById(parts[1]);
932 rz.originalHeight = rz.el.getHeight();
933 rz.originalWidth = rz.el.getWidth();
935 // NOTE: ext dosn't support move events via api
936 rz.onMouseMove = rz.onMouseMove.createSequence(function() {
937 var event = this.event;
939 //event already gone -> late event / busy brower?
943 var rzInfo = ui.getRzInfo(this);
945 this.durationEl.update(rzInfo.dtend.format(event.get('is_all_day_event') ? Ext.form.DateField.prototype.format : 'H:i'));
948 event.ui.markDirty();
950 // NOTE: Ext keeps proxy if element is not destroyed (diff !=0)
951 if (! rz.durationEl) {
952 rz.durationEl = rz.el.insertFirst({
953 'class': 'cal-daysviewpanel-event-rzduration',
954 'style': 'position: absolute; bottom: 3px; right: 2px; z-index: 1000;'
957 rz.durationEl.update(event.get('dtend').format(event.get('is_all_day_event') ? Ext.form.DateField.prototype.format : 'H:i'));
960 this.getSelectionModel().select(event);
962 this.getSelectionModel().clearSelections();
969 onEventResize: function(rz, width, height) {
970 var event = rz.event;
973 //event already gone -> late event / busy brower?
977 var rzInfo = event.ui.getRzInfo(rz, width, height);
979 if (rzInfo.diff != 0) {
980 if (rzInfo.duration > 0) {
981 event.set('dtend', rzInfo.dtend);
983 // force event length to at least 1 minute
984 var date = new Date(event.get('dtstart').getTime());
985 date.setMinutes(date.getMinutes() + 1);
986 event.set('dtend', date);
990 if (event.summaryEditor) {
991 event.summaryEditor.setHeight(event.ui.getEls()[0].getHeight() -18);
994 // don't fire update events on rangeAdd
995 if (rzInfo.diff != 0 && event != this.editing && ! event.isRangeAdd) {
996 this.fireEvent('updateEvent', event);
998 event.ui.clearDirty();
1005 onUpdate : function(ds, event){
1006 // don't update events while being created
1007 if (event.get('id').match(/new/)) {
1011 // relayout original context
1012 var originalRegistry = (event.modified.hasOwnProperty('is_all_day_event') ? event.modified.is_all_day_event : event.get('is_all_day_event')) ?
1013 this.parallelWholeDayEventsRegistry :
1014 this.parallelScrollerEventsRegistry;
1016 var registry = event.get('is_all_day_event') ? this.parallelWholeDayEventsRegistry : this.parallelScrollerEventsRegistry;
1017 var originalDtstart = event.modified.hasOwnProperty('dtstart') ? event.modified.dtstart : event.get('dtstart');
1018 var originalDtend = event.modified.hasOwnProperty('dtend') ? event.modified.dtend : event.get('dtend');
1020 var originalParallels = originalRegistry.getEvents(originalDtstart, originalDtend);
1021 for (var j=0; j<originalParallels.length; j++) {
1022 this.removeEvent(originalParallels[j]);
1024 originalRegistry.unregister(event);
1026 var originalParallels = originalRegistry.getEvents(originalDtstart, originalDtend);
1027 for (var j=0; j<originalParallels.length; j++) {
1028 this.insertEvent(originalParallels[j]);
1031 // relayout actual context
1032 var parallelEvents = registry.getEvents(event.get('dtstart'), event.get('dtend'));
1033 for (var j=0; j<parallelEvents.length; j++) {
1034 this.removeEvent(parallelEvents[j]);
1037 registry.register(event);
1038 var parallelEvents = registry.getEvents(event.get('dtstart'), event.get('dtend'));
1039 for (var j=0; j<parallelEvents.length; j++) {
1040 this.insertEvent(parallelEvents[j]);
1043 this.setActiveEvent(this.getActiveEvent());
1050 onAdd : function(ds, records, index){
1051 //console.log('onAdd');
1052 for (var i=0; i<records.length; i++) {
1053 var event = records[i];
1055 var registry = event.get('is_all_day_event') ? this.parallelWholeDayEventsRegistry : this.parallelScrollerEventsRegistry;
1056 registry.register(event);
1058 var parallelEvents = registry.getEvents(event.get('dtstart'), event.get('dtend'));
1060 for (var j=0; j<parallelEvents.length; j++) {
1061 this.removeEvent(parallelEvents[j]);
1062 this.insertEvent(parallelEvents[j]);
1065 //this.setActiveEvent(event);
1074 onRemove : function(ds, event, index, isUpdate) {
1075 if (!event || index == -1) {
1079 if(isUpdate !== true){
1080 //this.fireEvent("beforeeventremoved", this, index, record);
1082 var registry = event.get('is_all_day_event') ? this.parallelWholeDayEventsRegistry : this.parallelScrollerEventsRegistry;
1083 registry.unregister(event);
1084 this.removeEvent(event);
1085 this.getSelectionModel().unselect(event);
1089 onBeforeLoad: function(store, options) {
1090 if (! options.refresh) {
1091 this.store.each(this.removeEvent, this);
1092 this.transitionEvents = [];
1094 this.transitionEvents = this.store.data.items;
1101 onLoad : function() {
1102 if(! this.rendered){
1106 // remove old events
1107 Ext.each(this.transitionEvents, this.removeEvent, this);
1110 this.parallelScrollerEventsRegistry = new Tine.Calendar.ParallelEventsRegistry({dtStart: this.startDate, dtEnd: this.endDate});
1111 this.parallelWholeDayEventsRegistry = new Tine.Calendar.ParallelEventsRegistry({dtStart: this.startDate, dtEnd: this.endDate});
1113 // todo: sort generic?
1114 this.store.fields = Tine.Calendar.Model.Event.prototype.fields;
1115 this.store.sortInfo = {field: 'dtstart', direction: 'ASC'};
1116 this.store.applySort();
1118 this.store.each(function(event) {
1119 var registry = event.get('is_all_day_event') ? this.parallelWholeDayEventsRegistry : this.parallelScrollerEventsRegistry;
1120 registry.register(event);
1123 // put the events in
1124 this.store.each(this.insertEvent, this);
1132 print: function(printMode) {
1133 var renderer = new this.printRenderer({printMode: printMode});
1134 renderer.print(this);
1137 hex2dec: function(hex) {
1139 hex = hex.toString();
1140 var length = hex.length, multiplier, digit;
1141 for (var i=0; i<length; i++) {
1143 multiplier = Math.pow(16, (Math.abs(i - hex.length)-1));
1144 digit = parseInt(hex.toString().charAt([i]), 10);
1146 switch (hex.toString().charAt([i]).toUpperCase()) {
1147 case 'A': digit = 10; break;
1148 case 'B': digit = 11; break;
1149 case 'C': digit = 12; break;
1150 case 'D': digit = 13; break;
1151 case 'E': digit = 14; break;
1152 case 'F': digit = 15; break;
1153 default: return NaN;
1156 dec = dec + (multiplier * digit);
1162 getPeriod: function() {
1164 from: this.startDate,
1165 until: this.startDate.add(Date.DAY, this.numOfDays)
1170 * get date of a (event) target
1172 * @param {Ext.EventObject} e
1175 getTargetDateTime: function(e) {
1176 var target = e.getTarget('div[class^=cal-daysviewpanel-datetime]');
1178 if (target && target.id.match(/^ext-gen\d+:\d+/)) {
1179 var parts = target.id.split(':');
1181 var date = this.startDate.add(Date.DAY, parseInt(parts[1], 10));
1182 date.is_all_day_event = true;
1185 var timePart = this.timeScale.getAt(parts[2]);
1186 date = date.add(Date.MINUTE, timePart.get('minutes'));
1187 date.is_all_day_event = false;
1195 * gets event el of target
1197 * @param {Ext.EventObject} e
1198 * @return {Tine.Calendar.Model.Event}
1200 getTargetEvent: function(e) {
1201 var target = e.getTarget();
1202 var el = Ext.fly(target);
1204 if (el.hasClass('cal-daysviewpanel-event') || (el = el.up('[id*=event:]', 10))) {
1205 var parts = el.dom.id.split(':');
1207 return this.store.getById(parts[1]);
1211 getTimeOffset: function(date) {
1212 var d = this.granularityUnitHeights / this.timeGranularity;
1214 return Math.round(d * ( 60 * date.getHours() + date.getMinutes()));
1217 getTimeHeight: function(dtStart, dtEnd) {
1218 var d = this.granularityUnitHeights / this.timeGranularity;
1219 return Math.round(d * ((dtEnd.getTime() - dtStart.getTime()) / Date.msMINUTE));
1222 getHeightMinutes: function(height) {
1223 return Math.round(height * this.timeGranularity / this.granularityUnitHeights);
1227 * fetches elements from our generated dom
1229 initElements : function(){
1230 var E = Ext.Element;
1232 // var el = this.el.dom.firstChild;
1233 var cs = this.el.dom.firstChild.childNodes;
1235 // this.el = new E(el);
1237 this.mainWrap = new E(cs[0]);
1238 this.mainHd = new E(this.mainWrap.dom.firstChild);
1240 this.innerHd = this.mainHd.dom.firstChild;
1242 this.wholeDayScroller = this.innerHd.firstChild.childNodes[1];
1243 this.wholeDayArea = this.wholeDayScroller.firstChild;
1245 this.scroller = new E(this.mainWrap.dom.childNodes[1]);
1246 this.scroller.setStyle('overflow-x', 'hidden');
1247 this.mon(this.scroller, 'scroll', this.onBeforeScroll, this);
1248 this.mon(this.scroller, 'scroll', this.onScroll, this, {buffer: 200});
1250 this.mainBody = new E(this.scroller.dom.firstChild);
1251 this.dayCols = this.mainBody.dom.firstChild.lastChild.childNodes;
1253 this.focusEl = new E(this.el.dom.lastChild.lastChild);
1254 this.focusEl.swallowEvent("click", true);
1255 this.focusEl.swallowEvent("dblclick", true);
1256 this.focusEl.swallowEvent("contextmenu", true);
1258 this.aboveHints = this.mainBody.select('img[class=cal-daysviewpanel-body-daycolumn-hint-above]');
1259 this.belowHints = this.mainBody.select('img[class=cal-daysviewpanel-body-daycolumn-hint-below]');
1263 * @TODO this returns wrong cols on DST boundaries:
1264 * e.g. on DST switch form +2 to +1 an all day event is 25 hrs. long
1269 getColumnNumber: function(date) {
1270 return Math.floor((date.add(Date.SECOND, 1).getTime() - this.startDate.getTime()) / Date.msDAY);
1273 getDateColumnEl: function(pos) {
1274 return this.dayCols[pos];
1277 checkWholeDayEls: function() {
1279 for (var i=0; i<this.wholeDayArea.childNodes.length-1; i++) {
1280 if(this.wholeDayArea.childNodes[i].childNodes.length === 1) {
1285 for (var i=1; i<freeIdxs.length; i++) {
1286 Ext.fly(this.wholeDayArea.childNodes[freeIdxs[i]]).remove();
1293 onLayout: function() {
1294 Tine.Calendar.DaysView.superclass.onLayout.apply(this, arguments);
1296 return; // not rendered
1299 var csize = this.container.getSize(true);
1300 var vw = csize.width;
1302 this.el.setSize(csize.width, csize.height);
1304 // layout whole day area
1305 var wholeDayAreaEl = Ext.get(this.wholeDayArea);
1306 for (var i=0, bottom = wholeDayAreaEl.getTop(); i<this.wholeDayArea.childNodes.length -1; i++) {
1307 bottom = Math.max(parseInt(Ext.get(this.wholeDayArea.childNodes[i]).getBottom(), 10), bottom);
1309 var wholeDayAreaHeight = bottom - wholeDayAreaEl.getTop() + 10;
1310 // take one third of the available height maximum
1311 wholeDayAreaEl.setHeight(wholeDayAreaHeight);
1312 Ext.fly(this.wholeDayScroller).setHeight(Math.min(Math.round(csize.height/3), wholeDayAreaHeight));
1314 var hdHeight = this.mainHd.getHeight();
1315 var vh = csize.height - (hdHeight);
1317 this.scroller.setSize(vw, vh);
1319 // force positioning on scroll hints
1320 this.onScroll.defer(100, this);
1323 onDestroy: function() {
1324 this.removeAllEvents();
1325 this.initData(false);
1326 this.purgeListeners();
1328 Tine.Calendar.DaysView.superclass.onDestroy.apply(this, arguments);
1332 * returns HTML frament of the day headers
1334 getDayHeaders: function() {
1336 var width = 100/this.numOfDays;
1338 for (var i=0, date; i<this.numOfDays; i++) {
1339 var day = this.startDate.add(Date.DAY, i);
1340 html += this.templates.dayHeader.applyTemplate({
1341 day: String.format(this.dayFormatString, day.format('l'), day.format('j'), day.format('F')),
1342 height: this.granularityUnitHeights,
1344 left: i * width + '%'
1351 * updates HTML of day headers
1353 updateDayHeaders: function() {
1354 if (! this.rendered) {
1357 var dayHeaders = Ext.DomQuery.select('div[class=cal-daysviewpanel-dayheader-day]', this.innerHd),
1358 dayWidth = Ext.get(dayHeaders[0]).getWidth(),
1361 for (var i=0, date, isToDay, headerEl, dayColEl; i<dayHeaders.length; i++) {
1363 date = this.startDate.add(Date.DAY, i);
1364 isToDay = date.getTime() == new Date().clearTime().getTime();
1366 headerEl = Ext.get(dayHeaders[i]);
1368 if (dayWidth > 150) {
1369 headerString = String.format(this.dayFormatString, date.format('l'), date.format('j'), date.format('F'));
1370 } else if (dayWidth > 60){
1371 headerString = date.format('D') + ', ' + date.format('j') + '.' + date.format('n');
1373 headerString = date.format('j') + '.' + date.format('n');
1376 headerEl.update(headerString);
1377 headerEl.parent()[(isToDay ? 'add' : 'remove') + 'Class']('cal-daysviewpanel-dayheader-today');
1378 Ext.get(this.dayCols[i])[(isToDay ? 'add' : 'remove') + 'Class']('cal-daysviewpanel-body-daycolumn-today');
1383 * returns HTML fragment of the whole day cols
1385 getWholeDayCols: function() {
1387 var width = 100/this.numOfDays;
1389 var baseId = Ext.id();
1390 for (var i=0; i<this.numOfDays; i++) {
1391 html += this.templates.wholeDayCol.applyTemplate({
1392 //day: date.get('dateString'),
1393 //height: this.granularityUnitHeights,
1394 id: baseId + ':' + i,
1396 left: i * width + '%'
1404 * gets HTML fragment of the horizontal time rows
1406 getTimeRows: function() {
1408 this.timeScale.each(function(time){
1409 var index = time.get('index');
1410 html += this.templates.timeRow.applyTemplate({
1411 cls: index%2 ? 'cal-daysviewpanel-timeRow-off' : 'cal-daysviewpanel-timeRow-on',
1412 height: this.granularityUnitHeights + 'px',
1413 top: index * this.granularityUnitHeights + 'px',
1414 time: index%2 ? '' : time.get('time')
1422 * gets HTML fragment of the day columns
1424 getDayColumns: function() {
1426 var width = 100/this.numOfDays;
1428 for (var i=0; i<this.numOfDays; i++) {
1429 html += this.templates.dayColumn.applyTemplate({
1431 left: i * width + '%',
1432 overRows: this.getOverRows(i)
1440 * gets HTML fragment of the time over rows
1442 getOverRows: function(dayIndex) {
1444 var baseId = Ext.id();
1446 this.timeScale.each(function(time){
1447 var index = time.get('index');
1448 html += this.templates.overRow.applyTemplate({
1449 id: baseId + ':' + dayIndex + ':' + index,
1450 cls: 'cal-daysviewpanel-daycolumn-row-' + (index%2 ? 'off' : 'on'),
1451 height: this.granularityUnitHeights + 'px',
1452 time: time.get('time')
1460 * inits all tempaltes of this view
1462 initTemplates: function() {
1463 var ts = this.templates || {};
1465 ts.master = new Ext.XTemplate(
1466 '<div class="cal-daysviewpanel" hidefocus="true">',
1467 '<div class="cal-daysviewpanel-viewport">',
1468 '<div class="cal-daysviewpanel-header"><div class="cal-daysviewpanel-header-inner"><div class="cal-daysviewpanel-header-offset">{header}</div></div><div class="x-clear"></div></div>',
1469 '<div class="cal-daysviewpanel-scroller"><div class="cal-daysviewpanel-body">{body}</div></div>',
1471 '<a href="#" class="cal-daysviewpanel-focus" tabIndex="-1"></a>',
1475 ts.header = new Ext.XTemplate(
1476 '<div class="cal-daysviewpanel-daysheader">{daysHeader}</div>',
1477 '<div class="cal-daysviewpanel-wholedayheader-scroller">',
1478 '<div class="cal-daysviewpanel-wholedayheader">',
1479 '<div class="cal-daysviewpanel-wholedayheader-daycols">{wholeDayCols}</div>',
1484 ts.dayHeader = new Ext.XTemplate(
1485 '<div class="cal-daysviewpanel-dayheader" style="height: {height}; width: {width}; left: {left};">' +
1486 '<div class="cal-daysviewpanel-dayheader-day-wrap">' +
1487 '<div class="cal-daysviewpanel-dayheader-day">{day}</div>' +
1492 ts.wholeDayCol = new Ext.XTemplate(
1493 '<div class="cal-daysviewpanel-body-wholedaycolumn" style="left: {left}; width: {width};">' +
1494 '<div id="{id}" class="cal-daysviewpanel-datetime cal-daysviewpanel-body-wholedaycolumn-over"> </div>' +
1498 ts.body = new Ext.XTemplate(
1499 '<div class="cal-daysviewpanel-body-inner">' +
1501 '<div class="cal-daysviewpanel-body-daycolumns">{dayColumns}</div>' +
1505 ts.timeRow = new Ext.XTemplate(
1506 '<div class="{cls}" style="height: {height}; top: {top};">',
1507 '<div class="cal-daysviewpanel-timeRow-time">{time}</div>',
1511 ts.dayColumn = new Ext.XTemplate(
1512 '<div class="cal-daysviewpanel-body-daycolumn" style="left: {left}; width: {width};">',
1513 '<div class="cal-daysviewpanel-body-daycolumn-inner"> </div>',
1515 '<img src="', Ext.BLANK_IMAGE_URL, '" class="cal-daysviewpanel-body-daycolumn-hint-above" />',
1516 '<img src="', Ext.BLANK_IMAGE_URL, '" class="cal-daysviewpanel-body-daycolumn-hint-below" />',
1520 ts.overRow = new Ext.XTemplate(
1521 '<div id="{id}" class="cal-daysviewpanel-datetime cal-daysviewpanel-daycolumn-row" style="height: {height};">' +
1522 '<div class="{cls}" >{time}</div>'+
1526 ts.event = new Ext.XTemplate(
1527 '<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};">',
1528 '<div class="cal-daysviewpanel-event-header" style="background-color: {color};">',
1529 '<div class="cal-daysviewpanel-event-header-inner" style="color: {textColor}; background-color: {color}; z-index: {zIndex};">{startTime}</div>',
1530 '<div class="cal-daysviewpanel-event-header-icons">',
1531 '<tpl for="statusIcons">',
1532 '<img src="', Ext.BLANK_IMAGE_URL, '" class="cal-status-icon {status}-{[parent.textColor == \'#FFFFFF\' ? \'white\' : \'black\']}" ext:qtip="{[this.encode(values.text)]}" />',
1536 '<div class="cal-daysviewpanel-event-body">{[Ext.util.Format.nl2br(Ext.util.Format.htmlEncode(values.summary))]}</div>',
1537 '<div class="cal-daysviewpanel-event-tags">{tagsHtml}</div>',
1540 encode: function(v) { return Tine.Tinebase.common.doubleEncode(v); }
1544 ts.wholeDayEvent = new Ext.XTemplate(
1545 '<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};">' +
1546 '<div class="cal-daysviewpanel-wholedayevent-tags">{tagsHtml}</div>' +
1547 '<div class="cal-daysviewpanel-wholedayevent-body">{[Ext.util.Format.nl2br(Ext.util.Format.htmlEncode(values.summary))]}</div>' +
1548 '<div class="cal-daysviewpanel-event-header-icons" style="background-color: {bgColor};" >' +
1549 '<tpl for="statusIcons">' +
1550 '<img src="', Ext.BLANK_IMAGE_URL, '" class="cal-status-icon {status}-black" ext:qtip="{[this.encode(values.text)]}" />',
1555 encode: function(v) { return Tine.Tinebase.common.doubleEncode(v); }
1561 if(t && typeof t.compile == 'function' && !t.compiled){
1562 t.disableFormats = true;
1567 this.templates = ts;
1571 Ext.reg('Tine.Calendar.DaysView', Tine.Calendar.DaysView);