dojox
Version:
Dojo eXtensions, a rollup of many useful sub-projects and varying states of maturity – from very stable and robust, to alpha and experimental. See individual projects contain README files for details.
1,713 lines (1,390 loc) • 66.7 kB
JavaScript
define([
"./ViewBase",
"dijit/_TemplatedMixin",
"./_ScrollBarBase",
"dojo/text!./templates/ColumnView.html",
"dojo/_base/declare",
"dojo/_base/event",
"dojo/_base/lang",
"dojo/_base/array",
"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/mouse",
"dojo/query",
"dojox/html/metrics"],
function(
ViewBase,
_TemplatedMixin,
_ScrollBarBase,
template,
declare,
event,
lang,
arr,
has,
fx,
html,
on,
dom,
domClass,
domStyle,
domGeometry,
domConstruct,
mouse,
query,
metrics){
/*=====
var __ColumnClickEventArgs = {
// summary:
// A column click event.
// index: Integer
// The column index.
// date: Date
// The date displayed by the column.
// triggerEvent: Event
// The origin event.
};
=====*/
return declare("dojox.calendar.SimpleColumnView", [ViewBase, _TemplatedMixin], {
// summary:
// The simple column view is displaying a day per column. Each cell of a column is a time slot.
baseClass: "dojoxCalendarColumnView",
templateString: template,
// viewKind: String
// Type of the view. Used by the calendar widget to determine how to configure the view.
// This view kind is "columns".
viewKind: "columns",
// scroll container is the focusable item to enable scrolling using up and down arrows
_setTabIndexAttr: "domNode",
// renderData: Object
// The render data is the object that contains all the properties needed to render the component.
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,
// columnCount: Integer
// The number of column to display (from the startDate).
columnCount: 7,
// subcolumns: String[]
// Array of sub columns values.
subColumns: null,
// minHours: Integer
// The minimum hour to be displayed. It must be in the [0,23] interval and must be lower than the maxHours.
minHours: 8,
// maxHours: Integer
// The maximum hour to be displayed. It must be in the [1,36] interval and must be greater than the minHours.
maxHours: 18,
// hourSize: Integer
// The desired size in pixels of an hour on the screen.
// Note that the effective size may be different as the time slot size must be an integer.
hourSize: 100,
// timeSlotDuration: Integer
// Duration of the time slot in minutes. Must be a divisor of 60.
timeSlotDuration: 15,
// rowHeaderGridSlotDuration: Integer
// Duration of the time slot in minutes in the row header. Must be a divisor of 60 and a multiple/divisor of timeSlotDuration.
rowHeaderGridSlotDuration: 60,
// rowHeaderLabelSlotDuration: Integer
// Duration of the time slot in minutes in the row header labels. Must be a divisor of 60 and a multiple/divisor of timeSlotDuration.
rowHeaderLabelSlotDuration: 60,
// rowHeaderLabelOffset: Integer
// Offset of the row label from the top of the row header cell in pixels.
rowHeaderLabelOffset: 2,
// rowHeaderFirstLabelOffset: Integer
// Offset of the first row label from the top of the first row header cell in pixels.
rowHeaderFirstLabelOffset: 2,
// verticalRenderer: Class
// The class use to create vertical renderers.
verticalRenderer: null,
// verticalDecorationRenderer: Class
// The class use to create decoration renderers.
verticalDecorationRenderer: null,
// minColumnWidth: Integer
// The minimum column width. If the number of columns and sub columns displayed makes the
// width of a column greater than this property, a horizontal scroll bar is displayed.
// If value <= 0, this constraint is ignored and the columns are using the available space.
minColumnWidth: -1,
// percentOverlap: Integer
// The percentage of the renderer width used to superimpose one item renderer on another
// when two events are overlapping.
percentOverlap: 70,
// horizontalGap: Integer
// The number of pixels between two item renderers that are overlapping each other if the percentOverlap property is 0.
horizontalGap: 4,
_showSecondarySheet: false,
_columnHeaderHandlers: null,
constructor: function(){
this.invalidatingProperties = ["columnCount", "startDate", "minHours", "maxHours", "hourSize", "verticalRenderer", "verticalDecorationRenderer",
"rowHeaderTimePattern", "columnHeaderDatePattern", "timeSlotDuration", "rowHeaderGridSlotDuration", "rowHeaderLabelSlotDuration",
"rowHeaderLabelOffset", "rowHeaderFirstLabelOffset","percentOverlap", "horizontalGap", "scrollBarRTLPosition","itemToRendererKindFunc",
"layoutPriorityFunction", "formatItemTimeFunc", "textDir", "items", "subColumns", "minColumnWidth"];
this._columnHeaderHandlers = [];
},
destroy: function(preserveDom){
this._cleanupColumnHeader();
if(this.scrollBar){
this.scrollBar.destroy(preserveDom);
}
this.inherited(arguments);
},
_scrollBar_onScroll: function(value){
this._setScrollPosition(value);
},
_hscrollBar_onScroll: function(value){
this._setHScrollPosition(value);
},
buildRendering: function(){
this.inherited(arguments);
if(this.vScrollBar){
this.scrollBar = new _ScrollBarBase(
{content: this.vScrollBarContent},
this.vScrollBar);
this.scrollBar.on("scroll", lang.hitch(this, this._scrollBar_onScroll));
}
if(this.hScrollBar){
this.hScrollBarW = new _ScrollBarBase(
{content: this.hScrollBarContent, direction: "horizontal", value: 0},
this.hScrollBar);
this.hScrollBarW.on("scroll", lang.hitch(this, this._hscrollBar_onScroll));
this._hScrollNodes = [this.columnHeaderTable, this.subColumnHeaderTable, this.gridTable, this.itemContainerTable];
}
this._viewHandles.push(
on(this.scrollContainer, mouse.wheel,
dojo.hitch(this, this._mouseWheelScrollHander)));
},
postscript: function(){
this.inherited(arguments);
this._initialized = true;
if(!this.invalidRendering){
this.refreshRendering();
}
},
_setVerticalRendererAttr: function(value){
this._destroyRenderersByKind("vertical");
this._set("verticalRenderer", value);
},
_createRenderData: function(){
var rd = {};
rd.minHours = this.get("minHours");
rd.maxHours = this.get("maxHours");
rd.hourSize = this.get("hourSize");
rd.hourCount = rd.maxHours - rd.minHours;
rd.slotDuration = this.get("timeSlotDuration"); // must be consistent with previous statement
rd.rowHeaderGridSlotDuration = this.get("rowHeaderGridSlotDuration");
rd.slotSize = Math.ceil(rd.hourSize / (60 / rd.slotDuration));
rd.hourSize = rd.slotSize * (60 / rd.slotDuration);
rd.sheetHeight = rd.hourSize * rd.hourCount;
if(!this._rowHeaderWidth){
this._rowHeaderWidth = domGeometry.getMarginBox(this.rowHeader).w;
}
rd.rowHeaderWidth = this._rowHeaderWidth;
var sbMetrics = metrics.getScrollbar();
rd.scrollbarWidth = sbMetrics.w + 1;
rd.scrollbarHeight = sbMetrics.h + 1;
rd.dateLocaleModule = this.dateLocaleModule;
rd.dateClassObj = this.dateClassObj;
rd.dateModule = this.dateModule; // arithmetics on Dates
rd.dates = [];
rd.columnCount = this.get("columnCount");
rd.subColumns = this.get("subColumns");
rd.subColumnCount = rd.subColumns ? rd.subColumns.length : 1;
rd.hScrollPaneWidth = domGeometry.getMarginBox(this.grid).w;
rd.minSheetWidth = this.minColumnWidth < 0 ? -1 : this.minColumnWidth * rd.subColumnCount * rd.columnCount;
rd.hScrollBarEnabled = this.minColumnWidth > 0 && rd.hScrollPaneWidth < rd.minSheetWidth;
var d = this.get("startDate");
if (d == null){
d = new rd.dateClassObj();
}
d = this.floorToDay(d, false, rd);
this.startDate = d;
for(var col = 0; col < rd.columnCount ; col++){
rd.dates.push(d);
d = this.addAndFloor(d, "day", 1);
}
rd.startTime = new rd.dateClassObj(rd.dates[0]);
rd.startTime.setHours(rd.minHours);
rd.endTime = new rd.dateClassObj(rd.dates[rd.columnCount-1]);
rd.endTime.setHours(rd.maxHours);
if(this.displayedItemsInvalidated && !this._isEditing){
// while editing in no live layout we must not to recompute items (duplicate renderers)
rd.items = this.storeManager._computeVisibleItems(rd);
}else if (this.renderData){
rd.items = this.renderData.items;
}
if(this.displayedDecorationItemsInvalidated){
// while editing in no live layout we must not to recompute items (duplicate renderers)
rd.decorationItems = this.decorationStoreManager._computeVisibleItems(rd);
}else if (this.renderData){
rd.decorationItems = this.renderData.decorationItems;
}
return rd;
},
_validateProperties: function() {
this.inherited(arguments);
var v = this.minHours;
if(v < 0 || v>23 || isNaN(v)){
this.minHours = 0;
}
v = this.maxHours;
if (v < 1 || v>36 || isNaN(v)){
this.minHours = 36;
}
if(this.minHours > this.maxHours){
var t = this.maxHours;
this.maxHours = this.minHours;
this.minHours = t;
}
if (this.maxHours - this.minHours < 1){
this.minHours = 0;
this.maxHours = 24;
}
if (this.columnCount<1 || isNaN(this.columnCount)){
this.columnCount = 1;
}
v = this.percentOverlap;
if(v < 0 ||v > 100 || isNaN(v)){
this.percentOverlap = 70;
}
if(this.hourSize<5 || isNaN(this.hourSize)){
this.hourSize = 10;
}
v = this.timeSlotDuration;
if (v < 1 || v > 60 || isNaN(v)) {
this.timeSlotDuration = 15;
}
},
_setStartDateAttr: function(value){
this.displayedItemsInvalidated = true;
this._set("startDate", value);
},
_setColumnCountAttr: function(value){
this.displayedItemsInvalidated = true;
this._set("columnCount", value);
},
__fixEvt:function(e){
// tags:
// private
e.sheet = "primary";
e.source = this;
return e;
},
//////////////////////////////////////////
//
// Formatting functions
//
//////////////////////////////////////////
// rowHeaderTimePattern: String
// Custom date/time pattern for the row header labels to override default one coming from the CLDR.
// See dojo/date/locale documentation for format string.
rowHeaderTimePattern: null,
_formatRowHeaderLabel: function(/*Date*/d){
// summary:
// Computes the row header label for the specified time of day.
// By default a formatter is used, optionally the <code>rowHeaderTimePattern</code> property can be used to set a custom time pattern to the formatter.
// d: Date
// The date to format
// tags:
// protected
return this.renderData.dateLocaleModule.format(d, {
selector: "time",
timePattern: this.rowHeaderTimePattern
});
},
// columnHeaderDatePattern: String
// Custom date/time pattern for column header labels to override default one coming from the CLDR.
// See dojo/date/locale documentation for format string.
columnHeaderDatePattern: null,
_formatColumnHeaderLabel: function(/*Date*/d){
// summary:
// Computes the column header label for the specified date.
// By default a formatter is used, optionally the <code>columnHeaderDatePattern</code> property can be used to set a custom date pattern to the formatter.
// d: Date
// The date to format
// tags:
// protected
return this.renderData.dateLocaleModule.format(d, {
selector: "date",
datePattern: this.columnHeaderDatePattern,
formatLength: "medium"
});
},
//////////////////////////////////////////
//
// Time of day management
//
//////////////////////////////////////////
// scrollBarRTLPosition: String
// Position of the scroll bar in right-to-left display.
// Valid values are "left" and "right", default value is "left".
scrollBarRTLPosition: "left",
_getStartTimeOfDay: function(){
// summary:
// Returns the visible first time of day.
// tags:
// protected
// returns: Object
var v = (this.get("maxHours") - this.get("minHours")) *
this._getScrollPosition() / this.renderData.sheetHeight;
return {
hours: this.renderData.minHours + Math.floor(v),
minutes: (v - Math.floor(v)) * 60
};
},
_getEndTimeOfDay: function(){
// summary:
// Returns the visible last time of day.
// tags:
// protected
// returns: Integer[]
var v = (this.get("maxHours") - this.get("minHours")) *
(this._getScrollPosition() + this.scrollContainer.offsetHeight) / this.renderData.sheetHeight;
return {
hours: this.renderData.minHours + Math.floor(v),
minutes: (v - Math.floor(v)) * 60
};
},
// startTimeOfDay: Object
// First time (hour/minute) of day displayed, if reachable.
// An object containing "hours" and "minutes" properties.
startTimeOfDay: 0,
_setStartTimeOfDayAttr: function(value){
if(this.renderData){
this._setStartTimeOfDay(value.hours, value.minutes, value.duration, value.easing);
}else{
this._startTimeOfDayInvalidated = true;
}
this._set("startTimeOfDay", value);
},
_getStartTimeOfDayAttr: function(){
if(this.renderData){
return this._getStartTimeOfDay();
}else{
return this._get("startTimeOfDay");
}
},
_setStartTimeOfDay: function(hour, minutes, maxDuration, easing){
// summary:
// Scrolls the view to show the specified first time of day.
// hour: Integer
// The hour of the start time of day.
// minutes: Integer
// The minutes part of the start time of day.
// maxDuration: Integer
// The max duration of the scroll animation.
// tags:
// protected
var rd = this.renderData;
hour = hour || rd.minHours;
minutes = minutes || 0;
maxDuration = maxDuration || 0;
if (minutes < 0){
minutes = 0;
}else if (minutes > 59){
minutes = 59;
}
if (hour < 0){
hour = 0;
}else if (hour > rd.maxHours){
hour = rd.maxHours;
}
var timeInMinutes = hour * 60 + minutes;
var minH = rd.minHours*60;
var maxH = rd.maxHours*60;
if (timeInMinutes < minH){
timeInMinutes = minH;
}else if(timeInMinutes > maxH){
timeInMinutes = maxH;
}
var pos = (timeInMinutes - minH) * rd.sheetHeight / (maxH - minH);
pos = Math.min(rd.sheetHeight - this.scrollContainer.offsetHeight, pos);
this._scrollToPosition(pos, maxDuration, easing);
},
_scrollToPosition: function(position, maxDuration, easing){
// summary:
// Scrolls the view to show the specified first time of day.
// position: Integer
// The position in pixels.
// maxDuration: Integer
// The max duration of the scroll animation.
// tags:
// protected
if (maxDuration) {
if(this._scrollAnimation){
this._scrollAnimation.stop();
}
var scrollPos = this._getScrollPosition();
var duration = Math.abs(((position - scrollPos) * maxDuration) / this.renderData.sheetHeight);
this._scrollAnimation = new fx.Animation({
curve: [scrollPos, position],
duration: duration,
easing: easing,
onAnimate: lang.hitch(this, function(position) {
this._setScrollImpl(position);
})
});
this._scrollAnimation.play();
}else{
this._setScrollImpl(position);
}
},
_setScrollImpl: function(v){
this._setScrollPosition(v);
if(this.scrollBar){
this.scrollBar.set("value", v);
}
},
ensureVisibility: function(start, end, visibilityTarget, margin, duration){
// summary:
// Scrolls the view if the [start, end] time range is not visible or only partially visible.
// start: Date
// Start time of the range of interest.
// end: Date
// End time of the range of interest.
// margin: Integer
// Margin in minutes around the time range.
// visibilityTarget: String
// The end(s) of the time range to make visible.
// Valid values are: "start", "end", "both".
// duration: Number
// Optional, the maximum duration of the scroll animation.
margin = margin == undefined ? this.renderData.slotDuration : margin;
if(this.scrollable && this.autoScroll){
var s = start.getHours() * 60 + start.getMinutes() - margin;
var e = end.getHours() * 60 + end.getMinutes() + margin;
var vs = this._getStartTimeOfDay();
var ve = this._getEndTimeOfDay();
var viewStart = vs.hours * 60 + vs.minutes;
var viewEnd = ve.hours * 60 + ve.minutes;
var visible = false;
var target = null;
switch(visibilityTarget){
case "start":
visible = s >= viewStart && s <= viewEnd;
target = s ;
break;
case "end":
visible = e >= viewStart && e <= viewEnd;
target = e - (viewEnd - viewStart);
break;
case "both":
visible = s >= viewStart && e <= viewEnd;
target = s;
break;
}
if(!visible){
this._setStartTimeOfDay(Math.floor(target/60), target%60, duration);
}
}
},
scrollView: function(dir){
// summary:
// Scrolls the view to the specified direction of one time slot duration.
// dir: Integer
// Direction of the scroll. Valid values are -1 and 1.
//
var t = this._getStartTimeOfDay();
t = t.hours*60 + t.minutes + (dir * this.timeSlotDuration);
this._setStartTimeOfDay(Math.floor(t/60), t%60);
},
scrollViewHorizontal: function(dir){
// summary:
// Scrolls the view horizontally to the specified direction of one column or sub column (if set).
// dir: Integer
// Direction of the scroll. Valid values are -1 and 1.
//
this._setHScrollPosition(this._getHScrollPosition() + (dir * this.minColumnWidth));
if(this.hScrollBarW){
this.hScrollBarW.set("value", this._getHScrollPosition());
}
},
_hScrollNodes: null,
_setHScrollPositionImpl: function(pos, useDom, cssProp){
var elts = [this.columnHeaderTable, this.subColumnHeaderTable, this.gridTable, this.itemContainerTable];
var css = useDom ? null : "translateX(-"+pos+"px)";
arr.forEach(elts, function(elt){
if(useDom){
elt.scrollLeft = pos;
domStyle.set(elt, "left", (-pos) + "px");
}else{
domStyle.set(elt, cssProp, css);
}
}, this);
},
_mouseWheelScrollHander: function(e){
// summary:
// Mouse wheel handler.
// tags:
// protected
if(this.renderData.hScrollBarEnabled && e.altKey){
this.scrollViewHorizontal(e.wheelDelta > 0 ? -1 : 1);
}else{
this.scrollView(e.wheelDelta > 0 ? -1 : 1);
}
event.stop(e);
},
//////////////////////////////////////////
//
// HTML structure management
//
//////////////////////////////////////////
refreshRendering: function(){
if(!this._initialized){
return;
}
this._validateProperties();
var oldRd = this.renderData;
var rd = this._createRenderData();
this.renderData = rd;
this._createRendering(rd, oldRd);
this._layoutDecorationRenderers(rd);
this._layoutRenderers(rd);
},
_createRendering: function(/*Object*/renderData, /*Object*/oldRenderData){
// tags:
// private
domStyle.set(this.sheetContainer, "height", renderData.sheetHeight + "px");
// padding for the scroll bar.
this._configureVisibleParts(renderData);
this._configureScrollBar(renderData);
this._buildColumnHeader(renderData, oldRenderData);
this._buildSubColumnHeader(renderData, oldRenderData);
this._buildRowHeader(renderData, oldRenderData);
this._buildGrid(renderData, oldRenderData);
this._buildItemContainer(renderData, oldRenderData);
this._layoutTimeIndicator(renderData);
this._commitProperties(renderData);
},
_configureVisibleParts: function(renderData){
if(this.secondarySheetNode){
domStyle.set(this.secondarySheetNode, "display", this._showSecondarySheet ? "block" : "none");
}
domClass[this.subColumns == null?"remove":"add"](this.domNode, "subColumns");
domClass[this._showSecondarySheet?"add":"remove"](this.domNode, "secondarySheet");
},
_commitProperties: function(renderData){
if(this._startTimeOfDayInvalidated){
this._startTimeOfDayInvalidated = false;
var v = this.startTimeOfDay;
if(v != null){
this._setStartTimeOfDay(v.hours, v.minutes == undefined ? 0 : v.minutes); // initial position, no animation
}
}
},
_configureScrollBar: function(renderData){
// summary:
// Sets the scroll bar size and position.
// renderData: Object
// The render data.
// tags:
// protected
if(has("ie") && this.scrollBar){
domStyle.set(this.vScrollBar, "width", (renderData.scrollbarWidth + 1) + "px");
}
var atRight = this.isLeftToRight() ? true : this.scrollBarRTLPosition == "right";
var rPos = atRight ? "right" : "left";
var lPos = atRight ? "left" : "right";
if(this.scrollBar){
this.scrollBar.set("maximum", renderData.sheetHeight);
domStyle.set(this.vScrollBar, rPos, 0);
domStyle.set(this.vScrollBar, atRight? "left" : "right", "auto");
domStyle.set(this.vScrollBar, "bottom", renderData.hScrollBarEnabled? renderData.scrollbarHeight + "px" : "0");
}
domStyle.set(this.scrollContainer, rPos, renderData.scrollbarWidth + "px");
domStyle.set(this.scrollContainer, lPos, "0");
domStyle.set(this.header, rPos, renderData.scrollbarWidth + "px");
domStyle.set(this.header, lPos, "0");
domStyle.set(this.subHeader, rPos, renderData.scrollbarWidth + "px");
domStyle.set(this.subHeader, lPos, "0");
if(this.buttonContainer && this.owner != null && this.owner.currentView == this){
domStyle.set(this.buttonContainer, rPos, renderData.scrollbarWidth + "px");
domStyle.set(this.buttonContainer, lPos, "0");
}
if(this.hScrollBar){
arr.forEach(this._hScrollNodes, function(elt){
domClass[renderData.hScrollBarEnabled ? "add" : "remove"](elt.parentNode, "dojoxCalendarHorizontalScroll");
}, this);
if(!renderData.hScrollBarEnabled){
this._setHScrollPosition(0);
this.hScrollBarW.set("value", 0);
}
domStyle.set(this.hScrollBar, {
"display": renderData.hScrollBarEnabled ? "block" : "none",
"height": renderData.scrollbarHeight + "px",
"left": (atRight ? renderData.rowHeaderWidth : renderData.scrollbarWidth) + "px",
"right": (atRight ? renderData.scrollbarWidth : renderData.rowHeaderWidth) + "px"
});
domStyle.set(this.scrollContainer, "bottom", renderData.hScrollBarEnabled ? (renderData.scrollbarHeight + 1) + "px" : "0");
this._configureHScrollDomNodes(renderData.hScrollBarEnabled ? renderData.minSheetWidth + "px" : "100%");
this.hScrollBarW.set("maximum", renderData.minSheetWidth);
this.hScrollBarW.set("containerSize", renderData.hScrollPaneWidth);
}
},
_configureHScrollDomNodes: function(styleWidth){
arr.forEach(this._hScrollNodes, function(elt){
domStyle.set(elt, "width", styleWidth);
}, this);
},
resize: function(e){
this._resizeHandler(e);
},
_resizeHandler: function(e, apply){
// summary:
// Refreshes the scroll bars after a resize of the widget.
// e: Event
// The resize event (optional)
// apply: Boolean
// Whether apply the changes or wait for 100 ms
// tags:
// private
var rd = this.renderData;
if(rd == null){
return;
}
if(apply){
var hScrollPaneWidth = domGeometry.getMarginBox(this.grid).w;
if(rd.hScrollPaneWidth != hScrollPaneWidth){
// refresh values
rd.hScrollPaneWidth = hScrollPaneWidth;
rd.minSheetWidth = this.minColumnWidth < 0 ? -1 : this.minColumnWidth * rd.subColumnCount * rd.columnCount;
rd.hScrollBarEnabled = this.minColumnWidth > 0 && domGeometry.getMarginBox(this.grid).w < rd.minSheetWidth;
}
this._configureScrollBar(rd);
}else{
if(this._resizeTimer != undefined){
clearTimeout(this._resizeTimer);
}
this._resizeTimer = setTimeout(lang.hitch(this, function(){
this._resizeHandler(e, true);
}), 100);
}
},
_columnHeaderClick: function(e){
// tags:
// private
event.stop(e);
var index = query("td", this.columnHeaderTable).indexOf(e.currentTarget);
this._onColumnHeaderClick({
index: index,
date: this.renderData.dates[index],
triggerEvent: e
});
},
_buildColumnHeader: function(renderData, 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._cleanupColumnHeader();
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);
var h = [];
h.push(on(td, "click", lang.hitch(this, this._columnHeaderClick)));
if(has("touch-events")){
h.push(on(td, "touchstart", function(e){
event.stop(e);
domClass.add(e.currentTarget, "Active");
}));
h.push(on(td, "touchend", function(e){
event.stop(e);
domClass.remove(e.currentTarget, "Active");
}));
}else{
h.push(on(td, "mousedown", function(e){
event.stop(e);
domClass.add(e.currentTarget, "Active");
}));
h.push(on(td, "mouseup", function(e){
event.stop(e);
domClass.remove(e.currentTarget, "Active");
}));
h.push(on(td, "mouseover", function(e){
event.stop(e);
domClass.add(e.currentTarget, "Hover");
}));
h.push(on(td, "mouseout", function(e){
event.stop(e);
domClass.remove(e.currentTarget, "Hover");
}));
}
this._columnHeaderHandlers.push(h);
}
}else{ // deletion
count = -count;
for(var i=0; i < count; i++){
td = tr.lastChild;
tr.removeChild(td);
domConstruct.destroy(td);
var list = this._columnHeaderHandlers.pop();
while(list.length>0){
list.pop().remove();
}
}
}
// fill & configure
query("td", table).forEach(function(td, i){
td.className = "";
if(i == 0){
domClass.add(td, "first-child");
}else if(i == this.renderData.columnCount-1){
domClass.add(td, "last-child");
}
var d = renderData.dates[i];
this._setText(td, this._formatColumnHeaderLabel(d));
this.styleColumnHeaderCell(td, d, renderData);
}, this);
if(this.yearColumnHeaderContent){
var d = renderData.dates[0];
this._setText(this.yearColumnHeaderContent, renderData.dateLocaleModule.format(d,
{selector: "date", datePattern:"yyyy"}));
}
},
_cleanupColumnHeader: function(){
while(this._columnHeaderHandlers.length > 0){
var list = this._columnHeaderHandlers.pop();
while(list.length > 0){
list.pop().remove();
}
}
},
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:
// - "dojoxCalendarToday" class name if the date displayed is the current date,
// - "dojoxCalendarWeekend" if the date represents a weekend,
// - the CSS class corresponding of the displayed day of week ("Sun", "Mon" and so on).
// 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.isToday(date)){
domClass.add(node, "dojoxCalendarToday");
} else if(this.isWeekEnd(date)){
domClass.add(node, "dojoxCalendarWeekend");
}
},
_buildSubColumnHeader: function(renderData, 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.subColumnHeaderTable;
if (!table || this.subColumns == null){
return;
}
var count = renderData.columnCount - query("td", table).length;
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._colSubTableSave == null){
this._colSubTableSave = lang.clone(table);
}else if(count < 0){
this.subColumnHeader.removeChild(table);
domConstruct.destroy(table);
table = lang.clone(this._colSubTableSave);
this.subColumnHeaderTable = table;
this.subColumnHeader.appendChild(table);
count = renderData.columnCount;
}
} // else incremental dom add/remove for real browsers.
var tbodies = query(">tbody", table);
var tbody, tr, td;
if (tbodies.length == 1){
tbody = tbodies[0];
}else{
tbody = html.create("tbody", null, table);
}
var trs = query(">tr", tbody);
if (trs.length == 1){
tr = trs[0];
}else{
tr = domConstruct.create("tr", null, tbody);
}
var subCount = renderData.subColumnCount;
// Build HTML structure (incremental)
if(count > 0){ // creation
for(var i=0; i < count; i++){
td = domConstruct.create("td", null, tr);
domConstruct.create("div", {"className": "dojoxCalendarSubHeaderContainer"}, td);
}
}else{ // deletion
count = -count;
for(var i=0; i < count; i++){
td = tr.lastChild;
tr.removeChild(td);
domConstruct.destroy(td);
}
}
// fill & configure
query("td", table).forEach(function(td, i){
td.className = "";
if(i == 0){
domClass.add(td, "first-child");
}else if(i == this.renderData.columnCount-1){
domClass.add(td, "last-child");
}
query(".dojoxCalendarSubHeaderContainer", td).forEach(function(div, i){
var count = query(".dojoxCalendarSubHeaderContainer", div).length - subCount;
if(count != 0){
var len = div.childNodes.length;
for(var i=0; i<len; i++){
div.removeChild(div.lastChild);
}
for(var j=0; j<subCount; j++){
domConstruct.create("div", {"className": "dojoxCalendarSubHeaderCell dojoxCalendarSubHeaderLabel"}, div);
}
}
var colW = (100/subCount) + "%";
query(".dojoxCalendarSubHeaderCell", div).forEach(function(div, i){
div.className = "dojoxCalendarSubHeaderCell dojoxCalendarSubHeaderLabel";
var col = subCount == 1 ? i : Math.floor(i / subCount);
subColIdx = subCount == 1 ? 0 : i - col * subCount;
domStyle.set(div, {width: colW, left: ((subColIdx * 100)/subCount)+"%"});
domClass[subColIdx<subCount-1 && subCount !== 1?"add":"remove"](div, "subColumn");
domClass.add(div, this.subColumns[subColIdx]);
this._setText(div, this.subColumnLabelFunc(this.subColumns[subColIdx]));
}, this);
}, this);
var d = renderData.dates[i];
this.styleSubColumnHeaderCell(td, d, renderData);
}, this);
},
subColumnLabelFunc: function(value){
// summary:
// Computes the label for a sub column from the subColumns property.
// By default, return the value.
return value;
},
styleSubColumnHeaderCell: function(node, date, renderData){
// summary:
// Styles the CSS classes to the node that displays a sub column header cell.
// By default this method is not setting anythin:
// node: Node
// The DOM node that displays the column in the grid.
// subColumnIndex: Integer
// The cub column index.
// renderData: Object
// The render data.
// tags:
// protected
domClass.add(node, this._cssDays[date.getDay()]);
if(this.isToday(date)){
domClass.add(node, "dojoxCalendarToday");
} else if(this.isWeekEnd(date)){
domClass.add(node, "dojoxCalendarWeekend");
}
},
_addMinutesClasses: function(node, minutes){
switch(minutes){
case 0:
domClass.add(node, "hour");
break;
case 30:
domClass.add(node, "halfhour");
break;
case 15:
case 45:
domClass.add(node, "quarterhour");
break;
}
},
_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;
}
if(this._rowHeaderLabelContainer == null){
this._rowHeaderLabelContainer = domConstruct.create("div", {"class": "dojoxCalendarRowHeaderLabelContainer"}, this.rowHeader);
}
domStyle.set(rowHeaderTable, "height", renderData.sheetHeight + "px");
var tbodies = query("tbody", rowHeaderTable);
var tbody, tr, td;
if (tbodies.length == 1){
tbody = tbodies[0];
}else{
tbody = domConstruct.create("tbody", null, rowHeaderTable);
}
var nbRows = Math.floor(60 / renderData.rowHeaderGridSlotDuration) * renderData.hourCount;
var count = nbRows -
(oldRenderData ? Math.floor(60 / oldRenderData.rowHeaderGridSlotDuration) * oldRenderData.hourCount : 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);
}
}else{
count = -count;
// deletion of existing nodes
for(var i=0; i < count; i++){
tbody.removeChild(tbody.lastChild);
}
}
// fill labels
var rd = this.renderData;
var size = Math.ceil(renderData.hourSize / (60 / renderData.rowHeaderGridSlotDuration));
var d = new Date(2000, 0, 1, 0, 0, 0);
query("tr", rowHeaderTable).forEach(function(tr, i){
var td = query("td", tr)[0];
td.className = "";
domStyle.set(tr, "height", (has("ie") == 7)?size-2*(60 / renderData.rowHeaderGridSlotDuration):size + "px");
var h = renderData.minHours + (i * this.renderData.rowHeaderGridSlotDuration) / 60;
var m = (i * this.renderData.rowHeaderGridSlotDuration) % 60;
this.styleRowHeaderCell(td, h, m, rd);
this._addMinutesClasses(td, m);
}, this);
var lc = this._rowHeaderLabelContainer;
count = (Math.floor(60 / this.rowHeaderLabelSlotDuration) * renderData.hourCount) - lc.childNodes.length;
var span;
if(count>0){ // creation
for(var i=0; i < count; i++){
span = domConstruct.create("span", null, lc);
domClass.add(span, "dojoxCalendarRowHeaderLabel");
}
}else{
count = -count;
// deletion of existing nodes
for(var i=0; i < count; i++){
lc.removeChild(lc.lastChild);
}
}
size = Math.ceil(renderData.hourSize / (60 / this.rowHeaderLabelSlotDuration));
query(">span", lc).forEach(function(span, i){
d.setHours(0);
d.setMinutes(renderData.minHours * 60 + (i*this.rowHeaderLabelSlotDuration));
this._configureRowHeaderLabel(span, d, i, size*i, rd);
}, this);
},
_configureRowHeaderLabel: function(node, d, index, pos, renderData){
// summary:
// Configures the label of a row header cell.
// node: DOMNode
// The DOM node that is the parent of the label.
// d:Date
// A date object that contains the hours and minutes displayed by this row header cell.
// index: Integer
// The index of this row header cell
// pos: Integer
// The computed position of the row header cell
// renderData: Object
// The render data.
this._setText(node, this._formatRowHeaderLabel(d));
domStyle.set(node, "top", (pos + (index==0?this.rowHeaderFirstLabelOffset:this.rowHeaderLabelOffset))+"px");
var h = renderData.minHours + (index * this.rowHeaderLabelSlotDuration) / 60;
var m = (index * this.rowHeaderLabelSlotDuration) % 60;
domClass.remove(node, ["hour", "halfhour", "quarterhour"]);
this._addMinutesClasses(node, m);
this.styleRowHeaderCell(node, h, m, renderData);
},
styleRowHeaderCell: function(node, h, m, 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.
// h: Integer
// The time of day displayed by this row header cell.
// 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;
}
domStyle.set(table, "height", renderData.sheetHeight + "px");
var nbRows = Math.floor(60 / renderData.slotDuration) * renderData.hourCount;
var rowDiff = nbRows -
(oldRenderData ? Math.floor(60 / oldRenderData.slotDuration) * oldRenderData.hourCount : 0);
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 = nbRows;
addRows = true;
}
}
var tbodies = query("tbody", table);
var tbody;
if (tbodies.length == 1){
tbody = tbodies[0];
}else{
tbody = domConstruct.create("tbody", null, table);
}
// Build time slots (lines) HTML structure (incremental)
if(addRows){ // creation
for(var i=0; i<rowDiff; i++){
domConstruct.create("tr", null, tbody);
}
}else{ // deletion
rowDiff = -rowDiff;
for(var i=0; i<rowDiff; i++){
tbody.removeChild(tbody.lastChild);
}
}
var rowIndex = Math.floor(60 / renderData.slotDuration) * renderData.hourCount - rowDiff;
var addCols = addRows || colDiff >0;
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<len; i++){
domConstruct.create("td", null, tr);
}
}else{ // deletion
for(var i=0; i<colDiff; i++){
tr.removeChild(tr.lastChild);
}
}
});
// Set the CSS classes
query("tr", table).forEach(function (tr, i){
domStyle.set(tr, "height", renderData.slotSize + "px");
if(i == 0){
domClass.add(tr, "first-child");
}else if(i == nbRows-1){
domClass.add(tr, "last-child");
}
// the minutes part of the time of day displayed by the current tr
var m = (i * this.renderData.slotDuration) % 60;
var h = this.minHours + Math.floor((i * this.renderData.slotDuration) / 60);
query("td", tr).forEach(function (td, col){
td.className = "";
if(col == 0){
domClass.add(td, "first-child");
}else if(col == this.renderData.columnCount-1){
domClass.add(td, "last-child");
}
var d = renderData.dates[col];
this.styleGridCell(td, d, h, m, renderData);
this._addMinutesClasses(td, m);
}, this);
}, this);
},
// styleGridCellFunc: Function
// Custom function to customize the appearance of a grid cell by installing custom CSS class on the node.
// The signature of the function must be the same then the styleGridCell one.
// By default the defaultStyleGridCell function is used.
styleGridCellFunc: null,
defaultStyleGridCell: function(node, date, hours, minutes, renderData){
// summary:
// Styles the CSS classes to the node that displays a cell.
// By default this method is setting:
// - "dojoxCalendarToday" class name if the date displayed is the current date,
// - "dojoxCalendarWeekend" if the date represents a weekend,
// - the CSS class corresponding of the displayed day of week ("Sun", "Mon" and so on),
// - the CSS classes corresponfing to the time of day (e.g. "H14" and "M30" for for 2:30pm).
// node: Node
// The DOM node that displays the cell in the grid.
// date: Date
// The date displayed by this cell.
// hours: Integer
// The hours part of time of day displayed by the start of this cell.
// minutes: Integer
// The minutes part of time of day displayed by the start of this cell.
// renderData: Object
// The render data object.
// tags:
// protected
domClass.add(node, [this._cssDays[date.getDay()], "H"+hours, "M"+minutes]);
if(this.isToday(date)){
return domClass.add(node, "dojoxCalendarToday");
} else if(this.isWeekEnd(date)){
return domClass.add(node, "dojoxCalendarWeekend");
}
},
styleGridCell: function(node, date, hours, minutes, 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 column
// renderData: Object
// The render data object.
// tags:
// protected
if(this.styleGridCellFunc){
this.styleGridCellFunc(node, date, hours, minutes, renderData);
}else{
this.defaultStyleGridCell(node, date, hours, minutes, 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 bgCols = [], decoCols = [];
domStyle.set(table, "height", renderData.sheetHeight + "px");
var oldCount = oldRenderData ? oldRenderData.columnCount : 0;
var count = renderData.columnCount - oldCount;
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);
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 trs = query("tr", table);
var tbody, tr, td;
if (tbodies.length == 1){
tbody = tbodies[0];
}else{
tbody = domConstruct.create("tbody", null, table);
}
if (trs.length == 1){
tr = trs[0];
}else{
tr = domConstruct.create("tr", null, tbody);
}
var subCount = renderData.subColumnCount;
// Build HTML structure (incremental)
if(count>0){ // creation
for(var i=0; i < count; i++){
td = domConstruct.create("td", null, tr);
domConstruct.create("div", {"className": "dojoxCalendarContainerColumn"}, td);
}
}else{ // deletion
count = -count;
for(var i=0; i < count; i++){
tr.removeChild(tr.lastChild);
}
}
query("td", table).forEach(function(td, i){
query(".dojoxCalendarContainerColumn", td).forEach(function(div, i){
domStyle.set(div, "height", renderData.sheetHeight + "px");
var count = query(".dojoxCalendarSubContainerColumn", td).length - subCount;
if(count != 0){
var len = div.childNodes.length;
for(var i=0; i<len; i++){
div.removeChild(div.lastChild);
}
for(var j=0; j<subCount; j++){
var subdiv = domConstruct.create("div", {"className": "dojoxCalendarSubContainerColumn"}, div);
domConstruct.create("div", {"className": "dojoxCalendarDecorationContainerColumn"}, subdiv);
domConstruct.create("div", {"className": "dojoxCalendarEventContainerColumn"}, subdiv);
}
}
}, this);
var colW = (100/subCount) + "%";
query(".dojoxCalendarSubContainerColumn", td).forEach(function(div, i){
var col = subCount == 1 ? i : Math.floor(i / subCount);
subColIdx = subCount == 1 ? 0 : i - col * subCount;
domStyle.set(div, {width: colW, left: ((subColIdx * 100)/subCount)+"%"});
domClass[subColIdx<subCount-1 && subCount !== 1?"add":"remove"](div, "subColumn");
query(".dojoxCalendarEventContainerColumn", div).forEach(function(eventContainer, i){
bgCols.push(eventContainer);
}, this);
query(".dojoxCalendarDecorationContainerColumn", div).forEach(function(decoContainer, i){
decoCols.push(decoContainer);
}, this);
}, this);
}, this);
renderData.cells = bgCols;
renderData.decorationCells = decoCols;
},
// showTimeIndicator: Boolean
// Whether show or not an indicator (default a red line) at the current time.
showTimeIndicator: true,
// timeIndicatorRefreshInterval: Integer
// Maximal interval between two refreshes of time indicator, in milliseconds.
timeIndicatorRefreshInterval: 60000,
_setShowTimeIndicatorAttr: function(value){
this._set("showTimeIndicator", value);
this._layoutTimeIndicator(this.renderData);
},
_layoutTimeIndicator: function(renderData){
if(!renderData){
return;
}
if(this.showTimeIndicator){
var now = new renderData.dateClassObj();
var visible = this.isOverlapping(renderData, renderData.startTime, renderData.endTime, now, now) &&
now.getHours() >= this.get("minHours") &&
(now.getHours()*60+now.getMinutes() < this.get("maxHours")*60);
if(visible){
if(!this._timeIndicator){
this._timeIndicator = domConstruct.create("div",
{"className": "dojoxCalendarTimeIndicator"});
}
var node = this._timeIndicator;
for(var column=0; column<this.renderData.columnCount; column++){
if(this.isSameDay(now, this.renderData.dates[column])){
break;
}
}
var top = this.computeProjectionOnDate(renderData, this.floorToDay(now), now, renderData.sheetHeight);
if(top != renderData.sheetHeight){
domStyle.set(node, {top: top+"px", display: "block"});
var parentNode = renderData.cells[column*renderData.subColumnCount].parentNode.parentNode;
if(parentNode != node.parentNode){
if(node.parentNode != null){
node.parentNode.removeChild(node);
}
parentNode.appendChild(node);
}
if(this._timeIndicatorTimer == null){
this._timeIndicatorTimer = setInterval(lang.hitch(this, function(){
this._layoutTimeIndicator(this.renderData);
}), this.timeIndicatorRefreshInterval);
}
return;
}
}
}
// not visible or specifically not shown fallback
if(this._timeIndicatorTimer){
clearInterval(this._timeIndicatorTimer);
this._timeIndicatorTimer = null;
}
if(this._timeIndicator){
domStyle.set(this._timeIndicator, "display", "none");
}
},
beforeDeactivate: function(){
if(this._timeIndicatorTimer){
clearInterval(this._timeIndicatorTimer);
this._timeIndicatorTimer = null;
}
},
///////////////////////////////////////////////////////////////
//
// Layout
//
///////////////////////////////////////////////////////////////
_overlapLayoutPass2: function(lanes){
// summary:
// Second pass of the overlap layout (optional). Compute the extent of each layout item.
// lanes:
// The array of lanes.
// tags:
// private
var i,j,lane, layoutItem;
// last lane, no extent possible
lane = lanes[lanes.length-1];
for(j = 0; j < lane.length; j++){
lane[j].extent = 1;
}
for(i=0; i<lanes.length-1; i++){
lane = lanes[i];
for(var j=0; j<lane.length; j++){
layoutItem = lane[j];
// if item was already overlapping another one there is no extent possible.