define([ "dojo/_base/declare", "dojo/_base/array", "dojo/_base/event", "dojo/_base/lang", "dojo/_base/sniff", "dojo/_base/fx", "dojo/_base/html", "dojo/on", "dojo/dom", "dojo/dom-class", "dojo/dom-style", "dojo/dom-geometry", "dojo/dom-construct", "dojo/query", "dojox/html/metrics", "dojo/i18n", "./ViewBase", "dojo/text!./templates/MatrixView.html", "dijit/_TemplatedMixin"], function( declare, arr, event, lang, has, fx, html, on, dom, domClass, domStyle, domGeometry, domConstruct, query, metrics, i18n, ViewBase, template, _TemplatedMixin){ /*===== var __HeaderClickEventArgs = { // summary: // A column click event. // index: Integer // The column index. // date: Date // The date displayed by the column. // triggerEvent: Event // The origin event. }; =====*/ /*===== var __ExpandRendererClickEventArgs = { // summary: // A expand renderer click event. // columnIndex: Integer // The column index of the cell. // rowIndex: Integer // The row index of the cell. // date: Date // The date displayed by the cell. // triggerEvent: Event // The origin event. }; =====*/ return declare("dojox.calendar.MatrixView", [ViewBase, _TemplatedMixin], { // summary: // The matrix view is a calendar view that displaying a matrix where each cell is a day. templateString: template, baseClass: "dojoxCalendarMatrixView", _setTabIndexAttr: "domNode", // viewKind: String // Type of the view. Used by the calendar widget to determine how to configure the view. // This view kind is "matrix". viewKind: "matrix", // renderData: Object // The render data object contains all the data needed to render the widget. renderData: null, // startDate: Date // The start date of the time interval displayed. // If not set at initialization time, will be set to current day. startDate: null, // refStartTime: Date? // (Optional) Start of the time interval of interest. // It is used to style differently the displayed rows out of the // time interval of interest. refStartTime: null, // refStartTime: Date? // (Optional) End of the time interval of interest. // It is used to style differently the displayed rows out of the // time interval of interest. refEndTime: null, // columnCount: Integer // The number of column to display (from the startDate). columnCount: 7, // rowCount: Integer // The number of rows to display (from the startDate). rowCount: 5, // horizontalRenderer: Class // The class use to create horizontal renderers. horizontalRenderer: null, // labelRenderer: Class // The class use to create label renderers. labelRenderer: null, // expandRenderer: Class // The class use to create drill down renderers. expandRenderer: null, // percentOverlap: Integer // The percentage of the renderer width used to superimpose one item renderers on another // when two events are overlapping. By default 0. percentOverlap: 0, // verticalGap: Integer // The number of pixels between two item renderers that are overlapping each other if the percentOverlap property is 0. verticalGap: 2, // horizontalRendererHeight: Integer // The height in pixels of the horizontal and label renderers that is applied by the layout. horizontalRendererHeight: 17, // horizontalRendererHeight: Integer // The height in pixels of the horizontal and label renderers that is applied by the layout. labelRendererHeight: 14, // expandRendererHeight: Integer // The height in pixels of the expand/collapse renderers that is applied by the layout. expandRendererHeight: 15, // cellPaddingTop: Integer // The top offset in pixels of each cell applied by the layout. cellPaddingTop: 16, // expandDuration: Integer // Duration of the animation when expanding or collapsing a row. expandDuration: 300, // expandEasing: Function // Easing function of the animation when expanding or collapsing a row (null by default). expandEasing: null, // layoutDuringResize: Boolean // Indicates if the item renderers' position and size is updated or if they are hidden during a resize of the widget. layoutDuringResize: false, // roundToDay: Boolean // For horizontal renderers that are not filling entire days, whether fill the day or not. roundToDay: true, // showCellLabel: Boolean // Whether display or not the grid cells label (usually the day of month). showCellLabel: true, // scrollable: [private] Boolean scrollable: false, // resizeCursor: [private] Boolean resizeCursor: "e-resize", constructor: function(){ this.invalidatingProperties = ["columnCount", "rowCount", "startDate", "horizontalRenderer", "labelRenderer", "expandRenderer", "rowHeaderDatePattern", "columnHeaderLabelLength", "cellHeaderShortPattern", "cellHeaderLongPattern", "percentOverlap", "verticalGap", "horizontalRendererHeight", "labelRendererHeight", "expandRendererHeight", "cellPaddingTop", "roundToDay", "itemToRendererKindFunc", "layoutPriorityFunction", "formatItemTimeFunc", "textDir", "items"]; this._ddRendererList = []; this._ddRendererPool = []; this._rowHeaderHandles = []; }, destroy: function(preserveDom){ this._cleanupRowHeader(); this.inherited(arguments); }, postCreate: function(){ this.inherited(arguments); this._initialized = true; if(!this.invalidRendering){ this.refreshRendering(); } }, _createRenderData: function(){ var rd = {}; rd.dateLocaleModule = this.dateLocaleModule; rd.dateClassObj = this.dateClassObj; rd.dateModule = this.dateModule; // arithmetics on Dates rd.dates = []; rd.columnCount = this.get("columnCount"); rd.rowCount = this.get("rowCount"); rd.sheetHeight = this.itemContainer.offsetHeight; this._computeRowsHeight(rd); var d = this.get("startDate"); if(d == null){ d = new rd.dateClassObj(); } d = this.floorToDay(d, false, rd); this.startDate = d; for(var row = 0; row < rd.rowCount ; row++){ rd.dates.push([]); for(var col = 0; col < rd.columnCount ; col++){ rd.dates[row].push(d); d = rd.dateModule.add(d, "day", 1); d = this.floorToDay(d, false, rd); } } rd.startTime = this.newDate(rd.dates[0][0], rd); rd.endTime = this.newDate(rd.dates[rd.rowCount-1][rd.columnCount-1], rd); rd.endTime = rd.dateModule.add(rd.endTime, "day", 1); rd.endTime = this.floorToDay(rd.endTime, true); if(this.displayedItemsInvalidated && !this._isEditing){ this.displayedItemsInvalidated = false; this._computeVisibleItems(rd); }else if(this.renderData){ rd.items = this.renderData.items; } rd.rtl = !this.isLeftToRight(); return rd; }, _validateProperties: function(){ this.inherited(arguments); if(this.columnCount<1 || isNaN(this.columnCount)){ this.columnCount = 1; } if(this.rowCount<1 || isNaN(this.rowCount)){ this.rowCount = 1; } if(isNaN(this.percentOverlap) || this.percentOverlap < 0 || this.percentOverlap > 100){ this.percentOverlap = 0; } if(isNaN(this.verticalGap) || this.verticalGap < 0){ this.verticalGap = 2; } if(isNaN(this.horizontalRendererHeight) || this.horizontalRendererHeight < 1){ this.horizontalRendererHeight = 17; } if(isNaN(this.labelRendererHeight) || this.labelRendererHeight < 1){ this.labelRendererHeight = 14; } if(isNaN(this.expandRendererHeight) || this.expandRendererHeight < 1){ this.expandRendererHeight = 15; } }, _setStartDateAttr: function(value){ this.displayedItemsInvalidated = true; this._set("startDate", value); }, _setColumnCountAttr: function(value){ this.displayedItemsInvalidated = true; this._set("columnCount", value); }, _setRowCountAttr: function(value){ this.displayedItemsInvalidated = true; this._set("rowCount", value); }, __fixEvt:function(e){ e.sheet = "primary"; e.source = this; return e; }, ////////////////////////////////////////// // // Formatting functions // ////////////////////////////////////////// _formatRowHeaderLabel: function(/*Date*/d){ // summary: // Computes the row header label for the specified time of day. // By default the getWeekNumberLabel() function is called. // The rowHeaderDatePattern property can be used to set a // custom date pattern to the formatter. // d: Date // The date to format // tags: // protected if(this.rowHeaderDatePattern){ return this.renderData.dateLocaleModule.format(d, { selector: 'date', datePattern: this.rowHeaderDatePattern }); }else{ return this.getWeekNumberLabel(d); } }, _formatColumnHeaderLabel: function(d){ // summary: // Computes the column header label for the specified date. // By default a formatter is used, optionally the columnHeaderLabelLength // property can be used to specify the length of the string. // d: Date // The date to format // tags: // protected return this.renderData.dateLocaleModule.getNames('days', this.columnHeaderLabelLength ? this.columnHeaderLabelLength : 'wide', 'standAlone')[d.getDay()]; }, _formatGridCellLabel: function(d, row, col){ // summary: // Computes the column header label for the specified date. // By default a formatter is used, optionally the cellHeaderLongPattern and cellHeaderShortPattern // properties can be used to set a custom date pattern to the formatter. // d: Date // The date to format. // row: Integer // The row that displays the current date. // col: Integer // The column that displays the current date. // tags: // protected var isFirstDayOfMonth = row == 0 && col == 0 || d.getDate() == 1; var format, rb; if(isFirstDayOfMonth){ if(this.cellHeaderLongPattern){ format = this.cellHeaderLongPattern; }else{ rb = i18n.getLocalization("dojo.cldr", this._calendar); format = rb["dateFormatItem-MMMd"]; } }else{ if(this.cellHeaderShortPattern){ format = this.cellHeaderShortPattern; }else{ rb = i18n.getLocalization("dojo.cldr", this._calendar); format = rb["dateFormatItem-d"]; } } return this.renderData.dateLocaleModule.format(d, { selector: 'date', datePattern: format }); }, //////////////////////////////////////////// // // HTML structure management // /////////////////////////////////////////// refreshRendering: function(){ this.inherited(arguments); if(!this.domNode){ return; } this._validateProperties(); var oldRd = this.renderData; this.renderData = this._createRenderData(); this._createRendering(this.renderData, oldRd); this._layoutRenderers(this.renderData); }, _createRendering: function(renderData, oldRenderData){ // summary: // Creates the HTML structure (grid, place holders, headers, etc) // renderData: Object // The new render data // oldRenderData: Object // The previous render data // tags: // private if(renderData.rowHeight <= 0){ renderData.columnCount = 1; renderData.rowCount = 1; renderData.invalidRowHeight = true; return; } if(oldRenderData){ // make sure to have correct rowCount if(this.itemContainerTable){ var rows = query(".dojoxCalendarItemContainerRow", this.itemContainerTable); oldRenderData.rowCount = rows.length; } } this._buildColumnHeader(renderData, oldRenderData); this._buildRowHeader(renderData, oldRenderData); this._buildGrid(renderData, oldRenderData); this._buildItemContainer(renderData, oldRenderData); if(this.buttonContainer && this.owner != null && this.owner.currentView == this){ domStyle.set(this.buttonContainer, {"right":0, "left":0}); } }, _buildColumnHeader: function(/*Object*/ renderData, /*Object*/oldRenderData){ // summary: // Creates incrementally the HTML structure of the column header and configures its content. // // renderData: // The render data to display. // // oldRenderData: // The previously render data displayed, if any. // tags: // private var table = this.columnHeaderTable; if(!table){ return; } var count = renderData.columnCount - (oldRenderData ? oldRenderData.columnCount : 0); if(has("ie") == 8){ // workaround Internet Explorer 8 bug. // if on the table, width: 100% and table-layout: fixed are set // and columns are removed, width of remaining columns is not // recomputed: must rebuild all. if(this._colTableSave == null){ this._colTableSave = lang.clone(table); }else if(count < 0){ this.columnHeader.removeChild(table); domConstruct.destroy(table); table = lang.clone(this._colTableSave); this.columnHeaderTable = table; this.columnHeader.appendChild(table); count = renderData.columnCount; } } // else incremental dom add/remove for real browsers. var tbodies = query("tbody", table); var trs = query("tr", table); var tbody, tr, td; if(tbodies.length == 1){ tbody = tbodies[0]; }else{ tbody = html.create("tbody", null, table); } if(trs.length == 1){ tr = trs[0]; }else{ tr = domConstruct.create("tr", null, tbody); } // Build HTML structure (incremental) if(count > 0){ // creation for(var i=0; i < count; i++){ td = domConstruct.create("td", null, tr); } }else{ // deletion count = -count; for(var i=0; i < count; i++){ tr.removeChild(tr.lastChild); } } // fill & configure query("td", table).forEach(function(td, i){ td.className = ""; var d = renderData.dates[0][i]; this._setText(td, this._formatColumnHeaderLabel(d)); if(i == 0){ domClass.add(td, "first-child"); }else if(i == this.renderData.columnCount-1){ domClass.add(td, "last-child"); } this.styleColumnHeaderCell(td, d, renderData); }, this); if(this.yearColumnHeaderContent){ var d = renderData.dates[0][0]; this._setText(this.yearColumnHeaderContent, renderData.dateLocaleModule.format(d, {selector: "date", datePattern:"yyyy"})); } }, styleColumnHeaderCell: function(node, date, renderData){ // summary: // Styles the CSS classes to the node that displays a column header cell. // By default this method is setting the "dojoxCalendarWeekend" if the day of week represents a weekend. // node: Node // The DOM node that displays the column in the grid. // date: Date // The date displayed by this column // renderData: Object // The render data. // tags: // protected domClass.add(node, this._cssDays[date.getDay()]); if(this.isWeekEnd(date)){ domClass.add(node, "dojoxCalendarWeekend"); } }, _rowHeaderHandles: null, _cleanupRowHeader: function(){ // tags: // private while(this._rowHeaderHandles.length > 0){ var list = this._rowHeaderHandles.pop(); while(list.length>0){ list.pop().remove(); } } }, _rowHeaderClick: function(e){ // tags: // private var index = query("td", this.rowHeaderTable).indexOf(e.currentTarget); this._onRowHeaderClick({ index: index, date: this.renderData.dates[index][0], triggerEvent: e }); }, _buildRowHeader: function(renderData, oldRenderData){ // summary: // Creates incrementally the HTML structure of the row header and configures its content. // // renderData: // The render data to display. // // oldRenderData: // The previously render data displayed, if any. // tags: // private var rowHeaderTable = this.rowHeaderTable; if(!rowHeaderTable){ return; } var tbodies = query("tbody", rowHeaderTable); var tbody, tr, td; if(tbodies.length == 1){ tbody = tbodies[0]; }else{ tbody = domConstruct.create("tbody", null, rowHeaderTable); } var count = renderData.rowCount - (oldRenderData ? oldRenderData.rowCount : 0); // Build HTML structure if(count>0){ // creation for(var i=0; i < count; i++){ tr = domConstruct.create("tr", null, tbody); td = domConstruct.create("td", null, tr); var h = []; h.push(on(td, "click", lang.hitch(this, this._rowHeaderClick))); if(!has("touch")){ h.push(on(td, "mousedown", function(e){ domClass.add(e.currentTarget, "Active"); })); h.push(on(td, "mouseup", function(e){ domClass.remove(e.currentTarget, "Active"); })); h.push(on(td, "mouseover", function(e){ domClass.add(e.currentTarget, "Hover"); })); h.push(on(td, "mouseout", function(e){ domClass.remove(e.currentTarget, "Hover"); })); } this._rowHeaderHandles.push(h); } }else{ count = -count; // deletion of existing nodes for(var i=0; i < count; i++){ tbody.removeChild(tbody.lastChild); var list = this._rowHeaderHandles.pop(); while(list.length>0){ list.pop().remove(); } } } // fill labels query("tr", rowHeaderTable).forEach(function(tr, i){ domStyle.set(tr, "height", this._getRowHeight(i) + "px"); var d = renderData.dates[i][0]; var td = query("td", tr)[0]; td.className = ""; if(i == 0){ domClass.add(td, "first-child"); } if(i == this.renderData.rowCount-1){ domClass.add(td, "last-child"); } this.styleRowHeaderCell(td, d, renderData); this._setText(td, this._formatRowHeaderLabel(d)); }, this); }, styleRowHeaderCell: function(node, date, renderData){ // summary: // Styles the CSS classes to the node that displays a row header cell. // By default this method is doing nothing. // node: Node // The DOM node that displays the column in the grid. // date: Date // The date in the week. // renderData: Object // The render data. // tags: // protected }, _buildGrid: function (renderData, oldRenderData){ // summary: // Creates incrementally the HTML structure of the grid and configures its content. // // renderData: // The render data to display. // // oldRenderData: // The previously render data displayed, if any. // tags: // private var table = this.gridTable; if(!table){ return; } var currentTR = query("tr", table); var rowDiff = renderData.rowCount - currentTR.length; var addRows = rowDiff > 0; var colDiff = renderData.columnCount - (oldRenderData ? oldRenderData.columnCount : 0); if(has("ie") == 8){ // workaround Internet Explorer 8 bug. // if on the table, width: 100% and table-layout: fixed are set // and columns are removed, width of remaining columns is not // recomputed: must rebuild all. if(this._gridTableSave == null){ this._gridTableSave = lang.clone(table); }else if(colDiff < 0){ this.grid.removeChild(table); domConstruct.destroy(table); table = lang.clone(this._gridTableSave); this.gridTable = table; this.grid.appendChild(table); colDiff = renderData.columnCount; rowDiff = renderData.rowCount; addRows = true; } } var tbodies = query("tbody", table); var tbody; if(tbodies.length == 1){ tbody = tbodies[0]; }else{ tbody = domConstruct.create("tbody", null, table); } // Build rows HTML structure (incremental) if(addRows){ // creation for(var i=0; i0; colDiff = addCols ? colDiff : -colDiff; query("tr", table).forEach(function(tr, i){ if(addCols){ // creation var len = i >= rowIndex ? renderData.columnCount : colDiff; for(var i=0; i= 0 || cal.compare(cal.add(date, "day", 1), this.refStartTime) <= 0)){ domClass.add(node, "dojoxCalendarDayDisabled"); }else if(this.isWeekEnd(date)){ domClass.add(node, "dojoxCalendarWeekend"); } }, styleGridCell: function(node, date, renderData){ // summary: // Styles the CSS classes to the node that displays a cell. // Delegates to styleGridCellFunc if defined or defaultStyleGridCell otherwise. // node: Node // The DOM node that displays the cell in the grid. // date: Date // The date displayed by this cell. // renderData: Object // The render data. // tags: // protected if(this.styleGridCellFunc){ this.styleGridCellFunc(node, date, renderData); }else{ this.defaultStyleGridCell(node, date, renderData); } }, _buildItemContainer: function(renderData, oldRenderData){ // summary: // Creates the HTML structure of the item container and configures its content. // // renderData: // The render data to display. // // oldRenderData: // The previously render data displayed, if any. // tags: // private var table = this.itemContainerTable; if(!table){ return; } var rows = []; var count = renderData.rowCount - (oldRenderData ? oldRenderData.rowCount : 0) if(has("ie") == 8){ // workaround Internet Explorer 8 bug. // if on the table, width: 100% and table-layout: fixed are set // and columns are removed, width of remaining columns is not // recomputed: must rebuild all. if(this._itemTableSave == null){ this._itemTableSave = lang.clone(table); }else if(count < 0){ this.itemContainer.removeChild(table); this._recycleItemRenderers(true); this._recycleExpandRenderers(true); domConstruct.destroy(table); table = lang.clone(this._itemTableSave); this.itemContainerTable = table; this.itemContainer.appendChild(table); count = renderData.columnCount; } } // else incremental dom add/remove for real browsers. var tbodies = query("tbody", table); var tbody, tr, td, div; if(tbodies.length == 1){ tbody = tbodies[0]; }else{ tbody = domConstruct.create("tbody", null, table); } // Build HTML structure (incremental) if(count>0){ // creation for(var i=0; i < count; i++){ tr = domConstruct.create("tr", null, tbody); domClass.add(tr, "dojoxCalendarItemContainerRow"); td = domConstruct.create("td", null, tr); div = domConstruct.create("div", null, td); domClass.add(div, "dojoxCalendarContainerRow"); } }else{ // deletion count = -count; for(var i=0; i < count; i++){ tbody.removeChild(tbody.lastChild); } } query(".dojoxCalendarItemContainerRow", table).forEach(function(tr, i){ domStyle.set(tr, "height", this._getRowHeight(i) + "px"); rows.push(tr.childNodes[0].childNodes[0]); }, this); renderData.cells = rows; }, resize: function(changeSize){ this.inherited(arguments); this._resizeHandler(null, false); }, _resizeHandler: function(e, apply){ // summary: // Refreshes and apply the row height according to the widget height. // e: Event // The resize event (optional) // apply: Boolean // Whether take into account the layoutDuringResize flag to relayout item while resizing or not. // tags: // private var rd = this.renderData; if(rd == null){ this.refreshRendering(); return; } if(rd.sheetHeight != this.itemContainer.offsetHeight){ // refresh values rd.sheetHeight = this.itemContainer.offsetHeight; var expRow = this.getExpandedRowIndex(); if(expRow == -1){ this._computeRowsHeight(); this._resizeRows(); }else{ this.expandRow(rd.expandedRow, rd.expandedRowCol, 0, null, true); } if(rd.invalidRowHeight){ // complete recompute delete rd.invalidRowHeight; this.renderData = null; this.displayedItemsInvalidated = true; this.refreshRendering(); return; } } if(this.layoutDuringResize || apply){ // Use a time for FF (at least). In FF the cell size and position info are not ready yet. setTimeout(lang.hitch(this, function(){ this._layoutRenderers(this.renderData); }), 20); }else{ domStyle.set(this.itemContainer, "opacity", 0); this._recycleItemRenderers(); this._recycleExpandRenderers(); if(this._resizeTimer != undefined){ clearTimeout(this._resizeTimer); } this._resizeTimer = setTimeout(lang.hitch(this, function(){ delete this._resizeTimer; this._resizeRowsImpl(this.itemContainer, "tr"); this._layoutRenderers(this.renderData); if(this.resizeAnimationDuration == 0){ domStyle.set(this.itemContainer, "opacity", 1); }else{ fx.fadeIn({node:this.itemContainer, curve:[0, 1]}).play(this.resizeAnimationDuration); } }), 200); } }, // resizeAnimationDuration: Integer // Duration, in milliseconds, of the fade animation showing the item renderers after a widget resize. resizeAnimationDuration: 0, ///////////////////////////////////////////// // // Row height management // ////////////////////////////////////////////// getExpandedRowIndex: function(){ // summary: // Returns the index of the expanded row or -1 if there's no row expanded. return this.renderData.expandedRow == null ? -1 : this.renderData.expandedRow; }, collapseRow: function(duration, easing, apply){ // summary: // Collapses the expanded row, if any. // duration: Integer // Duration in milliseconds of the optional animation. // easing: Function // Easing function of the optional animation. var rd = this.renderData; if(apply == undefined){ apply = true; } if(duration == undefined){ duration = this.expandDuration; } if(rd && rd.expandedRow != null && rd.expandedRow != -1){ if(apply && duration){ var index = rd.expandedRow; var oldSize = rd.expandedRowHeight; delete rd.expandedRow; this._computeRowsHeight(rd); var size = this._getRowHeight(index); rd.expandedRow = index; this._recycleExpandRenderers(); this._recycleItemRenderers(); domStyle.set(this.itemContainer, "display", "none"); this._expandAnimation = new fx.Animation({ curve: [oldSize, size], duration: duration, easing: easing, onAnimate: lang.hitch(this, function(size) { this._expandRowImpl(Math.floor(size)); }), onEnd: lang.hitch(this, function(size) { this._expandAnimation = null; this._collapseRowImpl(false); this._resizeRows(); domStyle.set(this.itemContainer, "display", "block"); setTimeout(lang.hitch(this, function(){ this._layoutRenderers(rd); }), 100); this.onExpandAnimationEnd(false); }) }); this._expandAnimation.play(); }else{ this._collapseRowImpl(apply); } } }, _collapseRowImpl: function(apply){ // tags: // private var rd = this.renderData; delete rd.expandedRow; delete rd.expandedRowHeight; this._computeRowsHeight(rd); if(apply == undefined || apply){ this._resizeRows(); this._layoutRenderers(rd); } }, expandRow: function(rowIndex, colIndex, duration, easing, apply){ // summary: // Expands the specified row. // rowIndex: Integer // The index of the row to expand. // colIndex: Integer? // The column index of the expand renderer that triggers the action, optional. // duration: Integer? // Duration in milliseconds of the optional animation. // easing: Function? // Easing function of the optional animation. var rd = this.renderData; if(!rd || rowIndex < 0 || rowIndex >= rd.rowCount){ return -1; } if(colIndex == undefined || colIndex < 0 || colIndex >= rd.columnCount){ colIndex = -1; // ignore invalid values } if(apply == undefined){ apply = true; } if(duration == undefined){ duration = this.expandDuration; } if(easing == undefined){ easing = this.expandEasing; } var oldSize = this._getRowHeight(rowIndex); var size = rd.sheetHeight - Math.ceil(this.cellPaddingTop * (rd.rowCount-1)); rd.expandedRow = rowIndex; rd.expandedRowCol = colIndex; rd.expandedRowHeight = size; if(apply){ if(duration){ //debugger; this._recycleExpandRenderers(); this._recycleItemRenderers(); domStyle.set(this.itemContainer, "display", "none"); this._expandAnimation = new fx.Animation({ curve: [oldSize, size], duration: duration, delay:50, easing: easing, onAnimate: lang.hitch(this, function(size) { this._expandRowImpl(Math.floor(size)); }), onEnd: lang.hitch(this, function(){ this._expandAnimation = null; domStyle.set(this.itemContainer, "display", "block"); setTimeout(lang.hitch(this, function(){ this._expandRowImpl(size, true); }), 100); this.onExpandAnimationEnd(true); }) }); this._expandAnimation.play(); }else{ this._expandRowImpl(size, true); } } }, _expandRowImpl: function(size, layout){ // tags: // private var rd = this.renderData; rd.expandedRowHeight = size; this._computeRowsHeight(rd, rd.sheetHeight-size); this._resizeRows(); if(layout){ this._layoutRenderers(rd); } }, onExpandAnimationEnd: function(expand){ // summary: // Event dispatched at the end of an expand or collapse animation. // expand: Boolean // Whether the finished animation was an expand or a collapse animation. // tags: // callback }, _resizeRows: function(){ // summary: // Refreshes the height of the underlying HTML objects. // tags: // private if(this._getRowHeight(0) <= 0){ return; } if(this.rowHeaderTable){ this._resizeRowsImpl(this.rowHeaderTable, "tr"); } if(this.gridTable){ this._resizeRowsImpl(this.gridTable, "tr"); } if(this.itemContainerTable){ this._resizeRowsImpl(this.itemContainerTable, "tr"); } }, _computeRowsHeight:function(renderData, max){ // summary: // 1. Determine if it's better to add or remove pixels // 2. distribute added/removed pixels on first and last rows. // if rows are not too small, it is not noticeable. // tags: // private var rd = renderData == null ? this.renderData : renderData; max = max || rd.sheetHeight; max--; if(has("ie") == 7){ max -= rd.rowCount; } if(rd.rowCount == 1){ rd.rowHeight = max; rd.rowHeightFirst = max; rd.rowHeightLast = max; return; } var count = rd.expandedRow == null ? rd.rowCount : rd.rowCount-1; var rhx = max / count; var rhf, rhl, rh; var diffMin = max - (Math.floor(rhx) * count); var diffMax = Math.abs(max - (Math.ceil(rhx) * count)); var diff; var sign = 1; if(diffMin < diffMax){ rh = Math.floor(rhx); diff = diffMin; }else{ sign = -1; rh = Math.ceil(rhx); diff = diffMax; } rhf = rh + sign * Math.floor(diff/2); rhl = rhf + sign * (diff%2); rd.rowHeight = rh; rd.rowHeightFirst = rhf; rd.rowHeightLast = rhl; }, _getRowHeight: function(index){ // tags: // private var rd = this.renderData; if(index == rd.expandedRow){ return rd.expandedRowHeight; } else if(rd.expandedRow == 0 && index == 1 || index == 0){ return rd.rowHeightFirst; } else if(rd.expandedRow == this.renderData.rowCount-1 && index == this.renderData.rowCount-2 || index == this.renderData.rowCount-1){ return rd.rowHeightLast; }else{ return rd.rowHeight; } }, _resizeRowsImpl: function(tableNode, query){ // tags: // private dojo.query(query, tableNode).forEach(function(tr, i){ domStyle.set(tr, "height", this._getRowHeight(i)+"px"); }, this); }, //////////////////////////////////////////// // // Item renderers // /////////////////////////////////////////// _setHorizontalRendererAttr: function(value){ this._destroyRenderersByKind("horizontal"); this._set("horizontalRenderer", value); }, _setLabelRendererAttr: function(value){ this._destroyRenderersByKind("label"); this._set("labelRenderer", value); }, _destroyExpandRenderer: function(renderer){ // summary: // Destroys the expand renderer. // renderer: dojox/calendar/_RendererMixin // The item renderer to destroy. // tags: // protected if(renderer["destroyRecursive"]){ renderer.destroyRecursive(); } html.destroy(renderer.domNode); }, _setExpandRendererAttr: function(value){ while(this._ddRendererList.length>0){ this._destroyExpandRenderer(this._ddRendererList.pop()); } var pool = this._ddRendererPool; if(pool){ while(pool.length > 0){ this._destroyExpandRenderer(pool.pop()); } } this._set("expandRenderer", value); }, _ddRendererList: null, _ddRendererPool: null, _getExpandRenderer: function(date, items, rowIndex, colIndex, expanded){ // tags: // private if(this.expandRenderer == null){ return null; } var ir = this._ddRendererPool.pop(); if(ir == null){ ir = new this.expandRenderer(); } this._ddRendererList.push(ir); ir.set("owner", this); ir.set("date", date); ir.set("items", items); ir.set("rowIndex", rowIndex); ir.set("columnIndex", colIndex); ir.set("expanded", expanded); return ir; }, _recycleExpandRenderers: function(remove){ // tags: // private for(var i=0; i= 1440 ? "horizontal" : "label"; }, //////////////////////////////////////////// // // Layout // /////////////////////////////////////////// // naturalRowHeight: Integer[] // After an item layout has been done, contains for each row the natural height of the row. // Ie. the height, in pixels, needed to display all the item renderers. naturalRowsHeight: null, _roundItemToDay: function(item){ // tags: // private var s = item.startTime, e = item.endTime; if(!this.isStartOfDay(s)){ s = this.floorToDay(s, false, this.renderData); } if(!this.isStartOfDay(e)){ e = this.renderData.dateModule.add(e, "day", 1); e = this.floorToDay(e, true); } return {startTime:s, endTime:e}; }, _sortItemsFunction: function(a, b){ // tags: // private if(this.roundToDay){ a = this._roundItemToDay(a); b = this._roundItemToDay(b); } var res = this.dateModule.compare(a.startTime, b.startTime); if(res == 0){ res = -1 * this.dateModule.compare(a.endTime, b.endTime); } return res; }, _overlapLayoutPass3: function(lanes){ // summary: // Third pass of the overlap layout (optional). Compute the number of lanes used by sub interval. // lanes: Object[] // The array of lanes. // tags: // private var pos=0, posEnd=0; var res = []; var refPos = domGeometry.position(this.gridTable).x; for(var col=0; col=0 && !stop; lane--){ for (var i=0; i 0 && this.horizontalRenderer){ var hItems = this._createHorizontalLayoutItems(index, start, end, horizontalItems); var hOverlapLayout = this._computeHorizontalOverlapLayout(hItems, hOffsets); } var lItems; var lOffsets = []; if(labelItems.length > 0 && this.labelRenderer){ lItems = this._createLabelLayoutItems(index, start, end, labelItems); this._computeLabelOffsets(lItems, lOffsets); } var hasHiddenItems = this._computeColHasHiddenItems(index, hOffsets, lOffsets); if(hItems != null){ this._layoutHorizontalItemsImpl(index, hItems, hOverlapLayout, hasHiddenItems, hiddenItems); } if(lItems != null){ this._layoutLabelItemsImpl(index, lItems, hasHiddenItems, hiddenItems, hOffsets); } this._layoutExpandRenderers(index, hasHiddenItems, hiddenItems); this._hiddenItems[index] = hiddenItems; }, _createHorizontalLayoutItems: function(/*Integer*/index, /*Date*/startTime, /*Date*/endTime, /*Object[]*/items){ // tags: // private if(this.horizontalRenderer == null){ return; } var rd = this.renderData; var cal = rd.dateModule; var sign = rd.rtl ? -1 : 1; var layoutItems = []; // step 1: compute projected position and size for(var i = 0; i < items.length; i++){ var item = items[i]; var overlap = this.computeRangeOverlap(rd, item.startTime, item.endTime, startTime, endTime); var startOffset = cal.difference(startTime, this.floorToDay(overlap[0], false, rd), "day"); var dayStart = rd.dates[index][startOffset]; var celPos = domGeometry.position(this._getCellAt(index, startOffset, false)); var start = celPos.x - rd.gridTablePosX; if(rd.rtl){ start += celPos.w; } if(!this.roundToDay && !item.allDay){ start += sign * this.computeProjectionOnDate(rd, dayStart, overlap[0], celPos.w); } start = Math.ceil(start); var endOffset = cal.difference(startTime, this.floorToDay(overlap[1], false, rd), "day"); var end; if(endOffset > rd.columnCount-1){ celPos = domGeometry.position(this._getCellAt(index, rd.columnCount-1, false)); if(rd.rtl){ end = celPos.x - rd.gridTablePosX; }else{ end = celPos.x - rd.gridTablePosX + celPos.w; } }else{ dayStart = rd.dates[index][endOffset]; celPos = domGeometry.position(this._getCellAt(index, endOffset, false)); end = celPos.x - rd.gridTablePosX; if(rd.rtl){ end += celPos.w; } if(this.roundToDay){ if(!this.isStartOfDay(overlap[1])){ end += sign * celPos.w; } }else{ end += sign * this.computeProjectionOnDate(rd, dayStart, overlap[1], celPos.w); } } end = Math.floor(end); if(rd.rtl){ var t = end; end = start; start = t; } if(end > start){ // invalid items are not displayed var litem = lang.mixin({ start: start, end: end, range: overlap, item: item, startOffset: startOffset, endOffset: endOffset }, item); layoutItems.push(litem); } } return layoutItems; }, _computeHorizontalOverlapLayout: function(layoutItems, offsets){ // tags: // private var rd = this.renderData; var irHeight = this.horizontalRendererHeight; var overlapLayoutRes = this.computeOverlapping(layoutItems, this._overlapLayoutPass3); var vOverlap = this.percentOverlap / 100; for(var i=0; i= this.columnCount){ // If the offset is greater than the column count // the item will be processed in another row. break; } if(startOffset >= 0){ var list = layoutItems[startOffset]; if(list == null){ list = []; layoutItems[startOffset] = list; } list.push(lang.mixin( { startOffset: startOffset, range: overlap, item: item }, item)); } d = cal.add(d, "day", 1); this.floorToDay(d, true); } } return layoutItems; }, _computeLabelOffsets: function(layoutItems, offsets){ // tags: // private for(var i=0; i maxH){ maxH = h; } res[i] = h > cellH; } this.naturalRowsHeight[index] = maxH; return res; }, _layoutHorizontalItemsImpl: function(index, layoutItems, hOverlapLayout, hasHiddenItems, hiddenItems){ // tags: // private var rd = this.renderData; var cell = rd.cells[index]; var cellH = this._getRowHeight(index); var irHeight = this.horizontalRendererHeight; var vOverlap = this.percentOverlap / 100; for(var i=0; i= 9 && item.start + w < this.itemContainer.offsetWidth) { w++; } domStyle.set(ir.container, { "top": (fullHeight ? this.cellPaddingTop : posY) + "px", "left": item.start + "px", "width": w + "px", "height": h + "px" }); this._applyRendererLayout(item, ir, cell, w, h, "horizontal"); }else{ // The items does not fit in view, fill hidden items per column for(var d=item.startOffset;d r.w){ x = r.w-1; } if(y < 0){ y = 0; }else if(y > r.h){ y = r.h-1; } // compute the date from column the time in day instead of time from start date of row to prevent DST hour offset. var w = domGeometry.getMarginBox(this.itemContainer).w; var colW = w / rd.columnCount; var row; if(rd.expandedRow == null){ row = Math.floor(y / (domGeometry.getMarginBox(this.itemContainer).h / rd.rowCount)); }else{ row = rd.expandedRow; //other rows are not usable } var r = domGeometry.getContentBox(this.itemContainer); if(rd.rtl){ x = r.w - x; } var col = Math.floor(x / colW); var tm = Math.floor((x-(col*colW)) * 1440 / colW); var date = null; if(row < rd.dates.length && col < this.renderData.dates[row].length){ date = this.newDate(this.renderData.dates[row][col]); date = this.renderData.dateModule.add(date, "minute", tm); } return date; }, ///////////////////////////////////////////// // // Event management // ////////////////////////////////////////////// _onGridMouseUp: function(e){ // tags: // private this.inherited(arguments); if(this._gridMouseDown){ this._gridMouseDown = false; this._onGridClick({ date: this.getTime(e), triggerEvent: e }); } }, _onGridTouchEnd: function(e){ // tags: // private this.inherited(arguments); var g = this._gridProps; if(g){ if(!this._isEditing){ // touched on grid and on touch start editing was ongoing. if(!g.fromItem && !g.editingOnStart){ this.selectFromEvent(e, null, null, true); } if(!g.fromItem){ if(this._pendingDoubleTap && this._pendingDoubleTap.grid){ this._onGridDoubleClick({ date: this.getTime(this._gridProps.event), triggerEvent: this._gridProps.event }); clearTimeout(this._pendingDoubleTap.timer); delete this._pendingDoubleTap; }else{ this._onGridClick({ date: this.getTime(this._gridProps.event), triggerEvent: this._gridProps.event }); this._pendingDoubleTap = { grid: true, timer: setTimeout(lang.hitch(this, function(){ delete this._pendingDoubleTap; }), this.doubleTapDelay) }; } } } this._gridProps = null; } }, ///////////////////////////////////////////// // // Events // ////////////////////////////////////////////// _onRowHeaderClick: function(e){ this._dispatchCalendarEvt(e, "onRowHeaderClick"); // tags: // private }, onRowHeaderClick: function(e){ // summary: // Event dispatched when a row header cell is clicked. // e: __HeaderClickEventArgs // Header click event. // tags: // callback }, expandRendererClickHandler: function(e, renderer){ // summary: // Default action when an expand renderer is clicked. // e: Event // The mouse event. // renderer: Object // The expand renderer. // tags: // protected event.stop(e); var ri = renderer.get("rowIndex"); var ci = renderer.get("columnIndex"); this._onExpandRendererClick(lang.mixin(this._createItemEditEvent(), { rowIndex: ri, columnIndex: ci, renderer: renderer, triggerEvent: e, date: this.renderData.dates[ri][ci] })); }, onExpandRendererClick: function(e){ // summary: // Event dispatched when an expand renderer is clicked. // e: __ExpandRendererClickEventArgs // Expand renderer click event. // tags: // callback }, _onExpandRendererClick: function(e){ this._dispatchCalendarEvt(e, "onExpandRendererClick"); if(!e.isDefaultPrevented()){ if(this.getExpandedRowIndex() != -1){ this.collapseRow(); }else{ this.expandRow(e.rowIndex, e.columnIndex); } } }, //////////////////////////////////////////// // // Editing // /////////////////////////////////////////// snapUnit: "minute", snapSteps: 15, minDurationUnit: "minute", minDurationSteps: 15, triggerExtent: 3, liveLayout: false, stayInView: true, allowStartEndSwap: true, allowResizeLessThan24H: false }); });