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.

305 lines (244 loc) 9.95 kB
define([ "dojo/_base/array", "dojo/_base/lang", "dojo/_base/declare", "dojo/sniff", "dojo/dom-construct", "dojo/dom-geometry", "dijit/registry", "./common", "./viewRegistry" ], function(array, lang, declare, has, domConstruct, domGeometry, registry, dm, viewRegistry){ // module: // dojox/mobile/LongListMixin // summary: // A mixin that enhances performance of long lists contained in scrollable views. return declare("dojox.mobile.LongListMixin", null, { // summary: // This mixin enhances performance of very long lists contained in scrollable views. // description: // LongListMixin enhances a list contained in a ScrollableView // so that only a subset of the list items are actually contained in the DOM // at any given time. // The parent must be a ScrollableView or another scrollable component // that inherits from the dojox.mobile.scrollable mixin, otherwise the mixin has // no effect. Also, editable lists are not yet supported, so lazy scrolling is // disabled if the list's 'editable' attribute is true. // If this mixin is used, list items must be added, removed or reordered exclusively // using the addChild and removeChild methods of the list. If the DOM is modified // directly (for example using list.containerNode.appendChild(...)), the list // will not behave correctly. // pageSize: int // Items are loaded in the DOM by chunks of this size. pageSize: 20, // maxPages: int // When this limit is reached, previous pages will be unloaded. maxPages: 5, // unloadPages: int // Number of pages that will be unloaded when maxPages is reached. unloadPages: 1, startup : function(){ if(this._started){ return; } this.inherited(arguments); if(!this.editable){ this._sv = viewRegistry.getEnclosingScrollable(this.domNode); if(this._sv){ // Get all children already added (e.g. through markup) and initialize _items this._items = this.getChildren(); // remove all existing items from the old container node this._clearItems(); this.containerNode = domConstruct.create("div", null, this.domNode); // listen to scrollTo and slideTo from the parent scrollable object this.connect(this._sv, "scrollTo", lang.hitch(this, this._loadItems), true); this.connect(this._sv, "slideTo", lang.hitch(this, this._loadItems), true); // The _topDiv and _bottomDiv elements are place holders for the items // that are not actually in the DOM at the top and bottom of the list. this._topDiv = domConstruct.create("div", null, this.domNode, "first"); this._bottomDiv = domConstruct.create("div", null, this.domNode, "last"); this._reloadItems(); } } }, _loadItems : function(toPos){ // summary: Adds and removes items to/from the DOM when the list is scrolled. var sv = this._sv; // ScrollableView var h = sv.getDim().d.h; if(h <= 0){ return; } // view is hidden var cury = -sv.getPos().y; // current y scroll position var posy = toPos ? -toPos.y : cury; // get minimum and maximum visible y positions: // we use the largest area including both the current and new position // so that all items will be visible during slideTo animations var visibleYMin = Math.min(cury, posy), visibleYMax = Math.max(cury, posy) + h; // add pages at top and bottom as required to fill the visible area while(this._loadedYMin > visibleYMin && this._addBefore()){ } while(this._loadedYMax < visibleYMax && this._addAfter()){ } }, _reloadItems: function(){ // summary: Resets the internal state and reloads items according to the current scroll position. // remove all loaded items this._clearItems(); // reset internal state this._loadedYMin = this._loadedYMax = 0; this._firstIndex = 0; this._lastIndex = -1; this._topDiv.style.height = "0px"; this._loadItems(); }, _clearItems: function(){ // summary: Removes all currently loaded items. var c = this.containerNode; array.forEach(registry.findWidgets(c), function(item){ c.removeChild(item.domNode); }); }, _addBefore: function(){ // summary: Loads pages of items before the currently visible items to fill the visible area. var i, count; var oldBox = domGeometry.getMarginBox(this.containerNode); for(count = 0, i = this._firstIndex-1; count < this.pageSize && i >= 0; count++, i--){ var item = this._items[i]; domConstruct.place(item.domNode, this.containerNode, "first"); if(!item._started){ item.startup(); } this._firstIndex = i; } var newBox = domGeometry.getMarginBox(this.containerNode); this._adjustTopDiv(oldBox, newBox); if(this._lastIndex - this._firstIndex >= this.maxPages*this.pageSize){ var toRemove = this.unloadPages*this.pageSize; for(i = 0; i < toRemove; i++){ this.containerNode.removeChild(this._items[this._lastIndex - i].domNode); } this._lastIndex -= toRemove; newBox = domGeometry.getMarginBox(this.containerNode); } this._adjustBottomDiv(newBox); return count == this.pageSize; }, _addAfter: function(){ // summary: Loads pages of items after the currently visible items to fill the visible area. var i, count; var oldBox = null; for(count = 0, i = this._lastIndex+1; count < this.pageSize && i < this._items.length; count++, i++){ var item = this._items[i]; domConstruct.place(item.domNode, this.containerNode); if(!item._started){ item.startup(); } this._lastIndex = i; } if(this._lastIndex - this._firstIndex >= this.maxPages*this.pageSize){ oldBox = domGeometry.getMarginBox(this.containerNode); var toRemove = this.unloadPages*this.pageSize; for(i = 0; i < toRemove; i++){ this.containerNode.removeChild(this._items[this._firstIndex + i].domNode); } this._firstIndex += toRemove; } var newBox = domGeometry.getMarginBox(this.containerNode); if(oldBox){ this._adjustTopDiv(oldBox, newBox); } this._adjustBottomDiv(newBox); return count == this.pageSize; }, _adjustTopDiv: function(oldBox, newBox){ // summary: Adjusts the height of the top filler div after items have been added/removed. this._loadedYMin -= newBox.h - oldBox.h; this._topDiv.style.height = this._loadedYMin + "px"; }, _adjustBottomDiv: function(newBox){ // summary: Adjusts the height of the bottom filler div after items have been added/removed. // the total height is an estimate based on the average height of the already loaded items var h = this._lastIndex > 0 ? (this._loadedYMin + newBox.h) / this._lastIndex : 0; h *= this._items.length - 1 - this._lastIndex; this._bottomDiv.style.height = h + "px"; this._loadedYMax = this._loadedYMin + newBox.h; }, _childrenChanged : function(){ // summary: Called by addChild/removeChild, updates the loaded items. // Whenever an item is added or removed, this may impact the loaded items, // so we have to clear all loaded items and recompute them. We cannot afford // to do this on every add/remove, so we use a timer to batch these updates. // There would probably be a way to update the loaded items on the fly // in add/removeChild, but at the cost of much more code... if(!this._qs_timer){ this._qs_timer = this.defer(function(){ delete this._qs_timer; this._reloadItems(); }); } }, resize: function(){ // summary: Loads/unloads items to fit the new size this.inherited(arguments); if(this._items){ this._loadItems(); } }, // The rest of the methods are overrides of _Container and _WidgetBase. // We must override them because children are not all added to the DOM tree // under the list node, only a subset of them will really be in the DOM, // but we still want the list to look as if all children were there. addChild : function(/* dijit._Widget */widget, /* int? */insertIndex){ // summary: Overrides dijit._Container if(this._items){ if( typeof insertIndex == "number"){ this._items.splice(insertIndex, 0, widget); }else{ this._items.push(widget); } this._childrenChanged(); }else{ this.inherited(arguments); } }, removeChild : function(/* Widget|int */widget){ // summary: Overrides dijit._Container if(this._items){ this._items.splice(typeof widget == "number" ? widget : this._items.indexOf(widget), 1); this._childrenChanged(); }else{ this.inherited(arguments); } }, getChildren : function(){ // summary: Overrides dijit._WidgetBase if(this._items){ return this._items.slice(0); }else{ return this.inherited(arguments); } }, _getSiblingOfChild : function(/* dijit._Widget */child, /* int */dir){ // summary: Overrides dijit._Container if(this._items){ var index = this._items.indexOf(child); if(index >= 0){ index = dir > 0 ? index++ : index--; } return this._items[index]; }else{ return this.inherited(arguments); } }, generateList: function(/*Array*/items){ // summary: // Overrides dojox.mobile._StoreListMixin when the list is a store list. if(this._items && !this.append){ // _StoreListMixin calls destroyRecursive to delete existing items, not removeChild, // so we must remove all logical items (i.e. clear _items) before reloading the store. // And since the superclass destroys all children returned by getChildren(), and // this would actually return no children because _items is now empty, we must // destroy all children manually first. array.forEach(this.getChildren(), function(child){ child.destroyRecursive(); }); this._items = []; } this.inherited(arguments); } }); });