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 this.dayEndPx = this.getTimeOffset(this.dayEnd, true);
232 this.cropDayTime = !! Tine.Tinebase.configManager.get('daysviewcroptime', 'Calendar') && !(!this.getTimeOffset(this.dayStart, false) && !this.getTimeOffset(this.dayEnd, true));
234 Tine.Calendar.DaysView.superclass.initComponent.apply(this, arguments);
239 * @param {Ext.data.Store} ds
241 initData : function(ds){
243 this.store.un("load", this.onLoad, this);
244 this.store.un("beforeload", this.onBeforeLoad, this);
245 this.store.un("add", this.onAdd, this);
246 this.store.un("remove", this.onRemove, this);
247 this.store.un("update", this.onUpdate, this);
250 ds.on("load", this.onLoad, this);
251 ds.on("beforeload", this.onBeforeLoad, this);
252 ds.on("add", this.onAdd, this);
253 ds.on("remove", this.onRemove, this);
254 ds.on("update", this.onUpdate, this);
262 initTimeScale: function() {
264 var scaleSize = Date.msDAY/(this.timeGranularity * Date.msMINUTE);
265 var baseDate = this.startDate.clone();
268 for (var i=0; i<scaleSize; i++) {
269 minutes = i * this.timeGranularity;
270 data.push([i, minutes, minutes * Date.msMINUTE, baseDate.add(Date.MINUTE, minutes).format('H:i')]);
273 this.timeScale = new Ext.data.SimpleStore({
274 fields: ['index', 'minutes', 'milliseconds', 'time'],
280 initDropZone: function() {
281 this.dd = new Ext.dd.DropZone(this.mainWrap.dom, {
282 ddGroup: 'cal-event',
284 notifyOver : function(dd, e, data) {
285 var sourceEl = Ext.fly(data.sourceEl);
286 sourceEl.setStyle({'border-style': 'dashed'});
287 sourceEl.setOpacity(0.5);
290 var event = data.event;
292 var targetDateTime = Tine.Calendar.DaysView.prototype.getTargetDateTime.call(data.scope, e);
293 if (targetDateTime) {
294 var dtString = targetDateTime.format(targetDateTime.is_all_day_event ? Ext.form.DateField.prototype.format : 'H:i');
295 if (! event.data.is_all_day_event) {
296 Ext.fly(dd.proxy.el.query('div[class=cal-daysviewpanel-event-header-inner]')[0]).update(dtString);
299 if (event.get('editGrant')) {
300 return Math.abs(targetDateTime.getTime() - event.get('dtstart').getTime()) < Date.msMINUTE ? 'cal-daysviewpanel-event-drop-nodrop' : 'cal-daysviewpanel-event-drop-ok';
305 return 'cal-daysviewpanel-event-drop-nodrop';
308 notifyOut : function() {
309 //console.log('notifyOut');
313 notifyDrop : function(dd, e, data) {
316 var targetDate = v.getTargetDateTime(e);
319 var event = data.event;
321 var originalDuration = (event.get('dtend').getTime() - event.get('dtstart').getTime()) / Date.msMINUTE;
323 // Get the new endDate to ensure it's not out of croptimes
324 var copyTargetDate = targetDate;
325 var newEnd = copyTargetDate.add(Date.MINUTE, originalDuration);
327 v.dayEnd.setDate(newEnd.getDate());
329 // deny drop for missing edit grant or no time change
330 if (! event.get('editGrant') || Math.abs(targetDate.getTime() - event.get('dtstart').getTime()) < Date.msMINUTE
331 || ((v.cropDayTime == true) && (newEnd > v.dayEnd))) {
337 event.set('dtstart', targetDate);
339 if (! event.get('is_all_day_event') && targetDate.is_all_day_event && event.duration < Date.msDAY) {
340 // draged from scroller -> dropped to allDay and duration less than a day
341 event.set('dtend', targetDate.add(Date.DAY, 1).add(Date.SECOND, -1));
342 } else if (event.get('is_all_day_event') && !targetDate.is_all_day_event) {
343 // draged from allDay -> droped to scroller will be resetted to hone hour
344 event.set('dtend', targetDate.add(Date.HOUR, 1));
346 event.set('dtend', targetDate.add(Date.MINUTE, originalDuration));
349 event.set('is_all_day_event', targetDate.is_all_day_event);
352 v.fireEvent('updateEvent', event);
363 initDragZone: function() {
364 this.scroller.ddScrollConfig = {
365 vthresh: this.granularityUnitHeights * 2,
366 increment: this.granularityUnitHeights * 4,
370 Ext.dd.ScrollManager.register(this.scroller);
373 this.dragZone = new Ext.dd.DragZone(this.el, {
374 ddGroup: 'cal-event',
377 containerScroll: true,
379 getDragData: function(e) {
380 var selected = this.view.getSelectionModel().getSelectedEvents();
382 var eventEl = e.getTarget('div.cal-daysviewpanel-event', 10);
384 var parts = eventEl.id.split(':');
385 var event = this.view.store.getById(parts[1]);
387 // don't allow dragging of dirty events
388 // don't allow dragging with missing edit grant
389 if (! event || event.dirty || (this.view.denyDragOnMissingEditGrant && ! event.get('editGrant'))) {
393 // we need to clone an event with summary in
394 var d = Ext.get(event.ui.domIds[0]).dom.cloneNode(true);
397 if (event.get('is_all_day_event')) {
398 Ext.fly(d).setLeft(0);
400 var width = (Ext.fly(this.view.dayCols[0]).getWidth() * 0.9);
401 Ext.fly(d).setTop(0);
402 Ext.fly(d).setWidth(width);
403 Ext.fly(d).setHeight(this.view.getTimeHeight.call(this.view, event.get('dtstart'), event.get('dtend')));
411 selections: this.view.getSelectionModel().getSelectedEvents()
416 getRepairXY: function(e, dd) {
417 Ext.fly(this.dragData.sourceEl).setStyle({'border-style': 'solid'});
418 Ext.fly(this.dragData.sourceEl).setOpacity(1, 1);
420 return Ext.fly(this.dragData.sourceEl).getXY();
428 onRender: function(container, position) {
429 Tine.Calendar.DaysView.superclass.onRender.apply(this, arguments);
431 this.templates.master.append(this.el.dom, {
432 header: this.templates.header.applyTemplate({
433 daysHeader: this.getDayHeaders(),
434 wholeDayCols: this.getWholeDayCols()
436 body: this.templates.body.applyTemplate({
437 timeRows: this.getTimeRows(),
438 dayColumns: this.getDayColumns()
443 this.getSelectionModel().init(this);
447 * fill the events into the view
449 afterRender: function() {
450 Tine.Calendar.DaysView.superclass.afterRender.apply(this, arguments);
452 this.mon(this.el, 'click', this.onClick, this);
453 this.mon(this.el, 'dblclick', this.onDblClick, this);
454 this.mon(this.el, 'contextmenu', this.onContextMenu, this);
455 this.mon(this.el, 'mousedown', this.onMouseDown, this);
456 this.mon(this.el, 'mouseup', this.onMouseUp, this);
457 this.mon(this.el, 'keydown', this.onKeyDown, this);
462 this.updatePeriod({from: this.startDate});
464 if (this.store.getCount()) {
465 this.onLoad.apply(this);
468 // apply os specific scrolling space
469 Ext.fly(this.innerHd.firstChild.firstChild).setStyle('margin-right', Ext.getScrollBarWidth() + 'px');
472 if (this.cropDayTime) {
473 var cropStartPx = this.getTimeOffset(this.dayStart, false),
474 cropHeightPx = this.getTimeOffset(this.dayEnd, true) +2;
476 this.mainBody.setStyle('margin-top', '-' + cropStartPx + 'px');
477 this.mainBody.setStyle('height', cropHeightPx + 'px');
478 this.mainBody.setStyle('overflow', 'hidden');
479 this.scroller.addClass('cal-daysviewpanel-body-cropDayTime');
482 // scrollTo initial position
483 this.isScrolling = true;
485 this.scrollTo(this.dayStart);
491 this.rendered = true;
494 scrollTo: function(time) {
495 time = Ext.isDate(time) ? time : new Date();
496 this.scroller.dom.scrollTop = this.getTimeOffset(time, false);
499 onBeforeScroll: function() {
500 if (! this.isScrolling) {
501 this.isScrolling = true;
503 // walk all cols an hide hints
504 Ext.each(this.dayCols, function(dayCol, idx) {
505 this.aboveHints.item(idx).setDisplayed(false);
506 this.belowHints.item(idx).setDisplayed(false);
512 * add hint if events are outside visible area
518 onScroll: function(e, t, o) {
519 var visibleHeight = this.scroller.dom.clientHeight,
520 visibleStart = this.scroller.dom.scrollTop - this.mainBody.dom.offsetTop,
521 visibleEnd = visibleStart + visibleHeight,
522 vStartMinutes = this.getHeightMinutes(visibleStart),
523 vEndMinutes = this.getHeightMinutes(visibleEnd);
526 Ext.each(this.dayCols, function(dayCol, idx) {
527 var dayColEl = Ext.get(dayCol),
528 dayStart = this.startDate.add(Date.DAY, idx),
529 aboveEvents = this.parallelScrollerEventsRegistry.getEvents(dayStart, dayStart.add(Date.MINUTE, vStartMinutes)),
530 belowEvents = this.parallelScrollerEventsRegistry.getEvents(dayStart.add(Date.MINUTE, vEndMinutes), dayStart.add(Date.DAY, 1));
532 if (aboveEvents.length) {
533 var aboveHint = this.aboveHints.item(idx);
534 aboveHint.setTop(visibleStart + 5);
535 if (!aboveHint.isVisible()) {
536 aboveHint.fadeIn({duration: 1.6});
540 if (belowEvents.length) {
541 var belowHint = this.belowHints.item(idx);
542 belowHint.setTop(visibleEnd - 14);
543 if (!belowHint.isVisible()) {
544 belowHint.fadeIn({duration: 1.6});
549 this.isScrolling = false;
554 this.scroller.dom.scrollTop = this.lastScrollPos || this.getTimeOffset(new Date(), false);
557 onBeforeHide: function() {
558 this.lastScrollPos = this.scroller.dom.scrollTop;
562 * renders a single event into this daysview
563 * @param {Tine.Calendar.Model.Event} event
565 * @todo Add support vor Events spanning over a day boundary
567 insertEvent: function(event) {
568 event.ui = new Tine.Calendar.DaysViewEventUI(event);
569 event.ui.render(this);
575 removeAllEvents: function() {
576 this.store.each(function(event) {
584 * removes a event from the dom
585 * @param {Tine.Calendar.Model.Event} event
587 removeEvent: function(event) {
588 if (event == this.activeEvent) {
589 this.activeEvent = null;
593 this.abortCreateEvent(event);
602 * sets currentlcy active event
604 * NOTE: active != selected
605 * @param {Tine.Calendar.Model.Event} event
607 setActiveEvent: function(event) {
608 this.activeEvent = event || null;
612 * gets currentlcy active event
614 * @return {Tine.Calendar.Model.Event} event
616 getActiveEvent: function() {
617 return this.activeEvent;
621 * returns the selectionModel of the active panel
624 getSelectionModel: function() {
625 return this.selModel;
629 * creates a new event directly from this view
632 createEvent: function(e, event) {
634 // only add range events if mouse is down long enough
635 if (this.editing || (event.isRangeAdd && ! this.mouseDown)) {
639 // insert event silently into store
640 this.editing = event;
641 this.store.suspendEvents();
642 this.store.add(event);
643 this.store.resumeEvents();
646 var registry = event.get('is_all_day_event') ? this.parallelWholeDayEventsRegistry : this.parallelScrollerEventsRegistry;
647 registry.register(event);
648 this.insertEvent(event);
649 //this.setActiveEvent(event);
652 //var eventEls = event.ui.getEls();
653 //eventEls[0].setStyle({'border-style': 'dashed'});
654 //eventEls[0].setOpacity(0.5);
656 // start sizing for range adds
657 if (event.isRangeAdd) {
658 // don't create events with very small duration
659 event.ui.resizeable.on('resize', function() {
660 if (event.get('is_all_day_event')) {
663 var keep = (event.get('dtend').getTime() - event.get('dtstart').getTime()) / Date.msMINUTE >= this.timeGranularity;
667 this.startEditSummary(event);
669 this.abortCreateEvent(event);
673 var rzPos = event.get('is_all_day_event') ? 'east' : 'south';
676 e.browserEvent = {type: 'mousedown'};
679 event.ui.resizeable[rzPos].onMouseDown.call(event.ui.resizeable[rzPos], e);
680 //event.ui.resizeable.startSizing.defer(2000, event.ui.resizeable, [e, event.ui.resizeable[rzPos]]);
682 this.startEditSummary(event);
686 abortCreateEvent: function(event) {
687 this.store.remove(event);
688 this.editing = false;
691 startEditSummary: function(event) {
692 if (event.summaryEditor) {
696 var eventEls = event.ui.getEls();
698 var bodyCls = event.get('is_all_day_event') ? 'cal-daysviewpanel-wholedayevent-body' : 'cal-daysviewpanel-event-body';
699 event.summaryEditor = new Ext.form.TextArea({
701 renderTo: eventEls[0].down('div[class=' + bodyCls + ']'),
702 width: event.ui.getEls()[0].getWidth() -12,
703 height: Math.max(12, event.ui.getEls()[0].getHeight() -18),
704 style: 'background-color: transparent; background: 0: border: 0; position: absolute; top: 0px;',
705 value: this.newEventSummary,
707 maxLengthText: this.app.i18n._('The summary must not be longer than 255 characters.'),
709 minLengthText: this.app.i18n._('The summary must have at least 1 character.'),
710 enableKeyEvents: true,
713 render: function(field) {
714 field.focus(true, 100);
716 blur: this.endEditSummary,
717 specialkey: this.endEditSummary,
718 keydown: this.endEditSummary
724 endEditSummary: function(field, e) {
725 var event = field.event;
726 var summary = field.getValue();
728 if (! this.editing || this.validateMsg || !Ext.isDefined(e)) {
732 // abort edit on ESC key
733 if (e && (e.getKey() == e.ESC)) {
734 this.abortCreateEvent(event);
738 // only commit edit on Enter & blur
739 if (e && e.getKey() != e.ENTER) {
743 // Validate Summary maxLength
744 if (summary.length > field.maxLength) {
746 this.validateMsg = Ext.Msg.alert(this.app.i18n._('Summary too Long'), field.maxLengthText, function(){
748 this.validateMsg = false;
753 // Validate Summary minLength
754 if (!summary || summary.match(/^\s{1,}$/) || summary.length < field.minLength) {
756 this.validateMsg = Ext.Msg.alert(this.app.i18n._('Summary too Short'), field.minLengthText, function(){
758 this.validateMsg = false;
763 this.editing = false;
764 event.summaryEditor = false;
766 event.set('summary', summary);
768 this.store.suspendEvents();
769 this.store.remove(event);
770 this.store.resumeEvents();
772 var registry = event.get('is_all_day_event') ? this.parallelWholeDayEventsRegistry : this.parallelScrollerEventsRegistry;
773 registry.unregister(event);
774 this.removeEvent(event);
777 this.store.add(event);
778 this.fireEvent('addEvent', event);
781 onAppActivate: function(app) {
782 if (app === this.app) {
783 this.redrawWholeDayEvents();
787 onResize: function() {
788 Tine.Calendar.DaysView.superclass.onResize.apply(this, arguments);
790 this.updateDayHeaders();
791 this.redrawWholeDayEvents.defer(50, this);
794 redrawWholeDayEvents: function() {
795 this.store.each(function(event) {
796 if (event.ui && event.get('is_all_day_event')) {
797 this.removeEvent(event);
798 this.insertEvent(event);
803 onClick: function(e) {
804 // check for hint clicks first
805 var hint = e.getTarget('img[class^=cal-daysviewpanel-body-daycolumn-hint-]', 10, true);
807 this.scroller.scroll(hint.hasClass('cal-daysviewpanel-body-daycolumn-hint-above') ? 't' : 'b', 10000, true);
811 var event = this.getTargetEvent(e);
813 this.fireEvent('click', event, e);
817 onContextMenu: function(e) {
818 this.fireEvent('contextmenu', e);
821 onKeyDown : function(e){
822 this.fireEvent("keydown", e);
828 onDblClick: function(e, target) {
830 var event = this.getTargetEvent(e);
831 var dtStart = this.getTargetDateTime(e);
834 this.fireEvent('dblclick', event, e);
835 } else if (dtStart && !this.editing) {
836 var newId = 'cal-daysviewpanel-new-' + Ext.id();
837 var dtend = dtStart.add(Date.HOUR, 1);
838 if (dtStart.is_all_day_event) {
839 dtend = dtend.add(Date.HOUR, 23).add(Date.SECOND, -1);
842 // do not create an event exceeding the crop day time limit
843 else if (this.cropDayTime) {
845 if (dtStart.format(format) >= this.dayEnd.format(format)) {
849 if (dtend.format(format) >= this.dayEnd.format(format)) {
850 dtend.setHours(this.dayEnd.getHours());
851 dtend.setMinutes(this.dayEnd.getMinutes());
852 dtend.setSeconds(this.dayEnd.getSeconds());
856 var event = new Tine.Calendar.Model.Event(Ext.apply(Tine.Calendar.Model.Event.getDefaultData(), {
860 is_all_day_event: dtStart.is_all_day_event
863 this.createEvent(e, event);
865 } else if (target.className == 'cal-daysviewpanel-dayheader-day'){
866 var dayHeaders = Ext.DomQuery.select('div[class=cal-daysviewpanel-dayheader-day]', this.innerHd);
867 var date = this.startDate.add(Date.DAY, dayHeaders.indexOf(target));
868 this.fireEvent('changeView', 'day', date);
875 onMouseDown: function(e) {
876 // only care for left mouse button
877 if (e.button !== 0) {
881 if (! this.editing) {
882 this.focusEl.focus();
884 this.mouseDown = true;
886 var targetEvent = this.getTargetEvent(e);
887 if (this.editing && this.editing.summaryEditor && (targetEvent != this.editing)) {
888 this.editing.summaryEditor.fireEvent('blur', this.editing.summaryEditor, null);
892 var sm = this.getSelectionModel();
893 sm.select(targetEvent);
895 var dtStart = this.getTargetDateTime(e);
897 var newId = 'cal-daysviewpanel-new-' + Ext.id();
898 var event = new Tine.Calendar.Model.Event(Ext.apply(Tine.Calendar.Model.Event.getDefaultData(), {
901 dtend: dtStart.is_all_day_event ? dtStart.add(Date.HOUR, 24).add(Date.SECOND, -1) : dtStart.add(Date.MINUTE, 2*this.timeGranularity/2),
902 is_all_day_event: dtStart.is_all_day_event
904 event.isRangeAdd = true;
908 this.createEvent.defer(100, this, [e, event]);
915 onMouseUp: function() {
916 this.mouseDown = false;
922 onBeforeEventResize: function(rz, e) {
923 var parts = rz.el.id.split(':');
924 var event = this.store.getById(parts[1]);
927 rz.originalHeight = rz.el.getHeight();
928 rz.originalWidth = rz.el.getWidth();
930 // NOTE: ext dosn't support move events via api
931 rz.onMouseMove = rz.onMouseMove.createSequence(function() {
932 var event = this.event;
934 //event already gone -> late event / busy brower?
938 var rzInfo = ui.getRzInfo(this);
940 this.durationEl.update(rzInfo.dtend.format(event.get('is_all_day_event') ? Ext.form.DateField.prototype.format : 'H:i'));
943 event.ui.markDirty();
945 // NOTE: Ext keeps proxy if element is not destroyed (diff !=0)
946 if (! rz.durationEl) {
947 rz.durationEl = rz.el.insertFirst({
948 'class': 'cal-daysviewpanel-event-rzduration',
949 'style': 'position: absolute; bottom: 3px; right: 2px; z-index: 1000;'
952 rz.durationEl.update(event.get('dtend').format(event.get('is_all_day_event') ? Ext.form.DateField.prototype.format : 'H:i'));
955 this.getSelectionModel().select(event);
957 this.getSelectionModel().clearSelections();
964 onEventResize: function(rz, width, height) {
965 var event = rz.event;
968 //event already gone -> late event / busy brower?
972 var rzInfo = event.ui.getRzInfo(rz, width, height);
974 if (rzInfo.diff != 0) {
975 if (rzInfo.duration > 0) {
976 event.set('dtend', rzInfo.dtend);
978 // force event length to at least 1 minute
979 var date = new Date(event.get('dtstart').getTime());
980 date.setMinutes(date.getMinutes() + 1);
981 event.set('dtend', date);
985 if (event.summaryEditor) {
986 event.summaryEditor.setHeight(event.ui.getEls()[0].getHeight() -18);
989 // don't fire update events on rangeAdd
990 if (rzInfo.diff != 0 && event != this.editing && ! event.isRangeAdd) {
991 this.fireEvent('updateEvent', event);
993 event.ui.clearDirty();
1000 onUpdate : function(ds, event){
1001 // don't update events while being created
1002 if (event.get('id').match(/new/)) {
1006 // relayout original context
1007 var originalRegistry = (event.modified.hasOwnProperty('is_all_day_event') ? event.modified.is_all_day_event : event.get('is_all_day_event')) ?
1008 this.parallelWholeDayEventsRegistry :
1009 this.parallelScrollerEventsRegistry;
1011 var registry = event.get('is_all_day_event') ? this.parallelWholeDayEventsRegistry : this.parallelScrollerEventsRegistry;
1012 var originalDtstart = event.modified.hasOwnProperty('dtstart') ? event.modified.dtstart : event.get('dtstart');
1013 var originalDtend = event.modified.hasOwnProperty('dtend') ? event.modified.dtend : event.get('dtend');
1015 var originalParallels = originalRegistry.getEvents(originalDtstart, originalDtend);
1016 for (var j=0; j<originalParallels.length; j++) {
1017 this.removeEvent(originalParallels[j]);
1019 originalRegistry.unregister(event);
1021 var originalParallels = originalRegistry.getEvents(originalDtstart, originalDtend);
1022 for (var j=0; j<originalParallels.length; j++) {
1023 this.insertEvent(originalParallels[j]);
1026 // relayout actual context
1027 var parallelEvents = registry.getEvents(event.get('dtstart'), event.get('dtend'));
1028 for (var j=0; j<parallelEvents.length; j++) {
1029 this.removeEvent(parallelEvents[j]);
1032 registry.register(event);
1033 var parallelEvents = registry.getEvents(event.get('dtstart'), event.get('dtend'));
1034 for (var j=0; j<parallelEvents.length; j++) {
1035 this.insertEvent(parallelEvents[j]);
1038 this.setActiveEvent(this.getActiveEvent());
1045 onAdd : function(ds, records, index){
1046 //console.log('onAdd');
1047 for (var i=0; i<records.length; i++) {
1048 var event = records[i];
1050 var registry = event.get('is_all_day_event') ? this.parallelWholeDayEventsRegistry : this.parallelScrollerEventsRegistry;
1051 registry.register(event);
1053 var parallelEvents = registry.getEvents(event.get('dtstart'), event.get('dtend'));
1055 for (var j=0; j<parallelEvents.length; j++) {
1056 this.removeEvent(parallelEvents[j]);
1057 this.insertEvent(parallelEvents[j]);
1060 //this.setActiveEvent(event);
1069 onRemove : function(ds, event, index, isUpdate) {
1070 if (!event || index == -1) {
1074 if(isUpdate !== true){
1075 //this.fireEvent("beforeeventremoved", this, index, record);
1077 var registry = event.get('is_all_day_event') ? this.parallelWholeDayEventsRegistry : this.parallelScrollerEventsRegistry;
1078 registry.unregister(event);
1079 this.removeEvent(event);
1080 this.getSelectionModel().unselect(event);
1084 onBeforeLoad: function(store, options) {
1085 if (! options.refresh) {
1086 this.store.each(this.removeEvent, this);
1087 this.transitionEvents = [];
1089 this.transitionEvents = this.store.data.items;
1096 onLoad : function() {
1097 if(! this.rendered){
1101 // remove old events
1102 Ext.each(this.transitionEvents, this.removeEvent, this);
1105 this.parallelScrollerEventsRegistry = new Tine.Calendar.ParallelEventsRegistry({dtStart: this.startDate, dtEnd: this.endDate});
1106 this.parallelWholeDayEventsRegistry = new Tine.Calendar.ParallelEventsRegistry({dtStart: this.startDate, dtEnd: this.endDate});
1108 // todo: sort generic?
1109 this.store.fields = Tine.Calendar.Model.Event.prototype.fields;
1110 this.store.sortInfo = {field: 'dtstart', direction: 'ASC'};
1111 this.store.applySort();
1113 this.store.each(function(event) {
1114 var registry = event.get('is_all_day_event') ? this.parallelWholeDayEventsRegistry : this.parallelScrollerEventsRegistry;
1115 registry.register(event);
1118 // put the events in
1119 this.store.each(this.insertEvent, this);
1127 print: function(printMode) {
1128 var renderer = new this.printRenderer({printMode: printMode});
1129 renderer.print(this);
1132 hex2dec: function(hex) {
1134 hex = hex.toString();
1135 var length = hex.length, multiplier, digit;
1136 for (var i=0; i<length; i++) {
1138 multiplier = Math.pow(16, (Math.abs(i - hex.length)-1));
1139 digit = parseInt(hex.toString().charAt([i]), 10);
1141 switch (hex.toString().charAt([i]).toUpperCase()) {
1142 case 'A': digit = 10; break;
1143 case 'B': digit = 11; break;
1144 case 'C': digit = 12; break;
1145 case 'D': digit = 13; break;
1146 case 'E': digit = 14; break;
1147 case 'F': digit = 15; break;
1148 default: return NaN;
1151 dec = dec + (multiplier * digit);
1157 getPeriod: function() {
1159 from: this.startDate,
1160 until: this.startDate.add(Date.DAY, this.numOfDays)
1165 * get date of a (event) target
1167 * @param {Ext.EventObject} e
1170 getTargetDateTime: function(e) {
1171 var target = e.getTarget('div[class^=cal-daysviewpanel-datetime]');
1173 if (target && target.id.match(/^ext-gen\d+:\d+/)) {
1174 var parts = target.id.split(':');
1176 var date = this.startDate.add(Date.DAY, parseInt(parts[1], 10));
1177 date.is_all_day_event = true;
1180 var timePart = this.timeScale.getAt(parts[2]);
1181 date = date.add(Date.MINUTE, timePart.get('minutes'));
1182 date.is_all_day_event = false;
1190 * gets event el of target
1192 * @param {Ext.EventObject} e
1193 * @return {Tine.Calendar.Model.Event}
1195 getTargetEvent: function(e) {
1196 var target = e.getTarget();
1197 var el = Ext.fly(target);
1199 if (el.hasClass('cal-daysviewpanel-event') || (el = el.up('[id*=event:]', 10))) {
1200 var parts = el.dom.id.split(':');
1202 return this.store.getById(parts[1]);
1206 getTimeOffset: function(date, isEnd) {
1207 var d = this.granularityUnitHeights / this.timeGranularity
1208 hours = isEnd && (date.getHours() == 0) ? 24 : date.getHours();
1210 return Math.round(d * ( 60 * hours + date.getMinutes()));
1213 getTimeHeight: function(dtStart, dtEnd) {
1214 var d = this.granularityUnitHeights / this.timeGranularity;
1215 return Math.round(d * ((dtEnd.getTime() - dtStart.getTime()) / Date.msMINUTE));
1218 getHeightMinutes: function(height) {
1219 return Math.round(height * this.timeGranularity / this.granularityUnitHeights);
1223 * fetches elements from our generated dom
1225 initElements : function(){
1226 var E = Ext.Element;
1228 // var el = this.el.dom.firstChild;
1229 var cs = this.el.dom.firstChild.childNodes;
1231 // this.el = new E(el);
1233 this.mainWrap = new E(cs[0]);
1234 this.mainHd = new E(this.mainWrap.dom.firstChild);
1236 this.innerHd = this.mainHd.dom.firstChild;
1238 this.wholeDayScroller = this.innerHd.firstChild.childNodes[1];
1239 this.wholeDayArea = this.wholeDayScroller.firstChild;
1241 this.scroller = new E(this.mainWrap.dom.childNodes[1]);
1242 this.scroller.setStyle('overflow-x', 'hidden');
1243 this.mon(this.scroller, 'scroll', this.onBeforeScroll, this);
1244 this.mon(this.scroller, 'scroll', this.onScroll, this, {buffer: 200});
1246 this.mainBody = new E(this.scroller.dom.firstChild);
1247 this.dayCols = this.mainBody.dom.firstChild.lastChild.childNodes;
1249 this.focusEl = new E(this.el.dom.lastChild.lastChild);
1250 this.focusEl.swallowEvent("click", true);
1251 this.focusEl.swallowEvent("dblclick", true);
1252 this.focusEl.swallowEvent("contextmenu", true);
1254 this.aboveHints = this.mainBody.select('img[class=cal-daysviewpanel-body-daycolumn-hint-above]');
1255 this.belowHints = this.mainBody.select('img[class=cal-daysviewpanel-body-daycolumn-hint-below]');
1259 * @TODO this returns wrong cols on DST boundaries:
1260 * e.g. on DST switch form +2 to +1 an all day event is 25 hrs. long
1265 getColumnNumber: function(date) {
1266 return Math.floor((date.add(Date.SECOND, 1).getTime() - this.startDate.getTime()) / Date.msDAY);
1269 getDateColumnEl: function(pos) {
1270 return this.dayCols[pos];
1273 checkWholeDayEls: function() {
1275 for (var i=0; i<this.wholeDayArea.childNodes.length-1; i++) {
1276 if(this.wholeDayArea.childNodes[i].childNodes.length === 1) {
1281 for (var i=1; i<freeIdxs.length; i++) {
1282 Ext.fly(this.wholeDayArea.childNodes[freeIdxs[i]]).remove();
1289 onLayout: function() {
1290 Tine.Calendar.DaysView.superclass.onLayout.apply(this, arguments);
1292 return; // not rendered
1295 var csize = this.container.getSize(true);
1296 var vw = csize.width;
1298 this.el.setSize(csize.width, csize.height);
1300 // layout whole day area
1301 var wholeDayAreaEl = Ext.get(this.wholeDayArea);
1302 for (var i=0, bottom = wholeDayAreaEl.getTop(); i<this.wholeDayArea.childNodes.length -1; i++) {
1303 bottom = Math.max(parseInt(Ext.get(this.wholeDayArea.childNodes[i]).getBottom(), 10), bottom);
1305 var wholeDayAreaHeight = bottom - wholeDayAreaEl.getTop() + 10;
1306 // take one third of the available height maximum
1307 wholeDayAreaEl.setHeight(wholeDayAreaHeight);
1308 Ext.fly(this.wholeDayScroller).setHeight(Math.min(Math.round(csize.height/3), wholeDayAreaHeight));
1310 var hdHeight = this.mainHd.getHeight();
1311 var vh = csize.height - (hdHeight);
1313 this.scroller.setSize(vw, vh);
1315 // force positioning on scroll hints
1316 this.onScroll.defer(100, this);
1319 onDestroy: function() {
1320 this.removeAllEvents();
1321 this.initData(false);
1322 this.purgeListeners();
1324 Tine.Calendar.DaysView.superclass.onDestroy.apply(this, arguments);
1328 * returns HTML frament of the day headers
1330 getDayHeaders: function() {
1332 var width = 100/this.numOfDays;
1334 for (var i=0, date; i<this.numOfDays; i++) {
1335 var day = this.startDate.add(Date.DAY, i);
1336 html += this.templates.dayHeader.applyTemplate({
1337 day: String.format(this.dayFormatString, day.format('l'), day.format('j'), day.format('F')),
1338 height: this.granularityUnitHeights,
1340 left: i * width + '%'
1347 * updates HTML of day headers
1349 updateDayHeaders: function() {
1350 if (! this.rendered) {
1353 var dayHeaders = Ext.DomQuery.select('div[class=cal-daysviewpanel-dayheader-day]', this.innerHd),
1354 dayWidth = Ext.get(dayHeaders[0]).getWidth(),
1357 for (var i=0, date, isToDay, headerEl, dayColEl; i<dayHeaders.length; i++) {
1359 date = this.startDate.add(Date.DAY, i);
1360 isToDay = date.getTime() == new Date().clearTime().getTime();
1362 headerEl = Ext.get(dayHeaders[i]);
1364 if (dayWidth > 150) {
1365 headerString = String.format(this.dayFormatString, date.format('l'), date.format('j'), date.format('F'));
1366 } else if (dayWidth > 60){
1367 headerString = date.format('D') + ', ' + date.format('j') + '.' + date.format('n');
1369 headerString = date.format('j') + '.' + date.format('n');
1372 headerEl.update(headerString);
1373 headerEl.parent()[(isToDay ? 'add' : 'remove') + 'Class']('cal-daysviewpanel-dayheader-today');
1374 Ext.get(this.dayCols[i])[(isToDay ? 'add' : 'remove') + 'Class']('cal-daysviewpanel-body-daycolumn-today');
1379 * returns HTML fragment of the whole day cols
1381 getWholeDayCols: function() {
1383 var width = 100/this.numOfDays;
1385 var baseId = Ext.id();
1386 for (var i=0; i<this.numOfDays; i++) {
1387 html += this.templates.wholeDayCol.applyTemplate({
1388 //day: date.get('dateString'),
1389 //height: this.granularityUnitHeights,
1390 id: baseId + ':' + i,
1392 left: i * width + '%'
1400 * gets HTML fragment of the horizontal time rows
1402 getTimeRows: function() {
1404 this.timeScale.each(function(time){
1405 var index = time.get('index');
1406 html += this.templates.timeRow.applyTemplate({
1407 cls: index%2 ? 'cal-daysviewpanel-timeRow-off' : 'cal-daysviewpanel-timeRow-on',
1408 height: this.granularityUnitHeights + 'px',
1409 top: index * this.granularityUnitHeights + 'px',
1410 time: index%2 ? '' : time.get('time')
1418 * gets HTML fragment of the day columns
1420 getDayColumns: function() {
1422 var width = 100/this.numOfDays;
1424 for (var i=0; i<this.numOfDays; i++) {
1425 html += this.templates.dayColumn.applyTemplate({
1427 left: i * width + '%',
1428 overRows: this.getOverRows(i)
1436 * gets HTML fragment of the time over rows
1438 getOverRows: function(dayIndex) {
1440 var baseId = Ext.id();
1442 this.timeScale.each(function(time){
1443 var index = time.get('index');
1444 html += this.templates.overRow.applyTemplate({
1445 id: baseId + ':' + dayIndex + ':' + index,
1446 cls: 'cal-daysviewpanel-daycolumn-row-' + (index%2 ? 'off' : 'on'),
1447 height: this.granularityUnitHeights + 'px',
1448 time: time.get('time')
1456 * inits all tempaltes of this view
1458 initTemplates: function() {
1459 var ts = this.templates || {};
1461 ts.master = new Ext.XTemplate(
1462 '<div class="cal-daysviewpanel" hidefocus="true">',
1463 '<div class="cal-daysviewpanel-viewport">',
1464 '<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>',
1465 '<div class="cal-daysviewpanel-scroller"><div class="cal-daysviewpanel-body">{body}</div></div>',
1467 '<a href="#" class="cal-daysviewpanel-focus" tabIndex="-1"></a>',
1471 ts.header = new Ext.XTemplate(
1472 '<div class="cal-daysviewpanel-daysheader">{daysHeader}</div>',
1473 '<div class="cal-daysviewpanel-wholedayheader-scroller">',
1474 '<div class="cal-daysviewpanel-wholedayheader">',
1475 '<div class="cal-daysviewpanel-wholedayheader-daycols">{wholeDayCols}</div>',
1480 ts.dayHeader = new Ext.XTemplate(
1481 '<div class="cal-daysviewpanel-dayheader" style="height: {height}; width: {width}; left: {left};">' +
1482 '<div class="cal-daysviewpanel-dayheader-day-wrap">' +
1483 '<div class="cal-daysviewpanel-dayheader-day">{day}</div>' +
1488 ts.wholeDayCol = new Ext.XTemplate(
1489 '<div class="cal-daysviewpanel-body-wholedaycolumn" style="left: {left}; width: {width};">' +
1490 '<div id="{id}" class="cal-daysviewpanel-datetime cal-daysviewpanel-body-wholedaycolumn-over"> </div>' +
1494 ts.body = new Ext.XTemplate(
1495 '<div class="cal-daysviewpanel-body-inner">' +
1497 '<div class="cal-daysviewpanel-body-daycolumns">{dayColumns}</div>' +
1501 ts.timeRow = new Ext.XTemplate(
1502 '<div class="{cls}" style="height: {height}; top: {top};">',
1503 '<div class="cal-daysviewpanel-timeRow-time">{time}</div>',
1507 ts.dayColumn = new Ext.XTemplate(
1508 '<div class="cal-daysviewpanel-body-daycolumn" style="left: {left}; width: {width};">',
1509 '<div class="cal-daysviewpanel-body-daycolumn-inner"> </div>',
1511 '<img src="', Ext.BLANK_IMAGE_URL, '" class="cal-daysviewpanel-body-daycolumn-hint-above" />',
1512 '<img src="', Ext.BLANK_IMAGE_URL, '" class="cal-daysviewpanel-body-daycolumn-hint-below" />',
1516 ts.overRow = new Ext.XTemplate(
1517 '<div id="{id}" class="cal-daysviewpanel-datetime cal-daysviewpanel-daycolumn-row" style="height: {height};">' +
1518 '<div class="{cls}" >{time}</div>'+
1522 ts.event = new Ext.XTemplate(
1523 '<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};">',
1524 '<div class="cal-daysviewpanel-event-header" style="background-color: {color};">',
1525 '<div class="cal-daysviewpanel-event-header-inner" style="color: {textColor}; background-color: {color}; z-index: {zIndex};">{startTime}</div>',
1526 '<div class="cal-daysviewpanel-event-header-icons">',
1527 '<tpl for="statusIcons">',
1528 '<img src="', Ext.BLANK_IMAGE_URL, '" class="cal-status-icon {status}-{[parent.textColor == \'#FFFFFF\' ? \'white\' : \'black\']}" ext:qtip="{[this.encode(values.text)]}" />',
1532 '<div class="cal-daysviewpanel-event-body">{[Ext.util.Format.nl2br(Ext.util.Format.htmlEncode(values.summary))]}</div>',
1533 '<div class="cal-daysviewpanel-event-tags">{tagsHtml}</div>',
1536 encode: function(v) { return Tine.Tinebase.common.doubleEncode(v); }
1540 ts.wholeDayEvent = new Ext.XTemplate(
1541 '<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};">' +
1542 '<div class="cal-daysviewpanel-wholedayevent-tags">{tagsHtml}</div>' +
1543 '<div class="cal-daysviewpanel-wholedayevent-body">{[Ext.util.Format.nl2br(Ext.util.Format.htmlEncode(values.summary))]}</div>' +
1544 '<div class="cal-daysviewpanel-event-header-icons" style="background-color: {bgColor};" >' +
1545 '<tpl for="statusIcons">' +
1546 '<img src="', Ext.BLANK_IMAGE_URL, '" class="cal-status-icon {status}-black" ext:qtip="{[this.encode(values.text)]}" />',
1551 encode: function(v) { return Tine.Tinebase.common.doubleEncode(v); }
1557 if(t && typeof t.compile == 'function' && !t.compiled){
1558 t.disableFormats = true;
1563 this.templates = ts;
1567 Ext.reg('Tine.Calendar.DaysView', Tine.Calendar.DaysView);