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) defaultStart
90 * generic scroll start of the (work) day
92 defaultStart: '08:00',
94 * @cfg {String} (H:i) dayStart
95 * generic start of the (work) day
99 * @cfg {String} (H:i) dayEnd
100 * generic end of the (work) day
104 * @cfg {Bool} cropDayTime
105 * crop times before and after dayStart and dayEnd
109 * @cfg {Integer} wheelIncrement
110 * the number of pixels to increment on mouse wheel scrolling (defaults to 50)
114 * @cfg {String} newEventSummary
117 newEventSummary: 'New Event',
119 * @cfg {String} dayFormatString
120 * _('{0}, the {1}. of {2}')
122 dayFormatString: '{0}, the {1}. of {2}',
124 * @cfg {Number} timeGranularity
125 * granularity of timegrid in minutes
129 * @cfg {Number} granularityUnitHeights
130 * heights in px of a granularity unit
132 granularityUnitHeights: 18,
134 * @cfg {Boolean} denyDragOnMissingEditGrant
135 * deny drag action if edit grant for event is missing
137 denyDragOnMissingEditGrant: true,
139 * store holding timescale
140 * @type {Ext.data.Store}
144 * The amount of space to reserve for the scrollbar (defaults to 19 pixels)
150 * The time in milliseconds, a scroll should be delayed after using the mousewheel
157 * @property {bool} editing
162 * currently active event
163 * $type {Tine.Calendar.Model.Event}
167 * @property {Ext.data.Store}
173 * updates period to display
174 * @param {Array} period
176 updatePeriod: function(period) {
177 this.startDate = period.from;
179 var tbar = this.findParentBy(function(c) {return c.getTopToolbar()}).getTopToolbar();
181 tbar.periodPicker.update(this.startDate);
182 this.startDate = tbar.periodPicker.getPeriod().from;
185 this.endDate = this.startDate.add(Date.DAY, this.numOfDays+1);
187 //this.parallelScrollerEventsRegistry = new Tine.Calendar.ParallelEventsRegistry({dtStart: this.startDate, dtEnd: this.endDate});
188 //this.parallelWholeDayEventsRegistry = new Tine.Calendar.ParallelEventsRegistry({dtStart: this.startDate, dtEnd: this.endDate});
189 //this.store.each(this.removeEvent, this);
191 this.updateDayHeaders();
192 this.onBeforeScroll();
194 this.fireEvent('changePeriod', period);
200 initComponent: function() {
201 this.app = Tine.Tinebase.appMgr.get('Calendar');
203 this.newEventSummary = this.app.i18n._hidden(this.newEventSummary);
204 this.dayFormatString = this.app.i18n._hidden(this.dayFormatString);
206 this.startDate.setHours(0);
207 this.startDate.setMinutes(0);
208 this.startDate.setSeconds(0);
210 this.endDate = this.startDate.add(Date.DAY, this.numOfDays+1);
212 this.boxMinWidth = 60*this.numOfDays;
214 this.parallelScrollerEventsRegistry = new Tine.Calendar.ParallelEventsRegistry({dtStart: this.startDate, dtEnd: this.endDate});
215 this.parallelWholeDayEventsRegistry = new Tine.Calendar.ParallelEventsRegistry({dtStart: this.startDate, dtEnd: this.endDate});
217 this.initData(this.store);
219 this.initTimeScale();
220 this.initTemplates();
222 this.on('beforehide', this.onBeforeHide, this);
223 this.on('show', this.onShow, this);
225 this.mon(Tine.Tinebase.appMgr, 'activate', this.onAppActivate, this);
227 if (! this.selModel) {
228 this.selModel = this.selModel || new Tine.Calendar.EventSelectionModel();
231 this.onLayout = Function.createBuffered(this.unbufferedOnLayout, 100, this);
234 var prefs = this.app.getRegistry().get('preferences'),
235 defaultStartTime = Date.parseDate(prefs.get('daysviewdefaultstarttime'), 'H:i'),
236 startTime = Date.parseDate(prefs.get('daysviewstarttime'), 'H:i'),
237 endTime = Date.parseDate(prefs.get('daysviewendtime'), 'H:i');
239 this.dayStart = Ext.isDate(startTime) ? startTime : Date.parseDate(this.dayStart, 'H:i');
240 this.dayEnd = Ext.isDate(endTime) ? endTime : Date.parseDate(this.dayEnd, 'H:i');
241 // 00:00 in users timezone is a spechial case where the user expects
242 // something like 24:00 and not 00:00
243 if (this.dayEnd.format('H:i') == '00:00') {
244 this.dayEnd = this.dayEnd.add(Date.MINUTE, -1);
246 this.dayEndPx = this.getTimeOffset(this.dayEnd);
248 this.cropDayTime = !! Tine.Tinebase.configManager.get('daysviewcroptime', 'Calendar') && !(!this.getTimeOffset(this.dayStart) && !this.getTimeOffset(this.dayEnd));
250 if (this.cropDayTime) {
251 this.defaultStart = Ext.isDate(defaultStartTime) ? defaultStartTime : Date.parseDate(this.defaultStart, 'H:i');
253 this.defaultStart = this.dayStart;
256 this.wheelIncrement = Tine.Tinebase.configManager.get('daysviewwheelincrement', 'Calendar') || this.wheelIncrement;
258 Tine.Calendar.DaysView.superclass.initComponent.apply(this, arguments);
263 * @param {Ext.data.Store} ds
265 initData : function(ds){
267 this.store.un("load", this.onLoad, this);
268 this.store.un("beforeload", this.onBeforeLoad, this);
269 this.store.un("add", this.onAdd, this);
270 this.store.un("remove", this.onRemove, this);
271 this.store.un("update", this.onUpdate, this);
274 ds.on("load", this.onLoad, this);
275 ds.on("beforeload", this.onBeforeLoad, this);
276 ds.on("add", this.onAdd, this);
277 ds.on("remove", this.onRemove, this);
278 ds.on("update", this.onUpdate, this);
286 initTimeScale: function() {
288 var scaleSize = Date.msDAY/(this.timeGranularity * Date.msMINUTE);
289 var baseDate = this.startDate.clone();
292 for (var i=0; i<scaleSize; i++) {
293 minutes = i * this.timeGranularity;
294 data.push([i, minutes, minutes * Date.msMINUTE, baseDate.add(Date.MINUTE, minutes).format('H:i')]);
297 this.timeScale = new Ext.data.SimpleStore({
298 fields: ['index', 'minutes', 'milliseconds', 'time'],
304 initDropZone: function() {
305 this.dd = new Ext.dd.DropZone(this.mainWrap.dom, {
306 ddGroup: 'cal-event',
308 notifyOver : function(dd, e, data) {
309 var sourceEl = Ext.fly(data.sourceEl);
310 sourceEl.setStyle({'border-style': 'dashed'});
311 sourceEl.setOpacity(0.5);
314 var event = data.event;
316 var targetDateTime = Tine.Calendar.DaysView.prototype.getTargetDateTime.call(data.scope, e);
317 if (targetDateTime) {
318 var dtString = targetDateTime.format(targetDateTime.is_all_day_event ? Ext.form.DateField.prototype.format : 'H:i');
319 if (! event.data.is_all_day_event) {
320 Ext.fly(dd.proxy.el.query('div[class=cal-daysviewpanel-event-header-inner]')[0]).update(dtString);
323 if (event.get('editGrant')) {
324 return Math.abs(targetDateTime.getTime() - event.get('dtstart').getTime()) < Date.msMINUTE ? 'cal-daysviewpanel-event-drop-nodrop' : 'cal-daysviewpanel-event-drop-ok';
329 return 'cal-daysviewpanel-event-drop-nodrop';
332 notifyOut : function() {
333 //console.log('notifyOut');
337 notifyDrop : function(dd, e, data) {
340 var targetDate = v.getTargetDateTime(e);
343 var event = data.event;
345 var originalDuration = (event.get('dtend').getTime() - event.get('dtstart').getTime()) / Date.msMINUTE;
347 // Get the new endDate to ensure it's not out of croptimes
348 var copyTargetDate = targetDate;
349 var newEnd = copyTargetDate.add(Date.MINUTE, originalDuration);
351 v.dayEnd.setDate(newEnd.getDate());
353 // deny drop for missing edit grant or no time change
354 if (! event.get('editGrant') || Math.abs(targetDate.getTime() - event.get('dtstart').getTime()) < Date.msMINUTE
355 || ((v.cropDayTime == true) && (newEnd > v.dayEnd))) {
361 event.set('dtstart', targetDate);
363 if (! event.get('is_all_day_event') && targetDate.is_all_day_event && event.duration < Date.msDAY) {
364 // draged from scroller -> dropped to allDay and duration less than a day
365 event.set('dtend', targetDate.add(Date.DAY, 1).add(Date.SECOND, -1));
366 } else if (event.get('is_all_day_event') && !targetDate.is_all_day_event) {
367 // draged from allDay -> droped to scroller will be resetted to hone hour
368 event.set('dtend', targetDate.add(Date.HOUR, 1));
370 event.set('dtend', targetDate.add(Date.MINUTE, originalDuration));
373 event.set('is_all_day_event', targetDate.is_all_day_event);
376 v.fireEvent('updateEvent', event);
387 initDragZone: function() {
388 this.scroller.ddScrollConfig = {
389 vthresh: this.granularityUnitHeights * 2,
390 increment: this.granularityUnitHeights * 4,
394 Ext.dd.ScrollManager.register(this.scroller);
397 this.dragZone = new Ext.dd.DragZone(this.el, {
398 ddGroup: 'cal-event',
401 containerScroll: true,
403 getDragData: function(e) {
404 var selected = this.view.getSelectionModel().getSelectedEvents();
406 var eventEl = e.getTarget('div.cal-daysviewpanel-event', 10);
408 var parts = eventEl.id.split(':');
409 var event = this.view.store.getById(parts[1]);
411 // don't allow dragging of dirty events
412 // don't allow dragging with missing edit grant
413 if (! event || event.dirty || (this.view.denyDragOnMissingEditGrant && ! event.get('editGrant'))) {
417 // we need to clone an event with summary in
418 var d = Ext.get(event.ui.domIds[0]).dom.cloneNode(true);
421 if (event.get('is_all_day_event')) {
422 Ext.fly(d).setLeft(0);
424 var width = (Ext.fly(this.view.dayCols[0]).getWidth() * 0.9);
425 Ext.fly(d).setTop(0);
426 Ext.fly(d).setWidth(width);
427 Ext.fly(d).setHeight(this.view.getTimeHeight.call(this.view, event.get('dtstart'), event.get('dtend')));
435 selections: this.view.getSelectionModel().getSelectedEvents()
440 getRepairXY: function(e, dd) {
441 Ext.fly(this.dragData.sourceEl).setStyle({'border-style': 'solid'});
442 Ext.fly(this.dragData.sourceEl).setOpacity(1, 1);
444 return Ext.fly(this.dragData.sourceEl).getXY();
452 onRender: function(container, position) {
453 Tine.Calendar.DaysView.superclass.onRender.apply(this, arguments);
455 this.templates.master.append(this.el.dom, {
456 header: this.templates.header.applyTemplate({
457 daysHeader: this.getDayHeaders(),
458 wholeDayCols: this.getWholeDayCols()
460 body: this.templates.body.applyTemplate({
461 timeRows: this.getTimeRows(),
462 dayColumns: this.getDayColumns()
467 this.getSelectionModel().init(this);
471 * fill the events into the view
473 afterRender: function() {
474 Tine.Calendar.DaysView.superclass.afterRender.apply(this, arguments);
476 this.mon(this.el, 'click', this.onClick, this);
477 this.mon(this.el, 'dblclick', this.onDblClick, this);
478 this.mon(this.el, 'contextmenu', this.onContextMenu, this);
479 this.mon(this.el, 'mousedown', this.onMouseDown, this);
480 this.mon(this.el, 'mouseup', this.onMouseUp, this);
481 this.mon(this.el, 'keydown', this.onKeyDown, this);
486 this.updatePeriod({from: this.startDate});
488 if (this.store.getCount()) {
489 this.onLoad.apply(this);
492 // apply os specific scrolling space
493 Ext.fly(this.innerHd.firstChild.firstChild).setStyle('margin-right', Ext.getScrollBarWidth() + 'px');
496 if (this.cropDayTime) {
497 var cropStartPx = this.getTimeOffset(this.dayStart),
498 cropHeightPx = this.getTimeOffset(this.dayEnd) +2;
500 this.mainBody.setStyle('margin-top', '-' + cropStartPx + 'px');
501 this.mainBody.setStyle('height', cropHeightPx + 'px');
502 this.mainBody.setStyle('overflow', 'hidden');
503 this.scroller.addClass('cal-daysviewpanel-body-cropDayTime');
506 this.unbufferedOnLayout();
508 // scrollTo initial position
509 this.isScrolling = true;
512 this.scrollTo.defer(500, this, [this.defaultStart]);
517 this.rendered = true;
520 scrollTo: function(time) {
521 time = Ext.isDate(time) ? time : new Date();
523 var scrollTop = this.getTimeOffset(time);
524 if (this.cropDayTime) {
525 scrollTop = scrollTop - this.getTimeOffset(this.dayStart);
528 this.scroller.dom.scrollTop = scrollTop;
531 onMouseWheel: function(e) {
532 var d = e.getWheelDelta()*this.wheelIncrement*-1;
535 var scrollTop = this.scroller.dom.scrollTop,
536 newTop = scrollTop + d,
537 sh = this.scroller.dom.scrollHeight-this.scroller.dom.clientHeight;
539 var s = Math.max(0, Math.min(sh, newTop));
541 this.scroller.scrollTo('top', s);
545 onBeforeScroll: function() {
546 if (! this.isScrolling) {
547 this.isScrolling = true;
549 // walk all cols an hide hints
550 Ext.each(this.dayCols, function(dayCol, idx) {
551 this.aboveHints.item(idx).setDisplayed(false);
552 this.belowHints.item(idx).setDisplayed(false);
558 * add hint if events are outside visible area
564 onScroll: function(e, t, o) {
565 var visibleHeight = this.scroller.dom.clientHeight,
566 visibleStart = this.scroller.dom.scrollTop - this.mainBody.dom.offsetTop,
567 visibleEnd = visibleStart + visibleHeight,
568 vStartMinutes = this.getHeightMinutes(visibleStart),
569 vEndMinutes = this.getHeightMinutes(visibleEnd);
571 Ext.each(this.dayCols, function(dayCol, idx) {
572 var dayColEl = Ext.get(dayCol),
573 dayStart = this.startDate.add(Date.DAY, idx),
574 aboveEvents = this.parallelScrollerEventsRegistry.getEvents(dayStart, dayStart.add(Date.MINUTE, vStartMinutes)),
575 belowEvents = this.parallelScrollerEventsRegistry.getEvents(dayStart.add(Date.MINUTE, vEndMinutes), dayStart.add(Date.DAY, 1));
577 if (aboveEvents.length) {
578 var aboveHint = this.aboveHints.item(idx);
579 aboveHint.setTop(visibleStart + 5);
580 if (!aboveHint.isVisible()) {
581 aboveHint.fadeIn({duration: 1.6});
585 if (belowEvents.length) {
586 var belowHint = this.belowHints.item(idx);
587 belowHint.setTop(visibleEnd - 14);
588 if (!belowHint.isVisible()) {
589 belowHint.fadeIn({duration: 1.6});
594 this.isScrolling = false;
599 this.scroller.dom.scrollTop = this.lastScrollPos || this.getTimeOffset(this.defaultStart);
602 onBeforeHide: function() {
603 this.lastScrollPos = this.scroller.dom.scrollTop;
607 * renders a single event into this daysview
608 * @param {Tine.Calendar.Model.Event} event
610 * @todo Add support vor Events spanning over a day boundary
612 insertEvent: function(event) {
613 event.ui = new Tine.Calendar.DaysViewEventUI(event);
614 event.ui.render(this);
620 removeAllEvents: function() {
621 this.store.each(function(event) {
629 * removes a event from the dom
630 * @param {Tine.Calendar.Model.Event} event
632 removeEvent: function(event) {
633 if (event == this.activeEvent) {
634 this.activeEvent = null;
638 this.abortCreateEvent(event);
647 * sets currentlcy active event
649 * NOTE: active != selected
650 * @param {Tine.Calendar.Model.Event} event
652 setActiveEvent: function(event) {
653 this.activeEvent = event || null;
657 * gets currentlcy active event
659 * @return {Tine.Calendar.Model.Event} event
661 getActiveEvent: function() {
662 return this.activeEvent;
666 * returns the selectionModel of the active panel
669 getSelectionModel: function() {
670 return this.selModel;
674 * creates a new event directly from this view
677 createEvent: function(e, event) {
679 // only add range events if mouse is down long enough
680 if (this.editing || (event.isRangeAdd && ! this.mouseDown)) {
684 // insert event silently into store
685 this.editing = event;
686 this.store.suspendEvents();
687 this.store.add(event);
688 this.store.resumeEvents();
691 var registry = event.get('is_all_day_event') ? this.parallelWholeDayEventsRegistry : this.parallelScrollerEventsRegistry;
692 registry.register(event);
693 this.insertEvent(event);
694 //this.setActiveEvent(event);
697 //var eventEls = event.ui.getEls();
698 //eventEls[0].setStyle({'border-style': 'dashed'});
699 //eventEls[0].setOpacity(0.5);
701 // start sizing for range adds
702 if (event.isRangeAdd) {
703 // don't create events with very small duration
704 event.ui.resizeable.on('resize', function() {
705 if (event.get('is_all_day_event')) {
708 var keep = (event.get('dtend').getTime() - event.get('dtstart').getTime()) / Date.msMINUTE >= this.timeGranularity;
712 this.startEditSummary(event);
714 this.abortCreateEvent(event);
718 var rzPos = event.get('is_all_day_event') ? 'east' : 'south';
721 e.browserEvent = {type: 'mousedown'};
724 event.ui.resizeable[rzPos].onMouseDown.call(event.ui.resizeable[rzPos], e);
725 //event.ui.resizeable.startSizing.defer(2000, event.ui.resizeable, [e, event.ui.resizeable[rzPos]]);
727 this.startEditSummary(event);
731 abortCreateEvent: function(event) {
732 this.store.remove(event);
733 this.editing = false;
736 startEditSummary: function(event) {
737 if (event.summaryEditor) {
741 var eventEls = event.ui.getEls();
743 var bodyCls = event.get('is_all_day_event') ? 'cal-daysviewpanel-wholedayevent-body' : 'cal-daysviewpanel-event-body';
744 event.summaryEditor = new Ext.form.TextArea({
746 renderTo: eventEls[0].down('div[class=' + bodyCls + ']'),
747 width: event.ui.getEls()[0].getWidth() -12,
748 height: Math.max(12, event.ui.getEls()[0].getHeight() -18),
749 style: 'background-color: transparent; background: 0: border: 0; position: absolute; top: 0px;',
750 value: this.newEventSummary,
752 maxLengthText: this.app.i18n._('The summary must not be longer than 255 characters.'),
754 minLengthText: this.app.i18n._('The summary must have at least 1 character.'),
755 enableKeyEvents: true,
758 render: function(field) {
759 field.focus(true, 100);
761 blur: this.endEditSummary,
762 specialkey: this.endEditSummary,
763 keydown: this.endEditSummary
769 endEditSummary: function(field, e) {
770 var event = field.event;
771 var summary = field.getValue();
773 if (! this.editing || this.validateMsg || !Ext.isDefined(e)) {
777 // abort edit on ESC key
778 if (e && (e.getKey() == e.ESC)) {
779 this.abortCreateEvent(event);
783 // only commit edit on Enter & blur
784 if (e && e.getKey() != e.ENTER) {
788 // Validate Summary maxLength
789 if (summary.length > field.maxLength) {
791 this.validateMsg = Ext.Msg.alert(this.app.i18n._('Summary too Long'), field.maxLengthText, function(){
793 this.validateMsg = false;
798 // Validate Summary minLength
799 if (!summary || summary.match(/^\s{1,}$/) || summary.length < field.minLength) {
801 this.validateMsg = Ext.Msg.alert(this.app.i18n._('Summary too Short'), field.minLengthText, function(){
803 this.validateMsg = false;
808 this.editing = false;
809 event.summaryEditor = false;
811 event.set('summary', summary);
813 this.store.suspendEvents();
814 this.store.remove(event);
815 this.store.resumeEvents();
817 var registry = event.get('is_all_day_event') ? this.parallelWholeDayEventsRegistry : this.parallelScrollerEventsRegistry;
818 registry.unregister(event);
819 this.removeEvent(event);
822 this.store.add(event);
823 this.fireEvent('addEvent', event);
826 onAppActivate: function(app) {
827 if (app === this.app) {
828 this.redrawWholeDayEvents();
832 onResize: function() {
833 Tine.Calendar.DaysView.superclass.onResize.apply(this, arguments);
835 this.updateDayHeaders();
836 this.redrawWholeDayEvents.defer(50, this);
839 redrawWholeDayEvents: function() {
840 this.store.each(function(event) {
841 // check if event is currently visible by looking into ui.domIds
842 if (event.ui && event.ui.domIds.length > 0 && event.get('is_all_day_event')) {
843 this.removeEvent(event);
844 this.insertEvent(event);
849 onClick: function(e) {
850 // check for hint clicks first
851 var hint = e.getTarget('img[class^=cal-daysviewpanel-body-daycolumn-hint-]', 10, true);
853 this.scroller.scroll(hint.hasClass('cal-daysviewpanel-body-daycolumn-hint-above') ? 't' : 'b', 10000, true);
857 var event = this.getTargetEvent(e);
859 this.fireEvent('click', event, e);
863 onContextMenu: function(e) {
864 this.fireEvent('contextmenu', e);
867 onKeyDown : function(e){
868 this.fireEvent("keydown", e);
874 onDblClick: function(e, target) {
876 var event = this.getTargetEvent(e);
877 var dtStart = this.getTargetDateTime(e);
880 this.fireEvent('dblclick', event, e);
881 } else if (dtStart && !this.editing) {
882 var newId = 'cal-daysviewpanel-new-' + Ext.id();
883 var dtend = dtStart.add(Date.HOUR, 1);
884 if (dtStart.is_all_day_event) {
885 dtend = dtend.add(Date.HOUR, 23).add(Date.SECOND, -1);
888 // do not create an event exceeding the crop day time limit
889 else if (this.cropDayTime) {
891 if (dtStart.format(format) >= this.dayEnd.format(format)) {
895 if (dtend.format(format) >= this.dayEnd.format(format)) {
896 dtend.setHours(this.dayEnd.getHours());
897 dtend.setMinutes(this.dayEnd.getMinutes());
898 dtend.setSeconds(this.dayEnd.getSeconds());
902 var event = new Tine.Calendar.Model.Event(Ext.apply(Tine.Calendar.Model.Event.getDefaultData(), {
906 is_all_day_event: dtStart.is_all_day_event
909 this.createEvent(e, event);
911 } else if (target.className == 'cal-daysviewpanel-dayheader-day'){
912 var dayHeaders = Ext.DomQuery.select('div[class=cal-daysviewpanel-dayheader-day]', this.innerHd);
913 var date = this.startDate.add(Date.DAY, dayHeaders.indexOf(target));
914 this.fireEvent('changeView', 'day', date);
921 onMouseDown: function(e) {
922 // only care for left mouse button
923 if (e.button !== 0) {
927 if (! this.editing) {
928 this.focusEl.focus();
930 this.mouseDown = true;
932 var targetEvent = this.getTargetEvent(e);
933 if (this.editing && this.editing.summaryEditor && (targetEvent != this.editing)) {
934 this.editing.summaryEditor.fireEvent('blur', this.editing.summaryEditor, null);
938 var sm = this.getSelectionModel();
939 sm.select(targetEvent);
941 var dtStart = this.getTargetDateTime(e);
943 var newId = 'cal-daysviewpanel-new-' + Ext.id();
944 var event = new Tine.Calendar.Model.Event(Ext.apply(Tine.Calendar.Model.Event.getDefaultData(), {
947 dtend: dtStart.is_all_day_event ? dtStart.add(Date.HOUR, 24).add(Date.SECOND, -1) : dtStart.add(Date.MINUTE, 2*this.timeGranularity/2),
948 is_all_day_event: dtStart.is_all_day_event
950 event.isRangeAdd = true;
954 this.createEvent.defer(100, this, [e, event]);
961 onMouseUp: function() {
962 this.mouseDown = false;
968 onBeforeEventResize: function(rz, e) {
969 var parts = rz.el.id.split(':');
970 var event = this.store.getById(parts[1]);
973 rz.originalHeight = rz.el.getHeight();
974 rz.originalWidth = rz.el.getWidth();
976 // NOTE: ext dosn't support move events via api
977 rz.onMouseMove = rz.onMouseMove.createSequence(function() {
978 var event = this.event;
980 //event already gone -> late event / busy brower?
984 var rzInfo = ui.getRzInfo(this);
986 this.durationEl.update(rzInfo.dtend.format(event.get('is_all_day_event') ? Ext.form.DateField.prototype.format : 'H:i'));
989 event.ui.markDirty();
991 // NOTE: Ext keeps proxy if element is not destroyed (diff !=0)
992 if (! rz.durationEl) {
993 rz.durationEl = rz.el.insertFirst({
994 'class': 'cal-daysviewpanel-event-rzduration',
995 'style': 'position: absolute; bottom: 3px; right: 2px; z-index: 1000;'
998 rz.durationEl.update(event.get('dtend').format(event.get('is_all_day_event') ? Ext.form.DateField.prototype.format : 'H:i'));
1001 this.getSelectionModel().select(event);
1003 this.getSelectionModel().clearSelections();
1010 onEventResize: function(rz, width, height) {
1011 var event = rz.event;
1014 //event already gone -> late event / busy brower?
1018 var rzInfo = event.ui.getRzInfo(rz, width, height);
1020 if (rzInfo.diff != 0) {
1021 if (rzInfo.duration > 0) {
1022 event.set('dtend', rzInfo.dtend);
1024 // force event length to at least 1 minute
1025 var date = new Date(event.get('dtstart').getTime());
1026 date.setMinutes(date.getMinutes() + 1);
1027 event.set('dtend', date);
1031 if (event.summaryEditor) {
1032 event.summaryEditor.setHeight(event.ui.getEls()[0].getHeight() -18);
1035 // don't fire update events on rangeAdd
1036 if (rzInfo.diff != 0 && event != this.editing && ! event.isRangeAdd) {
1037 this.fireEvent('updateEvent', event);
1039 event.ui.clearDirty();
1046 onUpdate : function(ds, event){
1047 // don't update events while being created
1048 if (event.get('id').match(/new/)) {
1052 // relayout original context
1053 var originalRegistry = (event.modified.hasOwnProperty('is_all_day_event') ? event.modified.is_all_day_event : event.get('is_all_day_event')) ?
1054 this.parallelWholeDayEventsRegistry :
1055 this.parallelScrollerEventsRegistry;
1057 var registry = event.get('is_all_day_event') ? this.parallelWholeDayEventsRegistry : this.parallelScrollerEventsRegistry;
1058 var originalDtstart = event.modified.hasOwnProperty('dtstart') ? event.modified.dtstart : event.get('dtstart');
1059 var originalDtend = event.modified.hasOwnProperty('dtend') ? event.modified.dtend : event.get('dtend');
1061 var originalParallels = originalRegistry.getEvents(originalDtstart, originalDtend);
1062 for (var j=0; j<originalParallels.length; j++) {
1063 this.removeEvent(originalParallels[j]);
1065 originalRegistry.unregister(event);
1067 var originalParallels = originalRegistry.getEvents(originalDtstart, originalDtend);
1068 for (var j=0; j<originalParallels.length; j++) {
1069 this.insertEvent(originalParallels[j]);
1072 // relayout actual context
1073 var parallelEvents = registry.getEvents(event.get('dtstart'), event.get('dtend'));
1074 for (var j=0; j<parallelEvents.length; j++) {
1075 this.removeEvent(parallelEvents[j]);
1078 registry.register(event);
1079 var parallelEvents = registry.getEvents(event.get('dtstart'), event.get('dtend'));
1080 for (var j=0; j<parallelEvents.length; j++) {
1081 this.insertEvent(parallelEvents[j]);
1084 this.setActiveEvent(this.getActiveEvent());
1091 onAdd : function(ds, records, index){
1092 //console.log('onAdd');
1093 for (var i=0; i<records.length; i++) {
1094 var event = records[i];
1096 var registry = event.get('is_all_day_event') ? this.parallelWholeDayEventsRegistry : this.parallelScrollerEventsRegistry;
1097 registry.register(event);
1099 var parallelEvents = registry.getEvents(event.get('dtstart'), event.get('dtend'));
1101 for (var j=0; j<parallelEvents.length; j++) {
1102 this.removeEvent(parallelEvents[j]);
1103 this.insertEvent(parallelEvents[j]);
1106 //this.setActiveEvent(event);
1115 onRemove : function(ds, event, index, isUpdate) {
1116 if (!event || index == -1) {
1120 if(isUpdate !== true){
1121 //this.fireEvent("beforeeventremoved", this, index, record);
1123 var registry = event.get('is_all_day_event') ? this.parallelWholeDayEventsRegistry : this.parallelScrollerEventsRegistry;
1124 registry.unregister(event);
1125 this.removeEvent(event);
1126 this.getSelectionModel().unselect(event);
1130 onBeforeLoad: function(store, options) {
1131 if (! options.refresh) {
1132 this.store.each(this.removeEvent, this);
1133 this.transitionEvents = [];
1135 this.transitionEvents = this.store.data.items;
1142 onLoad : function() {
1143 if(! this.rendered){
1147 // remove old events
1148 Ext.each(this.transitionEvents, this.removeEvent, this);
1151 this.parallelScrollerEventsRegistry = new Tine.Calendar.ParallelEventsRegistry({dtStart: this.startDate, dtEnd: this.endDate});
1152 this.parallelWholeDayEventsRegistry = new Tine.Calendar.ParallelEventsRegistry({dtStart: this.startDate, dtEnd: this.endDate});
1154 // todo: sort generic?
1155 this.store.fields = Tine.Calendar.Model.Event.prototype.fields;
1156 this.store.sortInfo = {field: 'dtstart', direction: 'ASC'};
1157 this.store.applySort();
1159 this.store.each(function(event) {
1160 var registry = event.get('is_all_day_event') ? this.parallelWholeDayEventsRegistry : this.parallelScrollerEventsRegistry;
1161 registry.register(event);
1164 // put the events in
1165 this.store.each(this.insertEvent, this);
1173 print: function(printMode) {
1174 var renderer = new this.printRenderer({printMode: printMode});
1175 renderer.print(this);
1178 hex2dec: function(hex) {
1180 hex = hex.toString();
1181 var length = hex.length, multiplier, digit;
1182 for (var i=0; i<length; i++) {
1184 multiplier = Math.pow(16, (Math.abs(i - hex.length)-1));
1185 digit = parseInt(hex.toString().charAt([i]), 10);
1187 switch (hex.toString().charAt([i]).toUpperCase()) {
1188 case 'A': digit = 10; break;
1189 case 'B': digit = 11; break;
1190 case 'C': digit = 12; break;
1191 case 'D': digit = 13; break;
1192 case 'E': digit = 14; break;
1193 case 'F': digit = 15; break;
1194 default: return NaN;
1197 dec = dec + (multiplier * digit);
1203 getPeriod: function() {
1205 from: this.startDate,
1206 until: this.startDate.add(Date.DAY, this.numOfDays)
1211 * get date of a (event) target
1213 * @param {Ext.EventObject} e
1216 getTargetDateTime: function(e) {
1217 var target = e.getTarget('div[class^=cal-daysviewpanel-datetime]');
1219 if (target && target.id.match(/^ext-gen\d+:\d+/)) {
1220 var parts = target.id.split(':');
1222 var date = this.startDate.add(Date.DAY, parseInt(parts[1], 10));
1223 date.is_all_day_event = true;
1226 var timePart = this.timeScale.getAt(parts[2]);
1227 date = date.add(Date.MINUTE, timePart.get('minutes'));
1228 date.is_all_day_event = false;
1236 * gets event el of target
1238 * @param {Ext.EventObject} e
1239 * @return {Tine.Calendar.Model.Event}
1241 getTargetEvent: function(e) {
1242 var target = e.getTarget();
1243 var el = Ext.fly(target);
1245 if (el.hasClass('cal-daysviewpanel-event') || (el = el.up('[id*=event:]', 10))) {
1246 var parts = el.dom.id.split(':');
1248 return this.store.getById(parts[1]);
1252 getTimeOffset: function(date) {
1253 var d = this.granularityUnitHeights / this.timeGranularity;
1255 return Math.round(d * ( 60 * date.getHours() + date.getMinutes()));
1258 getTimeHeight: function(dtStart, dtEnd) {
1259 var d = this.granularityUnitHeights / this.timeGranularity;
1260 return Math.round(d * ((dtEnd.getTime() - dtStart.getTime()) / Date.msMINUTE));
1263 getHeightMinutes: function(height) {
1264 return Math.round(height * this.timeGranularity / this.granularityUnitHeights);
1268 * fetches elements from our generated dom
1270 initElements : function(){
1271 var E = Ext.Element;
1273 // var el = this.el.dom.firstChild;
1274 var cs = this.el.dom.firstChild.childNodes;
1276 // this.el = new E(el);
1278 this.mainWrap = new E(cs[0]);
1279 this.mainHd = new E(this.mainWrap.dom.firstChild);
1281 this.innerHd = this.mainHd.dom.firstChild;
1283 this.wholeDayScroller = this.innerHd.firstChild.childNodes[1];
1284 this.wholeDayArea = this.wholeDayScroller.firstChild;
1286 this.scroller = new E(this.mainWrap.dom.childNodes[1]);
1287 this.scroller.setStyle('overflow-x', 'hidden');
1290 this.mon(this.scroller, 'mousewheel', this.onMouseWheel, this);
1291 this.mon(this.scroller, 'scroll', this.onBeforeScroll, this);
1292 this.mon(this.scroller, 'scroll', this.onScroll, this, {buffer: 200});
1294 this.mainBody = new E(this.scroller.dom.firstChild);
1295 this.dayCols = this.mainBody.dom.firstChild.lastChild.childNodes;
1297 this.focusEl = new E(this.el.dom.lastChild.lastChild);
1298 this.focusEl.swallowEvent("click", true);
1299 this.focusEl.swallowEvent("dblclick", true);
1300 this.focusEl.swallowEvent("contextmenu", true);
1302 this.aboveHints = this.mainBody.select('img[class=cal-daysviewpanel-body-daycolumn-hint-above]');
1303 this.belowHints = this.mainBody.select('img[class=cal-daysviewpanel-body-daycolumn-hint-below]');
1307 * @TODO this returns wrong cols on DST boundaries:
1308 * e.g. on DST switch form +2 to +1 an all day event is 25 hrs. long
1313 getColumnNumber: function(date) {
1314 return Math.floor((date.add(Date.SECOND, 1).getTime() - this.startDate.getTime()) / Date.msDAY);
1317 getDateColumnEl: function(pos) {
1318 return this.dayCols[pos];
1321 checkWholeDayEls: function() {
1323 for (var i=0; i<this.wholeDayArea.childNodes.length-1; i++) {
1324 if(this.wholeDayArea.childNodes[i].childNodes.length === 1) {
1329 for (var i=1; i<freeIdxs.length; i++) {
1330 Ext.fly(this.wholeDayArea.childNodes[freeIdxs[i]]).remove();
1335 * buffered version of this.unbufferedOnLayout
1336 * @see this.initComponent
1338 onLayout: Ext.emptyFn,
1343 unbufferedOnLayout: function() {
1344 Tine.Calendar.DaysView.superclass.onLayout.apply(this, arguments);
1346 return; // not rendered
1349 var csize = this.container.getSize(true);
1350 var vw = csize.width;
1352 this.el.setSize(csize.width, csize.height);
1354 // layout whole day area
1355 var wholeDayAreaEl = Ext.get(this.wholeDayArea);
1356 for (var i=0, bottom = wholeDayAreaEl.getTop(); i<this.wholeDayArea.childNodes.length -1; i++) {
1357 bottom = Math.max(parseInt(Ext.get(this.wholeDayArea.childNodes[i]).getBottom(), 10), bottom);
1359 var wholeDayAreaHeight = bottom - wholeDayAreaEl.getTop() + 10;
1360 // take one third of the available height maximum
1361 wholeDayAreaEl.setHeight(wholeDayAreaHeight);
1362 Ext.fly(this.wholeDayScroller).setHeight(Math.min(Math.round(csize.height/3), wholeDayAreaHeight));
1364 var hdHeight = this.mainHd.getHeight();
1365 var vh = csize.height - (hdHeight);
1367 this.scroller.setSize(vw, vh);
1369 // force positioning on scroll hints
1370 this.onScroll.defer(100, this);
1373 onDestroy: function() {
1374 this.removeAllEvents();
1375 this.initData(false);
1376 this.purgeListeners();
1378 Tine.Calendar.DaysView.superclass.onDestroy.apply(this, arguments);
1382 * returns HTML frament of the day headers
1384 getDayHeaders: function() {
1386 var width = 100/this.numOfDays;
1388 for (var i=0, date; i<this.numOfDays; i++) {
1389 var day = this.startDate.add(Date.DAY, i);
1390 html += this.templates.dayHeader.applyTemplate({
1391 day: String.format(this.dayFormatString, day.format('l'), day.format('j'), day.format('F')),
1392 height: this.granularityUnitHeights,
1394 left: i * width + '%'
1401 * updates HTML of day headers
1403 updateDayHeaders: function() {
1404 if (! this.rendered) {
1407 var dayHeaders = Ext.DomQuery.select('div[class=cal-daysviewpanel-dayheader-day]', this.innerHd),
1408 dayWidth = Ext.get(dayHeaders[0]).getWidth(),
1411 for (var i=0, date, isToDay, headerEl, dayColEl; i<dayHeaders.length; i++) {
1413 date = this.startDate.add(Date.DAY, i);
1414 isToDay = date.getTime() == new Date().clearTime().getTime();
1416 headerEl = Ext.get(dayHeaders[i]);
1418 if (dayWidth > 150) {
1419 headerString = String.format(this.dayFormatString, date.format('l'), date.format('j'), date.format('F'));
1420 } else if (dayWidth > 60){
1421 headerString = date.format('D') + ', ' + date.format('j') + '.' + date.format('n');
1423 headerString = date.format('j') + '.' + date.format('n');
1426 headerEl.update(headerString);
1427 headerEl.parent()[(isToDay ? 'add' : 'remove') + 'Class']('cal-daysviewpanel-dayheader-today');
1428 Ext.get(this.dayCols[i])[(isToDay ? 'add' : 'remove') + 'Class']('cal-daysviewpanel-body-daycolumn-today');
1433 * returns HTML fragment of the whole day cols
1435 getWholeDayCols: function() {
1437 var width = 100/this.numOfDays;
1439 var baseId = Ext.id();
1440 for (var i=0; i<this.numOfDays; i++) {
1441 html += this.templates.wholeDayCol.applyTemplate({
1442 //day: date.get('dateString'),
1443 //height: this.granularityUnitHeights,
1444 id: baseId + ':' + i,
1446 left: i * width + '%'
1454 * gets HTML fragment of the horizontal time rows
1456 getTimeRows: function() {
1458 this.timeScale.each(function(time){
1459 var index = time.get('index');
1460 html += this.templates.timeRow.applyTemplate({
1461 cls: index%2 ? 'cal-daysviewpanel-timeRow-off' : 'cal-daysviewpanel-timeRow-on',
1462 height: this.granularityUnitHeights + 'px',
1463 top: index * this.granularityUnitHeights + 'px',
1464 time: index%2 ? '' : time.get('time')
1472 * gets HTML fragment of the day columns
1474 getDayColumns: function() {
1476 var width = 100/this.numOfDays;
1478 for (var i=0; i<this.numOfDays; i++) {
1479 html += this.templates.dayColumn.applyTemplate({
1481 left: i * width + '%',
1482 overRows: this.getOverRows(i)
1490 * gets HTML fragment of the time over rows
1492 getOverRows: function(dayIndex) {
1494 var baseId = Ext.id();
1496 this.timeScale.each(function(time){
1497 var index = time.get('index');
1498 html += this.templates.overRow.applyTemplate({
1499 id: baseId + ':' + dayIndex + ':' + index,
1500 cls: 'cal-daysviewpanel-daycolumn-row-' + (index%2 ? 'off' : 'on'),
1501 height: this.granularityUnitHeights + 'px',
1502 time: time.get('time')
1510 * inits all tempaltes of this view
1512 initTemplates: function() {
1513 var ts = this.templates || {};
1515 ts.master = new Ext.XTemplate(
1516 '<div class="cal-daysviewpanel" hidefocus="true">',
1517 '<div class="cal-daysviewpanel-viewport">',
1518 '<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>',
1519 '<div class="cal-daysviewpanel-scroller"><div class="cal-daysviewpanel-body">{body}</div></div>',
1521 '<a href="#" class="cal-daysviewpanel-focus" tabIndex="-1"></a>',
1525 ts.header = new Ext.XTemplate(
1526 '<div class="cal-daysviewpanel-daysheader">{daysHeader}</div>',
1527 '<div class="cal-daysviewpanel-wholedayheader-scroller">',
1528 '<div class="cal-daysviewpanel-wholedayheader">',
1529 '<div class="cal-daysviewpanel-wholedayheader-daycols">{wholeDayCols}</div>',
1534 ts.dayHeader = new Ext.XTemplate(
1535 '<div class="cal-daysviewpanel-dayheader" style="height: {height}; width: {width}; left: {left};">' +
1536 '<div class="cal-daysviewpanel-dayheader-day-wrap">' +
1537 '<div class="cal-daysviewpanel-dayheader-day">{day}</div>' +
1542 ts.wholeDayCol = new Ext.XTemplate(
1543 '<div class="cal-daysviewpanel-body-wholedaycolumn" style="left: {left}; width: {width};">' +
1544 '<div id="{id}" class="cal-daysviewpanel-datetime cal-daysviewpanel-body-wholedaycolumn-over"> </div>' +
1548 ts.body = new Ext.XTemplate(
1549 '<div class="cal-daysviewpanel-body-inner">' +
1551 '<div class="cal-daysviewpanel-body-daycolumns">{dayColumns}</div>' +
1555 ts.timeRow = new Ext.XTemplate(
1556 '<div class="{cls}" style="height: {height}; top: {top};">',
1557 '<div class="cal-daysviewpanel-timeRow-time">{time}</div>',
1561 ts.dayColumn = new Ext.XTemplate(
1562 '<div class="cal-daysviewpanel-body-daycolumn" style="left: {left}; width: {width};">',
1563 '<div class="cal-daysviewpanel-body-daycolumn-inner"> </div>',
1565 '<img src="', Ext.BLANK_IMAGE_URL, '" class="cal-daysviewpanel-body-daycolumn-hint-above" />',
1566 '<img src="', Ext.BLANK_IMAGE_URL, '" class="cal-daysviewpanel-body-daycolumn-hint-below" />',
1570 ts.overRow = new Ext.XTemplate(
1571 '<div id="{id}" class="cal-daysviewpanel-datetime cal-daysviewpanel-daycolumn-row" style="height: {height};">' +
1572 '<div class="{cls}" >{time}</div>'+
1576 ts.event = new Ext.XTemplate(
1577 '<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};">',
1578 '<div class="cal-daysviewpanel-event-header" style="background-color: {color};">',
1579 '<div class="cal-daysviewpanel-event-header-inner" style="color: {textColor}; background-color: {color}; z-index: {zIndex};">{startTime}</div>',
1580 '<div class="cal-daysviewpanel-event-header-icons">',
1581 '<tpl for="statusIcons">',
1582 '<img src="', Ext.BLANK_IMAGE_URL, '" class="cal-status-icon {status}-{[parent.textColor == \'#FFFFFF\' ? \'white\' : \'black\']}" ext:qtip="{[this.encode(values.text)]}" />',
1586 '<div class="cal-daysviewpanel-event-body">{[Ext.util.Format.nl2br(Ext.util.Format.htmlEncode(values.summary))]}</div>',
1587 '<div class="cal-daysviewpanel-event-tags">{tagsHtml}</div>',
1590 encode: function(v) { return Tine.Tinebase.common.doubleEncode(v); }
1594 ts.wholeDayEvent = new Ext.XTemplate(
1595 '<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};">' +
1596 '<div class="cal-daysviewpanel-wholedayevent-tags">{tagsHtml}</div>' +
1597 '<div class="cal-daysviewpanel-wholedayevent-body">{[Ext.util.Format.nl2br(Ext.util.Format.htmlEncode(values.summary))]}</div>' +
1598 '<div class="cal-daysviewpanel-event-header-icons" style="background-color: {bgColor};" >' +
1599 '<tpl for="statusIcons">' +
1600 '<img src="', Ext.BLANK_IMAGE_URL, '" class="cal-status-icon {status}-black" ext:qtip="{[this.encode(values.text)]}" />',
1605 encode: function(v) { return Tine.Tinebase.common.doubleEncode(v); }
1611 if(t && typeof t.compile == 'function' && !t.compiled){
1612 t.disableFormats = true;
1617 this.templates = ts;
1621 Ext.reg('Tine.Calendar.DaysView', Tine.Calendar.DaysView);