/* * Tine 2.0 * * @license http://www.gnu.org/licenses/agpl.html AGPL Version 3 * @author Cornelius Weiss * @copyright Copyright (c) 2007-2008 Metaways Infosystems GmbH (http://www.metaways.de) */ Ext.ns('Tine.Calendar'); require('../styles/monthviewpanel.scss'); require('./Printer/MonthView'); /** * @namespace Tine.Calendar * @class Tine.Calendar.MonthView * @extends Ext.util.Observable * @constructor * @param {Object} config */ Tine.Calendar.MonthView = function(config){ Ext.apply(this, config); Tine.Calendar.MonthView.superclass.constructor.call(this); this.printRenderer = Tine.Calendar.Printer.MonthViewRenderer; this.addEvents( /** * @event click * fired if an event got clicked * @param {Tine.Calendar.Model.Event} event * @param {Ext.EventObject} e */ 'click', /** * @event contextmenu * fired if an event got contextmenu * @param {Ext.EventObject} e */ 'contextmenu', /** * @event dblclick * fired if an event got dblclicked * @param {Tine.Calendar.Model.Event} event * @param {Ext.EventObject} e */ 'dblclick', /** * @event changeView * fired if user wants to change view * @param {String} requested view name * @param {mixed} start param of requested view */ 'changeView', /** * @event changePeriod * fired when period changed * @param {Object} period */ 'changePeriod', /** * @event addEvent * fired when a new event got inserted * * @param {Tine.Calendar.Model.Event} event */ 'addEvent', /** * @event updateEvent * fired when an event go resised/moved * * @param {Tine.Calendar.Model.Event} event */ 'updateEvent' ); }; Ext.extend(Tine.Calendar.MonthView, Ext.Container, { /** * @cfg {Date} startDate * start date */ startDate: new Date().clearTime(), /** * @cfg {String} newEventSummary * i18n._('New Event') */ newEventSummary: 'New Event', /** * @cfg {String} calWeekString * i18n._('WK') */ calWeekString: 'WK', /** * @cfg String moreString * i18n._('{0} more...') */ moreString: '{0} more...', /** * @cfg {Array} monthNames * An array of textual month names which can be overriden for localization support (defaults to Date.monthNames) */ monthNames : Date.monthNames, /** * @cfg {Array} dayNames * An array of textual day names which can be overriden for localization support (defaults to Date.dayNames) */ dayNames : Date.dayNames, /** * @cfg {Number} startDay * Day index at which the week should begin, 0-based */ startDay: Ext.DatePicker.prototype.startDay, /** * @cfg {Boolean} denyDragOnMissingEditGrant * deny drag action if edit grant for event is missing */ denyDragOnMissingEditGrant: true, /** * @private {Date} toDay */ toDay: null, /** * @private {Array} dateMesh */ dateMesh: null, /** * @private {Tine.Calendar.ParallelEventsRegistry} parallelEventsRegistry */ parallelEventsRegistry: null, cls: "cal-monthview", boxMinWidth: 550, /** * @private */ afterRender: function() { Tine.Calendar.MonthView.superclass.afterRender.apply(this, arguments); this.initElements(); this.getSelectionModel().init(this); this.mon(this.el, 'mousedown', this.onMouseDown, this); this.mon(this.el, 'dblclick', this.onDblClick, this); this.mon(this.el, 'click', this.onClick, this); this.mon(this.el, 'contextmenu', this.onContextMenu, this); this.mon(this.el, 'keydown', this.onKeyDown, this); this.initDragZone(); this.initDropZone(); this.updatePeriod({from: this.period.from}); // NOTE: store.getCount == 0 is no indicator for an unloaded store // as the view just might be empty. If an unloded store is // a problem here we have to find a smart solution this.onLoad.apply(this); this.rendered = true; }, /** * @private calculates mesh of dates for month this.startDate is in */ calcDateMesh: function() { var mesh = []; var d = Date.parseDate(this.startDate.format('Y-m') + '-01 00:00:00', Date.patterns.ISO8601Long); while(d.getDay() != this.startDay) { d = d.add(Date.DAY, -1); } while(d.getMonth() != this.startDate.add(Date.MONTH, 1).getMonth()) { for (var i=0; i<7; i++) { mesh.push(d.add(Date.DAY, i).clone()); } d = d.add(Date.DAY, 7); } this.dateMesh = mesh; }, /** * returns index of dateCell given date is in * @param {Date} date */ getDayCellIndex: function(date) { return Math.round((date.clearTime(true).getTime() - this.dateMesh[0].getTime())/Date.msDAY); }, /** * @private returns a child div in requested position * * @param {dom} dayCell * @param {Number} pos * @return {dom} */ getEventSlice: function(dayCell, pos) { pos = Math.abs(pos); for (var i=dayCell.childNodes.length; i<=pos; i++) { Ext.DomHelper.insertAfter(dayCell.lastChild, '
'); } // make sure cell is empty while (dayCell.childNodes[pos].innerHTML) { pos++; if (pos > dayCell.childNodes.length -1) { Ext.DomHelper.insertAfter(dayCell.lastChild, '
'); } } return dayCell.childNodes[pos]; }, /** * returns period of currently displayed month * @return {Object} */ getPeriod: function() { // happens if month view is rendered first if (! this.dateMesh) { this.calcDateMesh(); } return { from: this.dateMesh[0], until: this.dateMesh[this.dateMesh.length -1].add(Date.DAY, 1) }; }, getSelectionModel: function() { return this.selModel; }, getTargetDateTime: function(e) { var target = e.getTarget('td.cal-monthview-daycell', 3); if (target) { var dateIdx = this.dayCells.indexOf(target), date = this.dateMesh[this.dayCells.indexOf(target)]; // date has no time part date.date_only = true; return date; } }, getTargetEvent: function(e) { var target = e.getTarget('div.cal-monthview-alldayevent', 10) || e.getTarget('div.cal-monthview-event', 10); if (target) { var parts = target.id.split(':'); var event = this.store.getById(parts[1]); } return event; }, /** * init month view */ initComponent: function() { this.app = Tine.Tinebase.appMgr.get('Calendar'); this.newEventSummary = this.app.i18n._hidden(this.newEventSummary); this.calWeekString = this.app.i18n._hidden(this.calWeekString); this.moreString = this.app.i18n._hidden(this.moreString); // redefine this props in case ext translations got included after this component this.monthNames = Date.monthNames; this.dayNames = Date.dayNames; this.startDay = Ext.DatePicker.prototype.startDay; this.initData(this.store); this.initTemplates(); if (! this.selModel) { this.selModel = this.selModel || new Tine.Calendar.EventSelectionModel(); } Tine.Calendar.MonthView.superclass.initComponent.apply(this, arguments); }, /** * @private * @param {Ext.data.Store} ds */ initData : function(ds){ if(this.store){ this.store.un("load", this.onLoad, this); this.store.un("beforeload", this.onBeforeLoad, this); this.store.un("add", this.onAdd, this); this.store.un("remove", this.onRemove, this); this.store.un("update", this.onUpdate, this); } if(ds){ ds.on("load", this.onLoad, this); ds.on("beforeload", this.onBeforeLoad, this); ds.on("add", this.onAdd, this); ds.on("remove", this.onRemove, this); ds.on("update", this.onUpdate, this); } this.store = ds; }, /** * @private */ initDragZone: function() { this.dragZone = new Ext.dd.DragZone(this.el, { ddGroup: 'cal-event', view: this, scroll: false, getDragData: function(e) { var eventEl = e.getTarget('div.cal-monthview-alldayevent', 10) || e.getTarget('div.cal-monthview-event', 10); if (eventEl) { var parts = eventEl.id.split(':'); var event = this.view.store.getById(parts[1]); // don't allow dragging with missing edit grant if (this.view.denyDragOnMissingEditGrant && ! event.get('editGrant')) { return false; } // we need to clone an event with summary in var d = Ext.get(event.ui.domIds[0]).dom.cloneNode(true); var width = Ext.fly(eventEl).getWidth() * event.ui.domIds.length; Ext.fly(d).removeClass(['cal-monthview-alldayevent-cropleft', 'cal-monthview-alldayevent-cropright']); Ext.fly(d).setWidth(width); Ext.fly(d).setOpacity(0.5); d.id = Ext.id(); return { scope: this.view, sourceEl: eventEl, event: event, ddel: d, selections: this.view.getSelectionModel().getSelectedEvents() } } }, getRepairXY: function(e, dd) { Ext.fly(this.dragData.sourceEl).setOpacity(1, 1); return Ext.fly(this.dragData.sourceEl).getXY(); } }); }, initDropZone: function() { this.dd = new Ext.dd.DropZone(this.el.dom, { ddGroup: 'cal-event', notifyOver : function(dd, e, data) { var target = e.getTarget('td.cal-monthview-daycell', 3); var event = data.event; // we dont support multiple dropping yet if (event) { data.scope.getSelectionModel().select(event); } return target && event && event.get('editGrant') ? 'cal-daysviewpanel-event-drop-ok' : 'cal-daysviewpanel-event-drop-nodrop'; }, notifyDrop : function(dd, e, data) { var v = data.scope; var target = e.getTarget('td.cal-monthview-daycell', 3); var targetDate = v.dateMesh[v.dayCells.indexOf(target)]; if (targetDate) { var event = data.event; var diff = (targetDate.getTime() - event.get('dtstart').clearTime(true).getTime()) / Date.msDAY; if (! diff || ! event.get('editGrant')) { return false; } event.beginEdit(); event.set('dtstart', event.get('dtstart').add(Date.DAY, diff)); event.set('dtend', event.get('dtend').add(Date.DAY, diff)); event.endEdit(); v.fireEvent('updateEvent', event); } return !!targetDate; } }); }, /** * @private */ initElements: function() { var E = Ext.Element; this.focusEl = new E(this.el.dom.firstChild); this.mainHd = new E(this.el.dom.lastChild.firstChild); this.mainBody = new E(this.el.dom.lastChild.lastChild); this.dayCells = Ext.DomQuery.select('td[class=cal-monthview-daycell]', this.mainBody.dom); }, /** * inits all tempaltes of this view */ initTemplates: function() { var ts = this.templates || {}; ts.allDayEvent = new Ext.XTemplate( '
' + '
' + '
{[Ext.util.Format.htmlEncode(values.summary)]}
' + '
' + '
' ); ts.event = new Ext.XTemplate( '
' + '
' + '
{startTime} {[Ext.util.Format.htmlEncode(values.summary)]}
' + '
' + '
' ); for(var k in ts){ var t = ts[k]; if(t && typeof t.compile == 'function' && !t.compiled){ t.disableFormats = true; t.compile(); } } this.templates = ts; }, /** * @private * @param {Tine.Calendar.Model.Event} event */ insertEvent: function(event) { event.ui = new Tine.Calendar.MonthViewEventUI(event); //event.ui.render(this); var dtStart = event.get('dtstart'); var startCellNumber = this.getDayCellIndex(dtStart); var dtEnd = event.get('dtend'); // 00:00 in users timezone is a spechial case where the user expects // something like 24:00 and not 00:00 if (dtEnd.format('H:i') == '00:00') { dtEnd = dtEnd.add(Date.MINUTE, -1); } var endCellNumber = this.getDayCellIndex(dtEnd); // skip out of range events if (endCellNumber < 0 || startCellNumber >= this.dateMesh.length) { return; } var pos = this.parallelEventsRegistry.getPosition(event), attendeeRecord = this.ownerCt && this.ownerCt.attendee ? Tine.Calendar.Model.Attender.getAttendeeStore.getAttenderRecord(event.getAttendeeStore(), this.ownerCt.attendee) : event.getMyAttenderRecord(); // save some layout info event.ui.is_all_day_event = event.get('is_all_day_event') || startCellNumber != endCellNumber; event.ui.colorSet = event.colorSet = Tine.Calendar.colorMgr.getColor(event, attendeeRecord); event.ui.color = event.ui.colorSet.color; event.ui.bgColor = event.ui.colorSet.light; var data = { startTime: dtStart.format('H:i'), summary: event.getTitle(), color: event.ui.color, bgColor: event.ui.bgColor, width: '100%' }; for (var i=Math.max(startCellNumber, 0); i<=Math.min(endCellNumber, this.dayCells.length-1) ; i++) { var col = i%7, row = Math.floor(i/7); data.id = Ext.id() + '-event:' + event.get('id'); event.ui.domIds.push(data.id); var tmpl = this.templates.event; data.extraCls = event.get('editGrant') ? 'cal-monthview-event-editgrant' : ''; data.extraCls += ' cal-status-' + event.get('status'); if (event.ui.is_all_day_event) { tmpl = this.templates.allDayEvent; data.color = 'black'; if (i > startCellNumber) { data.extraCls += ' cal-monthview-alldayevent-cropleft'; } if (i < endCellNumber) { data.extraCls += ' cal-monthview-alldayevent-cropright'; } // show icon on startCell and leftCells data.showInfo = i == startCellNumber || i%7 == 0; // adopt summary width NOTE: we need width in row if (data.showInfo && startCellNumber != endCellNumber) { var cols = (row == Math.floor(endCellNumber/7) ? endCellNumber%7 : 6) - col +1; data.width = 100 * cols + '%' } } if (event.hasPoll()) { data.extraCls += ' cal-poll-event'; } var posEl = this.getEventSlice(this.dayCells[i].lastChild, pos); var eventEl = tmpl.overwrite(posEl, data, true); if (event.dirty) { eventEl.setOpacity(0.5); // the event was selected before event.ui.onSelectedChange(true); } } }, onLayout: function() { Tine.Calendar.MonthView.superclass.onLayout.apply(this, arguments); if(!this.mainBody){ return; // not rendered } var csize = this.container.getSize(true); var vw = csize.width; //this.el.setSize(csize.width, csize.height); var hsize = this.mainHd.getSize(true); var hdCels = this.mainHd.dom.firstChild.childNodes; Ext.fly(hdCels[0]).setWidth(50); for (var i=1; i 1) { this.layoutDayCell(this.dayCells[i], true, true); } } }, /** * layouts a single day cell * * @param {dom} cell * @param {Bool} hideOverflow * @param {Bool} updateHeader */ layoutDayCell: function(cell, hideOverflow, updateHeader) { // clean empty slices while (cell.lastChild.childNodes.length > 1 && cell.lastChild.lastChild.innerHTML == '') { Ext.fly(cell.lastChild.lastChild).remove(); } Tine.log.debug('Tine.Calendar.MonthView::layoutDayCell() - cell:'); Tine.log.debug(cell); for (var j=0, height=0, hideCount=0; j this.dayCellsHeight && hideOverflow ? 'hide' : 'show'](); if (height > this.dayCellsHeight && hideOverflow && cell.lastChild.childNodes[j].innerHTML !== '') { hideCount++; } } Tine.log.debug('Tine.Calendar.MonthView::layoutDayCell() - hideCount:' + hideCount); if (updateHeader) { cell.firstChild.firstChild.innerHTML = hideCount > 0 ? String.format(this.moreString, hideCount) : ''; } return height; }, /** * @private */ onAdd : function(ds, records, index){ for (var i=0; i', '', "" ]; for(var i = 0; i < 7; i++){ var d = this.startDay+i; if(d > 6){ d = d-7; } m.push(""); } m[m.length] = ""; for(var i = 0; i < 42; i++) { if(i % 7 == 0 && i != 0){ m[m.length] = ""; } m[m.length] = ''; } m.push('
", this.calWeekString, "", this.dayNames[d], "
' + '
' + '
' + '
' + '
' + '
' + '
'); this.el.update(m.join("")); }, updatePeriod: function(period) { this.toDay = new Date().clearTime(); this.startDate = period.from; this.calcDateMesh(); var tbar = this.findParentBy(function(c) {return c.getTopToolbar()}).getTopToolbar(); if (tbar) { tbar.periodPicker.update(this.startDate); this.startDate = tbar.periodPicker.getPeriod().from; } if (! this.rendered) return; // update dates and bg colors var dayHeaders = Ext.DomQuery.select('div[class=cal-monthview-dayheader-date]', this.mainBody.dom); for(var i = 0; i < this.dateMesh.length; i++) { clsToAdd = ((this.dateMesh[i].getMonth() == this.startDate.getMonth()) ? ' cal-monthview-daycell-valid' : ' cal-monthview-daycell-invalid'); if (this.dateMesh[i].getTime() == this.toDay.getTime()) { clsToAdd = ' cal-monthview-daycell-today'; } this.dayCells[i].setAttribute('class', 'cal-monthview-daycell ' + clsToAdd); dayHeaders[i].innerHTML = this.dateMesh[i].format('j'); } // update weeks var wkCells = Ext.DomQuery.select('td[class=cal-monthview-wkcell]', this.mainBody.dom); for(var i = 0; i < wkCells.length; i++) { if (this.dateMesh.length > i * 7 + 1) { // NOTE: '+1' is to ensure we display the ISO8601 based week where weeks always start on monday! wkCells[i].innerHTML = this.dateMesh[i * 7 + 1].getWeekOfYear(); //Ext.fly(wkCells[i]).unselectable(); // this supresses events ;-( } } this.onLayout(); this.fireEvent('changePeriod', period); }, unZoom: function() { if (this.zoomCell) { // this prevents reopen of cell on header clicks this.lastClickTime = new Date().getTime(); var cell = Ext.get(this.zoomCell); var dayBodyEl = cell.last(); var height = cell.getHeight() - cell.first().getHeight(); dayBodyEl.scrollTo('top'); dayBodyEl.removeClass('cal-monthview-daypreviewbox'); dayBodyEl.setStyle('background-color', cell.getStyle('background-color')); dayBodyEl.setStyle('border-top', 'none'); dayBodyEl.setHeight(height); // NOTE: we need both setWidht statements, otherwise safari keeps scroller space for (var i=0; i