UNPKG

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,862 lines (1,568 loc) 81.3 kB
define([ "dojo/_base/declare", "dojo/_base/lang", "dojo/_base/array", "dojo/_base/window", "dojo/_base/event", "dojo/_base/html", "dojo/sniff", "dojo/query", "dojo/dom", "dojo/dom-style", "dojo/dom-class", "dojo/dom-construct", "dojo/dom-geometry", "dojo/on", "dojo/date", "dojo/date/locale", "dojo/when", "dijit/_WidgetBase", "dojox/widget/_Invalidating", "dojox/widget/Selection", "./time", "./StoreMixin", "./StoreManager", "./RendererManager"], function( declare, lang, arr, win, event, html, has, query, dom, domStyle, domClass, domConstruct, domGeometry, on, date, locale, when, _WidgetBase, _Invalidating, Selection, timeUtil, StoreMixin, StoreManager, RendererManager){ /*===== var __GridClickEventArgs = { // summary: // The event dispatched when the grid is clicked or double-clicked. // date: Date // The start of the previously displayed time interval, if any. // triggerEvent: Event // The event at the origin of this event. }; =====*/ /*===== var __ItemMouseEventArgs = { // summary: // The event dispatched when an item is clicked, double-clicked or context-clicked. // item: Object // The item clicked. // renderer: dojox/calendar/_RendererMixin // The item renderer clicked. // triggerEvent: Event // The event at the origin of this event. }; =====*/ /*===== var __itemEditingEventArgs = { // summary: // An item editing event. // item: Object // The render item that is being edited. Set/get the startTime and/or endTime properties to customize editing behavior. // storeItem: Object // The real data from the store. DO NOT change properties, but you may use properties of this item in the editing behavior logic. // editKind: String // Kind of edit: "resizeBoth", "resizeStart", "resizeEnd" or "move". // dates: Date[] // The computed date/time of the during the event editing. One entry per edited date (touch use case). // startTime: Date? // The start time of data item. // endTime: Date? // The end time of data item. // sheet: String // For views with several sheets (columns view for example), the sheet when the event occurred. // source: dojox/calendar/ViewBase // The view where the event occurred. // eventSource: String // The device that triggered the event. This property can take the following values: // // - "mouse", // - "keyboard", // - "touch" // triggerEvent: Event // The event at the origin of this event. }; =====*/ /*===== var __rendererLifecycleEventArgs = { // summary: // An renderer lifecycle event. // renderer: Object // The renderer. // source: dojox/calendar/ViewBase // The view where the event occurred. // item:Object? // The item that will be displayed by the renderer for the "rendererCreated" and "rendererReused" events. }; =====*/ return declare("dojox.calendar.ViewBase", [_WidgetBase, StoreMixin, _Invalidating, Selection], { // summary: // The dojox.calendar.ViewBase widget is the base of calendar view widgets // datePackage: Object // JavaScript namespace to find Calendar routines. Uses Gregorian Calendar routines at dojo.date by default. datePackage: date, _calendar: "gregorian", // viewKind: String // Kind of the view. Used by the calendar widget to determine how to configure the view. viewKind: null, // _layoutStep: [protected] Integer // The number of units displayed by a visual layout unit (i.e. a column or a row) _layoutStep: 1, // _layoutStep: [protected] Integer // The unit displayed by a visual layout unit (i.e. a column or a row) _layoutUnit: "day", // resizeCursor: String // CSS value to apply to the cursor while resizing an item renderer. resizeCursor: "n-resize", // formatItemTimeFunc: Function // Optional function to format the time of day of the item renderers. // The function takes the date, the render data object, the view and the data item as arguments and returns a String. formatItemTimeFunc: null, _cssDays: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"], _getFormatItemTimeFuncAttr: function(){ if(this.formatItemTimeFunc){ return this.formatItemTimeFunc; } if(this.owner != null){ return this.owner.get("formatItemTimeFunc"); } }, // The listeners added by the view itself. _viewHandles: null, // doubleTapDelay: Integer // The maximum time amount in milliseconds between to touchstart events that trigger a double-tap event. doubleTapDelay: 300, constructor: function(/*Object*/ args){ args = args || {}; this._calendar = args.datePackage ? args.datePackage.substr(args.datePackage.lastIndexOf(".")+1) : this._calendar; this.dateModule = args.datePackage ? lang.getObject(args.datePackage, false) : date; this.dateClassObj = this.dateModule.Date || Date; this.dateLocaleModule = args.datePackage ? lang.getObject(args.datePackage+".locale", false) : locale; this._viewHandles = []; this.storeManager = new StoreManager({owner: this, _ownerItemsProperty: "items"}); this.storeManager.on("layoutInvalidated", lang.hitch(this, this._refreshItemsRendering)); this.storeManager.on("dataLoaded", lang.hitch(this, function(items){ this.set("items", items); })); this.storeManager.on("renderersInvalidated", lang.hitch(this, function(item){ this.updateRenderers(item); })); this.rendererManager = new RendererManager({owner: this}); this.rendererManager.on("rendererCreated", lang.hitch(this, this._onRendererCreated)); this.rendererManager.on("rendererReused", lang.hitch(this, this._onRendererReused)); this.rendererManager.on("rendererRecycled", lang.hitch(this, this._onRendererRecycled)); this.rendererManager.on("rendererDestroyed", lang.hitch(this, this._onRendererDestroyed)); this.decorationStoreManager = new StoreManager({owner: this, _ownerItemsProperty: "decorationItems"}); this.decorationStoreManager.on("layoutInvalidated", lang.hitch(this, this._refreshDecorationItemsRendering)); this.decorationStoreManager.on("dataLoaded", lang.hitch(this, function(items){ this.set("decorationItems", items); })); this.decorationRendererManager = new RendererManager({owner: this}); this._setupDayRefresh(); }, destroy: function(preserveDom){ this.rendererManager.destroy(); this.decorationRendererManager.destroy(); while(this._viewHandles.length > 0){ this._viewHandles.pop().remove(); } this.inherited(arguments); }, _setupDayRefresh: function(){ // Refresh the view when the current day changes. var now = this.newDate(new Date()); var d = timeUtil.floor(now, "day", 1, this.dateClassObj); var d = this.dateModule.add(d, "day", 1); // manages DST at 24h if(d.getHours() == 23){ d = this.dateModule.add(d, "hour", 2); // go to 1am }else{ d = timeUtil.floorToDay(d, true, this.dateClassObj); } setTimeout(lang.hitch(this, function(){ if(!this._isEditing){ this.refreshRendering(true); // recursive refresh } this._setupDayRefresh(); }), d.getTime()-now.getTime() + 5000); // add 5 seconds to be sure to be tomorrow }, resize: function(changeSize){ // summary: // Function to call when the view is resized. // If the view is in a Dijit container or in a Dojo mobile container, it will be automatically called. // On other use cases, this method must called when the window is resized and/or when the orientation has changed. if(changeSize){ domGeometry.setMarginBox(this.domNode, changeSize); } }, // view lifecycle methods beforeActivate: function(){ // summary: // Function invoked just before the view is displayed by the calendar. // tags: // protected }, afterActivate: function(){ // summary: // Function invoked just after the view is displayed by the calendar. // tags: // protected }, beforeDeactivate: function(){ // summary: // Function invoked just before the view is hidden or removed by the calendar. // tags: // protected }, afterDeactivate: function(){ // summary: // Function invoked just after the view is the view is hidden or removed by the calendar. // tags: // protected }, _getTopOwner: function(){ // summary: // Returns the top owner: the calendar or the parent view. var p = this; while(p.owner != undefined){ p = p.owner; } return p; }, _createRenderData: function(){ // summary: // Creates the object that contains all the data needed to render this widget. // tags: // protected }, _validateProperties: function(){ // summary: // Validates the widget properties before the rendering pass. // tags: // protected }, _setText: function(node, text, allowHTML){ // summary: // Creates a text node under the parent node after having removed children nodes if any. // node: Node // The node that will contain the text node. // text: String // The text to set to the text node. if(text != null){ if(!allowHTML && node.hasChildNodes()){ // span > textNode node.childNodes[0].childNodes[0].nodeValue = text; }else{ while(node.hasChildNodes()){ node.removeChild(node.lastChild); } var tNode = win.doc.createElement("span"); if(has("dojo-bidi")){ this.applyTextDir(tNode, text); } if(allowHTML){ tNode.innerHTML = text; }else{ tNode.appendChild(win.doc.createTextNode(text)); } node.appendChild(tNode); } } }, isAscendantHasClass: function(node, ancestor, className){ // summary: // Determines if a node has an ascendant node that has the css class specified. // node: Node // The DOM node. // ancestor: Node // The ancestor node used to limit the search in hierarchy. // className: String // The css class name. // returns: Boolean while(node != ancestor && node != document){ if(domClass.contains(node, className)){ return true; } node = node.parentNode; } return false; }, isWeekEnd: function(date){ // summary: // Determines whether the specified date is a week-end. // This method is using dojo.date.locale.isWeekend() method as // dojox.date.XXXX calendars are not supporting this method. // date: Date // The date to test. return locale.isWeekend(date); }, getWeekNumberLabel: function(date){ // summary: // Returns the week number string from dojo.date.locale.format() method as // dojox.date.XXXX calendar are not supporting the "w" pattern. // date: Date // The date to format. if(date.toGregorian){ date = date.toGregorian(); } return locale.format(date, { selector: "date", datePattern: "w"}); }, addAndFloor: function(date, unit, steps){ // date must be floored!! // unit >= day var d = this.dateModule.add(date, unit, steps); if(d.getHours() == 23){ d = this.dateModule.add(d, "hour", 2); // go to 1am }else{ d = timeUtil.floorToDay(d, true, this.dateClassObj); } return d; }, floorToDay: function(date, reuse){ // summary: // Floors the specified date to the start of day. // date: Date // The date to floor. // reuse: Boolean // Whether use the specified instance or create a new one. Default is false. // returns: Date return timeUtil.floorToDay(date, reuse, this.dateClassObj); }, floorToMonth: function(date, reuse){ // summary: // Floors the specified date to the start of the date's month. // date: Date // The date to floor. // reuse: Boolean // Whether use the specified instance or create a new one. Default is false. // returns: Date return timeUtil.floorToMonth(date, reuse, this.dateClassObj); }, floorDate: function(date, unit, steps, reuse){ // summary: // floors the date to the unit. // date: Date // The date/time to floor. // unit: String // The unit. Valid values are "minute", "hour", "day". // steps: Integer // For "day" only 1 is valid. // reuse: Boolean // Whether use the specified instance or create a new one. Default is false. // returns: Date return timeUtil.floor(date, unit, steps, reuse, this.dateClassObj); }, isToday: function(date){ // summary: // Returns whether the specified date is in the current day. // date: Date // The date to test. // renderData: Object // The current renderData // returns: Boolean return timeUtil.isToday(date, this.dateClassObj); }, isStartOfDay: function(d){ // summary: // Tests if the specified date represents the starts of day. // d:Date // The date to test. // returns: Boolean return timeUtil.isStartOfDay(d, this.dateClassObj, this.dateModule); }, isOverlapping: function(renderData, start1, end1, start2, end2, includeLimits){ // summary: // Computes if the first time range defined by the start1 and end1 parameters // is overlapping the second time range defined by the start2 and end2 parameters. // renderData: Object // The render data. // start1: Date // The start time of the first time range. // end1: Date // The end time of the first time range. // start2: Date // The start time of the second time range. // end2: Date // The end time of the second time range. // includeLimits: Boolean // Whether include the end time or not. // returns: Boolean return timeUtil.isOverlapping(renderData, start1, end1, start2, end2, includeLimits); }, computeRangeOverlap: function(renderData, start1, end1, start2, end2, includeLimits){ // summary: // Computes the overlap time range of the time ranges. // Returns a vector of Date with at index 0 the start time and at index 1 the end time. // renderData: Object. // The render data. // start1: Date // The start time of the first time range. // end1: Date // The end time of the first time range. // start2: Date // The start time of the second time range. // end2: Date // The end time of the second time range. // includeLimits: Boolean // Whether include the end time or not. // returns: Date[] var cal = renderData.dateModule; if(start1 == null || start2 == null || end1 == null || end2 == null){ return null; } var comp1 = cal.compare(start1, end2); var comp2 = cal.compare(start2, end1); if(includeLimits){ if(comp1 == 0 || comp1 == 1 || comp2 == 0 || comp2 == 1){ return null; } } else if(comp1 == 1 || comp2 == 1){ return null; } return [ this.newDate(cal.compare(start1, start2)>0 ? start1: start2, renderData), this.newDate(cal.compare(end1, end2)>0 ? end2: end1, renderData) ]; }, isSameDay : function(date1, date2){ // summary: // Tests if the specified dates are in the same day. // date1: Date // The first date. // date2: Date // The second date. // returns: Boolean if(date1 == null || date2 == null){ return false; } return date1.getFullYear() == date2.getFullYear() && date1.getMonth() == date2.getMonth() && date1.getDate() == date2.getDate(); }, computeProjectionOnDate: function(renderData, refDate, date, max){ // summary: // Computes the time to pixel projection in a day. // renderData: Object // The render data. // refDate: Date // The reference date that defines the destination date. // date: Date // The date to project. // max: Integer // The size in pixels of the representation of a day. // tags: // protected // returns: Number var cal = renderData.dateModule; var minH = renderData.minHours; var maxH = renderData.maxHours; if(max <= 0 || cal.compare(date, refDate) == -1){ return 0; } var gt = function(d){ return d.getHours() * 3600 + d.getMinutes() * 60 + d.getSeconds(); }; var referenceDate = this.floorToDay(refDate, false, renderData); if(date.getDate() != referenceDate.getDate()){ if(date.getMonth() == referenceDate.getMonth()){ if(date.getDate() < referenceDate.getDate()){ return 0; } else if(date.getDate() > referenceDate.getDate() && maxH < 24){ return max; } }else{ if(date.getFullYear() == referenceDate.getFullYear()){ if(date.getMonth() < referenceDate.getMonth()){ return 0; } else if(date.getMonth() > referenceDate.getMonth()){ return max; } }else{ if(date.getFullYear() < referenceDate.getFullYear()){ return 0; } else if(date.getFullYear() > referenceDate.getFullYear()){ return max; } } } } var res; var ONE_DAY = 86400; // 24h x 60m x 60s if(this.isSameDay(refDate, date) || maxH > 24){ var d = lang.clone(refDate); var minTime = 0; if(minH != null && minH != 0){ d.setHours(minH); minTime = gt(d); } d = lang.clone(refDate); d.setHours(maxH); var maxTime; if(maxH == null || maxH == 24){ maxTime = ONE_DAY; }else if(maxH > 24){ maxTime = ONE_DAY + gt(d); }else{ maxTime = gt(d); } //precision is the second //use this API for daylight time issues. var delta = 0; if(maxH > 24 && refDate.getDate() != date.getDate()){ delta = ONE_DAY + gt(date); }else{ delta = gt(date); } if(delta < minTime){ return 0; } if(delta > maxTime){ return max; } delta -= minTime; res = (max * delta)/(maxTime - minTime); }else{ if(date.getDate() < refDate.getDate() && date.getMonth() == refDate.getMonth()){ return 0; } var d2 = this.floorToDay(date); var dp1 = renderData.dateModule.add(refDate, "day", 1); dp1 = this.floorToDay(dp1, false, renderData); if(cal.compare(d2, refDate) == 1 && cal.compare(d2, dp1) == 0 || cal.compare(d2, dp1) == 1){ res = max; }else{ res = 0; } } return res; }, getTime: function(e, x, y, touchIndex){ // summary: // Returns the time displayed at the specified point by this component. // e: Event // Optional mouse event. // x: Number // Position along the x-axis with respect to the sheet container used if event is not defined. // y: Number // Position along the y-axis with respect to the sheet container (scroll included) used if event is not defined. // touchIndex: Integer // If parameter 'e' is not null and a touch event, the index of the touch to use. // returns: Date return null; }, getSubColumn: function(e, x, y, touchIndex){ // summary: // Returns the sub column at the specified point by this component. // e: Event // Optional mouse event. // x: Number // Position along the x-axis with respect to the sheet container used if event is not defined. // y: Number // Position along the y-axis with respect to the sheet container (scroll included) used if event is not defined. // touchIndex: Integer // If parameter 'e' is not null and a touch event, the index of the touch to use. // returns: Object return null; }, getSubColumnIndex: function(value){ // summary: // Returns the sub column index that has the specified value, if any. -1 otherwise. // value: String // The sub column index. if(this.subColumns){ for(var i=0; i<this.subColumns.length; i++){ if(this.subColumns[i] == value){ return i; } } } return -1; }, newDate: function(obj){ // summary: // Creates a new Date object. // obj: Object // This object can have several values: // // - the time in milliseconds since gregorian epoch. // - a Date instance // returns: Date return timeUtil.newDate(obj, this.dateClassObj); }, _isItemInView: function(item){ // summary: // Computes whether the specified item is entirely in the view or not. // item: Object // The item to test // returns: Boolean var rd = this.renderData; var cal = rd.dateModule; if(cal.compare(item.startTime, rd.startTime) == -1){ return false; } return cal.compare(item.endTime, rd.endTime) != 1; }, _ensureItemInView: function(item){ // summary: // If needed, moves the item to be entirely in view. // item: Object // The item to test // returns: Boolean // Whether the item has been moved to be in view or not. // tags: // protected var rd = this.renderData; var cal = rd.dateModule; var duration = Math.abs(cal.difference(item.startTime, item.endTime, "millisecond")); var fixed = false; if(cal.compare(item.startTime, rd.startTime) == -1){ item.startTime = rd.startTime; item.endTime = cal.add(item.startTime, "millisecond", duration); fixed = true; }else if(cal.compare(item.endTime, rd.endTime) == 1){ item.endTime = rd.endTime; item.startTime = cal.add(item.endTime, "millisecond", -duration); fixed = true; } return fixed; }, ///////////////////////////////////////////////////////// // // Scrollable // ///////////////////////////////////////////////////////// // scrollable: Boolean // Indicates whether the view can be scrolled or not. scrollable: true, // autoScroll: Boolean // Indicates whether the view can be scrolled automatically. // Auto scrolling is used when moving focus to a non visible renderer using keyboard // and while editing an item. autoScroll: true, _autoScroll: function(gx, gy, orientation){ // summary: // Starts or stops the auto scroll according to the mouse cursor position during an item editing. // gx: Integer // The position of the mouse cursor along the x-axis. // gy: Integer // The position of the mouse cursor along the y-axis. // tags: // extension return false; }, // scrollMethod: String // Method used to scroll the view, for example the scroll of column view. // Valid value are: // // - "auto": let the view decide (default), // - "css": use css 3d transform, // - "dom": use the scrollTop property. scrollMethod: "auto", _setScrollMethodAttr: function(value){ if(this.scrollMethod != value){ this.scrollMethod = value; // reset if(this._domScroll !== undefined){ if(this._domScroll){ domStyle.set(this.sheetContainer, this._cssPrefix+"transform", "translateY(0px)"); }else{ this.scrollContainer.scrollTop = 0; } } delete this._domScroll; var pos = this._getScrollPosition(); delete this._scrollPos; this._setScrollPosition(pos); } }, _startAutoScroll: function(step){ // summary: // Starts the auto scroll of the view (if it's scrollable). Used only during editing. // tags: // protected var sp = this._scrollProps; if(!sp){ sp = this._scrollProps = {}; } sp.scrollStep = step; if (!sp.isScrolling){ sp.isScrolling = true; sp.scrollTimer = setInterval(lang.hitch(this, this._onScrollTimer_tick), 10); } }, _stopAutoScroll: function(){ // summary: // Stops the auto scroll of the view (if it's scrollable). Used only during editing. // tags: // protected var sp = this._scrollProps; if (sp && sp.isScrolling) { clearInterval(sp.scrollTimer); sp.scrollTimer = null; } this._scrollProps = null; }, _onScrollTimer_tick: function(pos){ }, _scrollPos: 0, _hscrollPos: 0, getCSSPrefix: function(){ // summary: // Utility method that return the specific CSS prefix // for non standard CSS properties. Ex: -moz-border-radius. if(has("ie")){ return "-ms-"; } if(has("webkit")){ return "-webkit-"; } if(has("mozilla")){ return "-moz-"; } if(has("opera")){ return "-o-"; } return ""; }, // _hScrollNodes: DOMNodes[] // Array of nodes that will be scrolled horizontally. // Must be set by sub class on buildRendering. _hScrollNodes: null, _setScrollPositionBase: function(pos, vertical){ // summary: // Sets the scroll position (if the view is scrollable), using the scroll method defined. // tags: // protected if(vertical && this._scrollPos == pos || !vertical && this._hScrollPos == pos){ return; } // determine scroll method once. if(this._domScroll === undefined){ var sm = this.get("scrollMethod"); if(sm === "auto"){ this._domScroll = !has("ios") && !has("android") && !has("webkit"); }else{ this._domScroll = sm === "dom"; } } var max = 0; if(vertical){ var containerSize = domGeometry.getMarginBox(this.scrollContainer); var sheetSize = domGeometry.getMarginBox(this.sheetContainer); max = sheetSize.h - containerSize.h; }else{ var gridSize = domGeometry.getMarginBox(this.grid); var gridTableSize = domGeometry.getMarginBox(this.gridTable); max = gridTableSize.w - gridSize.w; } if(pos < 0){ pos = 0; }else if(pos > max){ pos = max; } if(vertical){ this._scrollPos = pos; }else{ this._hScrollPos = pos; } var rtl = !this.isLeftToRight(); if(this._domScroll){ if(vertical){ this.scrollContainer.scrollTop = pos; }else{ arr.forEach(this._hScrollNodes, function(elt){ domStyle.set(elt, "left", ((rtl?1:-1) * pos) + "px"); }, this); } }else{ if(!this._cssPrefix){ this._cssPrefix = this.getCSSPrefix(); } var cssProp = this._cssPrefix+"transform"; if(vertical){ domStyle.set(this.sheetContainer, cssProp, "translateY(-"+pos+"px)"); }else{ var css = "translateX("+(rtl?"":"-")+pos+"px)"; arr.forEach(this._hScrollNodes, function(elt){ domStyle.set(elt, cssProp, css); }, this); } } }, _setScrollPosition: function(pos){ // summary: // Sets the verical scroll position (if the view is scrollable), using the scroll method defined. // tags: // protected this._setScrollPositionBase(pos, true); }, _getScrollPosition: function(){ // summary: // Returns the vertical scroll position (if the view is scrollable), using the scroll method defined. // tags: // protected return this._scrollPos; }, _setHScrollPosition: function(pos){ // summary: // Sets the horizontal scroll position (if the view is scrollable), using the scroll method defined. // tags: // protected this._setScrollPositionBase(pos, false); }, _setHScrollPositionImpl: function(pos, useDom, cssProperty){ // summary: // Sets the horizontal scroll position on sub elements (if the view is scrollable), using the scroll method defined. // Important: must be implemented by sub classes and not called directly. Use _setHScrollPosition() method instead. // tags: // private var css = useDom ? null : "translateX(-"+pos+"px)"; arr.forEach(this._hScrollNodes, function(elt){ if(useDom){ elt.scrollLeft = pos; domStyle.set(elt, "left", (-pos) + "px"); }else{ domStyle.set(elt, cssProp, css); } }, this); }, _hScrollPos: 0, _getHScrollPosition: function(){ // summary: // Returns the horizontal scroll position (if the view is scrollable), using the scroll method defined. // tags: // protected return this._hScrollPos; }, scrollView: function(dir){ // summary: // If the view is scrollable, scrolls it vertically to the specified direction. // dir: Integer // Direction of the scroll. Valid values are -1 and 1. // tags: // extension }, ensureVisibility: function(start, end, margin, visibilityTarget, 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: int // 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. // tags: // extension }, //////////////////////////////////////////////////////// // // Store & Items // //////////////////////////////////////////////////////// _getStoreAttr: function(){ if(this.owner){ return this.owner.get("store"); } return this.store; }, _setItemsAttr: function(value){ this._set("items", value); this.displayedItemsInvalidated = true; }, _refreshItemsRendering: function(){ var rd = this.renderData; this._computeVisibleItems(rd); this._layoutRenderers(rd); }, _refreshDecorationItemsRendering: function(){ var rd = this.renderData; this._computeVisibleItems(rd); this._layoutDecorationRenderers(rd); }, invalidateLayout: function(){ // summary: // Triggers a re-layout of the renderers. this._layoutRenderers(this.renderData); this._layoutDecorationRenderers(this.renderData); }, _setDecorationItemsAttr: function(value){ this._set("decorationItems", value); this.displayedDecorationItemsInvalidated = true; }, _getDecorationStoreAttr: function(){ if(this.owner){ return this.owner.get("decorationStore"); } return this.decorationStore; }, _setDecorationStoreAttr: function(value){ this.decorationStore = value; this.decorationStoreManager.set("store", value); }, //////////////////////////////////////////////////////// // // Layout // //////////////////////////////////////////////////////// computeOverlapping: function(layoutItems, func){ // summary: // Computes the overlap layout of a list of items. A lane and extent properties are added to each layout item. // layoutItems: Object[] // List of layout items, each item must have a start and end properties. // addedPass: Function // Whether computes the extent of each item renderer on free sibling lanes. // returns: Object // tags: // protected if(layoutItems.length == 0){ return { numLanes: 0, addedPassRes: [1] }; } var lanes = []; for(var i=0; i<layoutItems.length; i++){ var layoutItem = layoutItems[i]; this._layoutPass1(layoutItem, lanes); } var addedPassRes = null; if(func){ addedPassRes = lang.hitch(this, func)(lanes); } return { numLanes: lanes.length, addedPassRes: addedPassRes }; }, _layoutPass1: function (layoutItem, lanes){ // summary: // First pass of the overlap layout. Find a lane where the item can be placed or create a new one. // layoutItem: Object // An object that contains a start and end properties at least. // lanes: // The array of lanes. // tags: // protected var stop = true; for(var i=0; i<lanes.length; i++){ var lane = lanes[i]; stop = false; for(var j=0; j<lane.length && !stop; j++){ if(lane[j].start < layoutItem.end && layoutItem.start < lane[j].end){ // one already placed item is overlapping stop = true; lane[j].extent = 1; } } if(!stop){ //we have found a place layoutItem.lane = i; layoutItem.extent = -1; lane.push(layoutItem); return; } } //no place found -> add a lane lanes.push([layoutItem]); layoutItem.lane = lanes.length-1; layoutItem.extent = -1; }, _layoutInterval: function(renderData, index, start, end, items){ // summary: // For each item in the items list: retrieve a renderer, compute its location and size and add it to the DOM. // renderData: Object // The render data. // index: Integer // The index of the interval. // start: Date // The start time of the displayed date interval. // end: Date // The end time of the displayed date interval. // items: Object[] // The list of the items to represent. // tags: // extension }, // layoutPriorityFunction: Function // An optional comparison function use to determine the order the item will be laid out // The function is used to sort an array and must, as any sorting function, take two items // as argument and must return an integer whose sign define order between arguments. // By default, a comparison by start time then end time is used. layoutPriorityFunction: null, _sortItemsFunction: function(a, b){ var res = this.dateModule.compare(a.startTime, b.startTime); if(res == 0){ res = -1 * this.dateModule.compare(a.endTime, b.endTime); } return res; }, _layoutRenderers: function(renderData){ this._layoutRenderersImpl(renderData, this.rendererManager, renderData.items, "dataItems"); }, _layoutDecorationRenderers: function(renderData){ this._layoutRenderersImpl(renderData, this.decorationRendererManager, renderData.decorationItems, "decorationItems"); }, _layoutRenderersImpl: function(renderData, rendererManager, items, itemType){ // summary: // Renders the data items. This method will call the _layoutInterval() method. // renderData: Object // The render data. // tags: // protected if(!items){ return; } // recycle renderers first rendererManager.recycleItemRenderers(); var cal = renderData.dateModule; // Date var startDate = this.newDate(renderData.startTime); // Date and time var startTime = lang.clone(startDate); var endDate; var items = items.concat(); var itemsTemp = [], events; var processing = {}; var index = 0; while(cal.compare(startDate, renderData.endTime) == -1 && items.length > 0){ endDate = this.addAndFloor(startDate, this._layoutUnit, this._layoutStep); var endTime = lang.clone(endDate); if(renderData.minHours){ startTime.setHours(renderData.minHours); } if(renderData.maxHours != undefined && renderData.maxHours != 24){ if(renderData.maxHours < 24){ endTime = cal.add(endDate, "day", -1); } // else > 24 endTime = this.floorToDay(endTime, true, renderData); endTime.setHours(renderData.maxHours - (renderData.maxHours < 24 ? 0 : 24)); } // look for events that overlap the current sub interval events = arr.filter(items, function(item){ var r = this.isOverlapping(renderData, item.startTime, item.endTime, startTime, endTime); if(r){ processing[item.id] = true; itemsTemp.push(item); }else{ if(processing[item.id]){ delete processing[item.id]; }else{ itemsTemp.push(item); } } return r; }, this); items = itemsTemp; itemsTemp = []; // if event are in the current sub interval, layout them if(events.length > 0){ // Sort the item according a sorting function, by default start time then end time comparison are used. events.sort(lang.hitch(this, this.layoutPriorityFunction ? this.layoutPriorityFunction : this._sortItemsFunction)); this._layoutInterval(renderData, index, startTime, endTime, events, itemType); } startDate = endDate; startTime = lang.clone(startDate); index++; } this._onRenderersLayoutDone(this); }, ///////////////////////////////////////////////////////////////// // // Renderers management // //////////////////////////////////////////////////////////////// _recycleItemRenderers: function(remove){ this.rendererManager.recycleItemRenderers(remove); }, getRenderers: function(item){ // summary: // Returns the renderers that are currently used to displayed the speficied item. // Returns an array of objects that contains two properties: // - container: The DOM node that contains the renderer. // - renderer: The dojox.calendar._RendererMixin instance. // Do not keep references on the renderers are they are recycled and reused for other items. // item: Object // The data or render item. // returns: Object[] return this.rendererManager.getRenderers(item); }, // itemToRendererKindFunc: Function // An optional function to associate a kind of renderer ("horizontal", "label" or null) with the specified item. // By default, if an item is lasting more that 24 hours an horizontal item is used, otherwise a label is used. itemToRendererKindFunc: null, _itemToRendererKind: function(item){ // summary: // Associates a kind of renderer with a data item. // item: Object // The data item. // returns: String // tags: // protected if(this.itemToRendererKindFunc){ return this.itemToRendererKindFunc(item); } return this._defaultItemToRendererKindFunc(item); // String }, _defaultItemToRendererKindFunc:function(item){ // tags: // private return null; }, _createRenderer: function(item, kind, rendererClass, cssClass){ // summary: // Creates an item renderer of the specified kind. A renderer is an object with the "container" and "instance" properties. // item: Object // The data item. // kind: String // The kind of renderer. // rendererClass: Object // The class to instantiate to create the renderer. // returns: Object // tags: // protected return this.rendererManager.createRenderer(item, kind, rendererClass, cssClass); }, _onRendererCreated: function(e){ if(e.source == this){ this.onRendererCreated(e); } if(this.owner != null){ this.owner._onRendererCreated(e); } }, onRendererCreated: function(e){ // summary: // Event dispatched when an item renderer has been created. // e: __rendererLifecycleEventArgs // The renderer lifecycle event. // tags: // callback }, _onRendererRecycled: function(e){ if(e.source == this){ this.onRendererRecycled(e); } if(this.owner != null){ this.owner._onRendererRecycled(e); } }, onRendererRecycled: function(e){ // summary: // Event dispatched when an item renderer has been recycled. // e: __rendererLifecycleEventArgs // The renderer lifecycle event. // tags: // callback }, _onRendererReused: function(e){ if(e.source == this){ this.onRendererReused(e); } if(this.owner != null){ this.owner._onRendererReused(e); } }, onRendererReused: function(e){ // summary: // Event dispatched when an item renderer that was recycled is reused. // e: __rendererLifecycleEventArgs // The renderer lifecycle event. // tags: // callback }, _onRendererDestroyed: function(e){ if(e.source == this){ this.onRendererDestroyed(e); } if(this.owner != null){ this.owner._onRendererDestroyed(e); } }, onRendererDestroyed: function(e){ // summary: // Event dispatched when an item renderer is destroyed. // e: __rendererLifecycleEventArgs // The renderer lifecycle event. // tags: // callback }, _onRenderersLayoutDone: function(view){ // tags: // private this.onRenderersLayoutDone(view); if(this.owner != null){ this.owner._onRenderersLayoutDone(view); } }, onRenderersLayoutDone: function(view){ // summary: // Event triggered when item renderers layout has been done. // tags: // callback }, _recycleRenderer: function(renderer, remove){ // summary: // Recycles the item renderer to be reused in the future. // renderer: dojox/calendar/_RendererMixin // The item renderer to recycle. // tags: // protected this.rendererManager.recycleRenderer(renderer, remove); }, _destroyRenderer: function(renderer){ // summary: // Destroys the item renderer. // renderer: dojox/calendar/_RendererMixin // The item renderer to destroy. // tags: // protected this.rendererManager.destroyRenderer(renderer); }, _destroyRenderersByKind: function(kind){ // tags: // private this.rendererManager.destroyRenderersByKind(kind); }, _updateEditingCapabilities: function(item, renderer){ // summary: // Update the moveEnabled and resizeEnabled properties of a renderer according to its event current editing state. // item: Object // The store data item. // renderer: dojox/calendar/_RendererMixin // The item renderer. // tags: // protected var moveEnabled = this.isItemMoveEnabled(item, renderer.rendererKind); var resizeEnabled = this.isItemResizeEnabled(item, renderer.rendererKind); var changed = false; if(moveEnabled != renderer.get("moveEnabled")){ renderer.set("moveEnabled", moveEnabled); changed = true; } if(resizeEnabled != renderer.get("resizeEnabled")){ renderer.set("resizeEnabled", resizeEnabled); changed = true; } if(changed){ renderer.updateRendering(); } }, updateRenderers: function(obj, stateOnly){ // summary: // Updates all the renderers that represents the specified item(s). // obj: Object // A render item or an array of render items. // stateOnly: Boolean // Whether only the state of the item has changed (selected, edited, edited, focused) or a more global change has occured. // tags: // protected if(obj == null){ return; } var items = lang.isArray(obj) ? obj : [obj]; for(var i=0; i<items.length; i++){ var item = items[i]; if(item == null || item.id == null){ continue; } var list = this.rendererManager.itemToRenderer[item.id]; if(list == null){ continue; } var selected = this.isItemSelected(item); var hovered = this.isItemHovered(item); var edited = this.isItemBeingEdited(item); var focused = this.showFocus ? this.isItemFocused(item) : false; for(var j = 0; j < list.length; j++){ var renderer = list[j].renderer; renderer.set("hovered", hovered); renderer.set("selected", selected); renderer.set("edited", edited); renderer.set("focused", focused); renderer.set("storeState", this.getItemStoreState(item)); this.applyRendererZIndex(item, list[j], hovered, selected, edited, focused); if(!stateOnly){ renderer.set("item", item); // force content refresh if(renderer.updateRendering){ renderer.updateRendering(); // reuse previously set dimensions } } } } }, applyRendererZIndex: function(item, renderer, hovered, selected, edited, focused){ // summary: // Applies the z-index to the renderer based on the state of the item. // This methods is setting a z-index of 20 is the item is selected or edited // and the current lane value computed by the overlap layout (i.e. the renderers // are stacked according to their lane). // item: Object // The render item. // renderer: Object // A renderer associated with the render item. // hovered: Boolean // Whether the item is hovered or not. // selected: Boolean // Whether the item is selected or not. // edited: Boolean // Whether the item is being edited not not. // focused: Boolean // Whether the item is focused not not. // tags: // protected domStyle.set(renderer.container, {"zIndex": edited || selected ? 20: item.lane == undefined ? 0 : item.lane}); }, getIdentity: function(item){ return this.owner ? this.owner.getIdentity(item) : item.id; }, ///////////////////////////////////////////////////// // // Hovered item // //////////////////////////////////////////////////// _setHoveredItem: function(item, renderer){ // summary: // Sets the current hovered item. // item: Object // The data item. // renderer: dojox/calendar/_RendererMixin // The item renderer. // tags: // protected if(this.owner){ this.owner._setHoveredItem(item, renderer); return; } if(this.hoveredItem && item && this.hoveredItem.id != item.id || item == null || this.hoveredItem == null){ var old = this.hoveredItem; this.hoveredItem = item; this.updateRenderers([old, this.hoveredItem], true); if(item && renderer){ this._updateEditingCapabilities(item._item ? item._item : item, renderer); } } }, // hoveredItem: Object // The currently hovered data item. hoveredItem: null, isItemHovered: function(item){ // summary: // Returns whether the specified item is hovered or not. // item: Object // The item. // returns: Boolean if (this._isEditing && this._edProps){ return item.id == this._edProps.editedItem.id; } return this.owner ? this.owner.isItemHovered(item) : this.hoveredItem != null && this.hoveredItem.id == item.id; }, isItemFocused: function(item){ // summary: // Returns whether the specified item is focused or not. // item: Object // The item. // returns: Boolean return this._isItemFocused ? this._isItemFocused(item) : false; }, //////////////////////////////////////////////////////////////////// // // Selection delegation // /////////////////////////////////////////////////////////////////// _setSelectionModeAttr: function(value){ if(this.owner){ this.owner.set("selectionMode", value); }else{ this.inherited(arguments); } }, _getSelectionModeAttr: function(value){ if(this.owner){ return this.owner.get("selectionMode"); } return this.inherited(arguments); }, _setSelectedItemAttr: function(value){ if(this.owner){ this.owner.set("selectedItem", value); }else{ this.inherited(arguments); } }, _getSelectedItemAttr: function(value){ if(this.owner){ return this.owner.get("selectedItem"); } return this.selectedItem; // no getter on super class (dojox.widget.Selection) }, _setSelectedItemsAttr: function(value){ if(this.owner){ this.owner.set("selectedItems", value); }else{ this.inherited(arguments); } }, _getSelectedItemsAttr: function(){ if(this.owner){ return this.owner.get("selectedItems"); } return this.inherited(arguments); }, isItemSelected: function(item){ if(this.owner){ return this.owner.isItemSelected(item); } return this.inherited(arguments); }, selectFromEvent: function(e, item, renderer, dispatch){ if(this.owner){ this.owner.selectFromEvent(e, item, renderer, dispatch); }else{ this.inherited(arguments); } }, setItemSelected: function(item, value){ if(this.owner){ this.owner.setItemSelected(item, value); }else{ this.inherited(arguments); } }, //////////////////////////////////////////////////////////////////// // // Event creation // /////////////////////////////////////////////////////////////////// createItemFunc: null, /*===== createItemFunc: function(view, d, e){ // summary: // A user supplied function that creates a new event. // view: ViewBase // the current view, // d: Date // the date at the clicked location. // e: MouseEvemt // the mouse event (can be used to return null for example) // subColumn: Object // the subcolumn at clicked location (can return null) }, =====*/ _getCreateItemFuncAttr: function(){ if(this.owner){ return this.owner.get("createItemFunc"); } return this.createItemFunc; }, // createOnGridClick: Boolean // Indicates whether the user can create new event by clicking and dragging the grid. // A createItem function must be defined on the view or the calendar object. createOnGridClick: false, _getCreateOnGridClickAttr: function(){ if(this.owner){ return this.owner.get("createOnGridClick"); } return this.createOnGridClick; }, //////////////////////////////////////////////////////////////////// // // Event creation // /////////////////////////////////////////////////////////////////// _gridMouseDown: false, _tempIdCount: 0, _tempItemsMap: null, _onGridMouseDown: function(e){ // tags: // private this._gridMouseDown = true; this.showFocus = false; if(this._isEditing){ this._endItemEditing("mouse", false); } this._doEndItemEditing(this.owner, "mouse"); this.set("focusedItem", null); this.selectFromEvent(e, null, null, true); if(this._setTabIndexAttr){ this[this._setTabIndexAttr].focus(); } if(this._onRendererHandleMouseDown){ var f = this.get("createItemFunc"); if(!f){ return; } var newItem = this._createdEvent = f(this, this.getTime(e), e, this.getSubColumn(e)); var store = this.get("store"); if(!newItem || store == null){ return; } // calendar needs an ID to work with if(store.getIdentity(newItem) == undefined){ var id = "_tempId_" + (this._tempIdCount++); newItem[store.idProperty] = id; if(this._tempItemsMap == null){ this._tempItemsMap = {}; } this._tempItemsMap[id] = true; } var newRenderItem = this.itemToRenderItem(newItem, store); newRenderItem._item = newItem; this._setItemStoreState(newItem, "unstored"); // add the new temporary item to the displayed list and force view refresh var owner = this._getTopOwner(); var items = owner.get("items"); owner.set("items", items ? items.concat([newRenderItem]) : [newRenderItem]); this._refreshItemsRendering(); // renderer created in _refreshItemsRenderering() var renderers = this.getRenderers(newItem); if(renderers && renderers.length>0){ var renderer = renderers[0]; if(renderer){ // trigger editing this._onRendererHandleMouseDown(e, renderer.renderer, "resizeEnd"); this._startItemEditing(newRenderItem, "mouse"); } } } }, _onGridMouseMove: function(e){ // tags: // private }, _onGridMouseUp: function(e){ // tags: // private }, _onGridTouchStart: function(e){ // tags: // privat