source: Dev/trunk/src/client/dojox/mobile/LongListMixin.js

Last change on this file was 483, checked in by hendrikvanantwerpen, 11 years ago

Added Dojo 1.9.3 release.

File size: 9.7 KB
Line 
1define([ "dojo/_base/array",
2         "dojo/_base/lang",
3         "dojo/_base/declare",
4         "dojo/sniff",
5         "dojo/dom-construct",
6         "dojo/dom-geometry",
7         "dijit/registry",
8         "./common",
9         "./viewRegistry" ],
10                function(array, lang, declare, has, domConstruct, domGeometry, registry, dm, viewRegistry){
11
12        // module:
13        //              dojox/mobile/LongListMixin
14        // summary:
15        //              A mixin that enhances performance of long lists contained in scrollable views.
16
17        return declare("dojox.mobile.LongListMixin", null, {
18                // summary:
19                //              This mixin enhances performance of very long lists contained in scrollable views.
20                // description:
21                //              LongListMixin enhances a list contained in a ScrollableView
22                //              so that only a subset of the list items are actually contained in the DOM
23                //              at any given time.
24                //              The parent must be a ScrollableView or another scrollable component
25                //              that inherits from the dojox.mobile.scrollable mixin, otherwise the mixin has
26                //              no effect. Also, editable lists are not yet supported, so lazy scrolling is
27                //              disabled if the list's 'editable' attribute is true.
28                //              If this mixin is used, list items must be added, removed or reordered exclusively
29                //              using the addChild and removeChild methods of the list. If the DOM is modified
30                //              directly (for example using list.containerNode.appendChild(...)), the list
31                //              will not behave correctly.
32               
33                // pageSize: int
34                //              Items are loaded in the DOM by chunks of this size.
35                pageSize: 20,
36               
37                // maxPages: int
38                //              When this limit is reached, previous pages will be unloaded.
39                maxPages: 5,
40               
41                // unloadPages: int
42                //              Number of pages that will be unloaded when maxPages is reached.
43                unloadPages: 1,
44               
45                startup : function(){
46                        if(this._started){ return; }
47                       
48                        this.inherited(arguments);
49
50                        if(!this.editable){
51
52                                this._sv = viewRegistry.getEnclosingScrollable(this.domNode);
53
54                                if(this._sv){
55
56                                        // Get all children already added (e.g. through markup) and initialize _items
57                                        this._items = this.getChildren();
58
59                                        // remove all existing items from the old container node
60                                        this._clearItems();
61
62                                        this.containerNode = domConstruct.create("div", null, this.domNode);
63
64                                        // listen to scrollTo and slideTo from the parent scrollable object
65
66                                        this.connect(this._sv, "scrollTo", lang.hitch(this, this._loadItems), true);
67                                        this.connect(this._sv, "slideTo", lang.hitch(this, this._loadItems), true);
68
69                                        // The _topDiv and _bottomDiv elements are place holders for the items
70                                        // that are not actually in the DOM at the top and bottom of the list.
71
72                                        this._topDiv = domConstruct.create("div", null, this.domNode, "first");
73                                        this._bottomDiv = domConstruct.create("div", null, this.domNode, "last");
74
75                                        this._reloadItems();
76                                }
77                        }
78                },
79               
80                _loadItems : function(toPos){
81                        // summary:     Adds and removes items to/from the DOM when the list is scrolled.
82                       
83                        var sv = this._sv;                      // ScrollableView
84                        var h = sv.getDim().d.h;
85                        if(h <= 0){ return; }                   // view is hidden
86
87                        var cury = -sv.getPos().y; // current y scroll position
88                        var posy = toPos ? -toPos.y : cury;
89
90                        // get minimum and maximum visible y positions:
91                        // we use the largest area including both the current and new position
92                        // so that all items will be visible during slideTo animations
93                        var visibleYMin = Math.min(cury, posy),
94                                visibleYMax = Math.max(cury, posy) + h;
95                       
96                        // add pages at top and bottom as required to fill the visible area
97                        while(this._loadedYMin > visibleYMin && this._addBefore()){ }
98                        while(this._loadedYMax < visibleYMax && this._addAfter()){ }
99                },
100               
101                _reloadItems: function(){
102                        // summary:     Resets the internal state and reloads items according to the current scroll position.
103
104                        // remove all loaded items
105                        this._clearItems();
106                       
107                        // reset internal state
108                        this._loadedYMin = this._loadedYMax = 0;
109                        this._firstIndex = 0;
110                        this._lastIndex = -1;
111                        this._topDiv.style.height = "0px";
112                       
113                        this._loadItems();
114                },
115               
116                _clearItems: function(){
117                        // summary: Removes all currently loaded items.
118                        var c = this.containerNode;
119                        array.forEach(registry.findWidgets(c), function(item){
120                                c.removeChild(item.domNode);
121                        });
122                },
123               
124                _addBefore: function(){
125                        // summary:     Loads pages of items before the currently visible items to fill the visible area.
126                       
127                        var i, count;
128                       
129                        var oldBox = domGeometry.getMarginBox(this.containerNode);
130                       
131                        for(count = 0, i = this._firstIndex-1; count < this.pageSize && i >= 0; count++, i--){
132                                var item = this._items[i];
133                                domConstruct.place(item.domNode, this.containerNode, "first");
134                                if(!item._started){
135                                        item.startup();
136                                }
137                                this._firstIndex = i;
138                        }
139                       
140                        var newBox = domGeometry.getMarginBox(this.containerNode);
141
142                        this._adjustTopDiv(oldBox, newBox);
143                       
144                        if(this._lastIndex - this._firstIndex >= this.maxPages*this.pageSize){
145                                var toRemove = this.unloadPages*this.pageSize;
146                                for(i = 0; i < toRemove; i++){
147                                        this.containerNode.removeChild(this._items[this._lastIndex - i].domNode);
148                                }
149                                this._lastIndex -= toRemove;
150                               
151                                newBox = domGeometry.getMarginBox(this.containerNode);
152                        }
153
154                        this._adjustBottomDiv(newBox);
155                       
156                        return count == this.pageSize;
157                },
158               
159                _addAfter: function(){
160                        // summary:     Loads pages of items after the currently visible items to fill the visible area.
161                       
162                        var i, count;
163                       
164                        var oldBox = null;
165                       
166                        for(count = 0, i = this._lastIndex+1; count < this.pageSize && i < this._items.length; count++, i++){
167                                var item = this._items[i];
168                                domConstruct.place(item.domNode, this.containerNode);
169                                if(!item._started){
170                                        item.startup();
171                                }
172                                this._lastIndex = i;
173                        }
174                        if(this._lastIndex - this._firstIndex >= this.maxPages*this.pageSize){
175                                oldBox = domGeometry.getMarginBox(this.containerNode);
176                                var toRemove = this.unloadPages*this.pageSize;
177                                for(i = 0; i < toRemove; i++){
178                                        this.containerNode.removeChild(this._items[this._firstIndex + i].domNode);
179                                }
180                                this._firstIndex += toRemove;
181                        }
182                       
183                        var newBox = domGeometry.getMarginBox(this.containerNode);
184
185                        if(oldBox){
186                                this._adjustTopDiv(oldBox, newBox);
187                        }
188                        this._adjustBottomDiv(newBox);
189
190                        return count == this.pageSize;
191                },
192               
193                _adjustTopDiv: function(oldBox, newBox){
194                        // summary:     Adjusts the height of the top filler div after items have been added/removed.
195                       
196                        this._loadedYMin -= newBox.h - oldBox.h;
197                        this._topDiv.style.height = this._loadedYMin + "px";
198                },
199               
200                _adjustBottomDiv: function(newBox){
201                        // summary:     Adjusts the height of the bottom filler div after items have been added/removed.
202                       
203                        // the total height is an estimate based on the average height of the already loaded items
204                        var h = this._lastIndex > 0 ? (this._loadedYMin + newBox.h) / this._lastIndex : 0;
205                        h *= this._items.length - 1 - this._lastIndex;
206                        this._bottomDiv.style.height = h + "px";
207                        this._loadedYMax = this._loadedYMin + newBox.h;
208                },
209               
210                _childrenChanged : function(){
211                        // summary: Called by addChild/removeChild, updates the loaded items.
212                       
213                        // Whenever an item is added or removed, this may impact the loaded items,
214                        // so we have to clear all loaded items and recompute them. We cannot afford
215                        // to do this on every add/remove, so we use a timer to batch these updates.
216                        // There would probably be a way to update the loaded items on the fly
217                        // in add/removeChild, but at the cost of much more code...
218                        if(!this._qs_timer){
219                                this._qs_timer = this.defer(function(){
220                                        delete this._qs_timer;
221                                        this._reloadItems();
222                                });
223                        }
224                },
225
226                resize: function(){
227                        // summary: Loads/unloads items to fit the new size
228                        this.inherited(arguments);
229                        if(this._items){
230                                this._loadItems();
231                        }
232                },
233               
234                // The rest of the methods are overrides of _Container and _WidgetBase.
235                // We must override them because children are not all added to the DOM tree
236                // under the list node, only a subset of them will really be in the DOM,
237                // but we still want the list to look as if all children were there.
238
239                addChild : function(/* dijit._Widget */widget, /* int? */insertIndex){
240                        // summary: Overrides dijit._Container
241                        if(this._items){
242                                if( typeof insertIndex == "number"){
243                                        this._items.splice(insertIndex, 0, widget);
244                                }else{
245                                        this._items.push(widget);
246                                }
247                                this._childrenChanged();
248                        }else{
249                                this.inherited(arguments);
250                        }
251                },
252
253                removeChild : function(/* Widget|int */widget){
254                        // summary: Overrides dijit._Container
255                        if(this._items){
256                                this._items.splice(typeof widget == "number" ? widget : this._items.indexOf(widget), 1);
257                                this._childrenChanged();
258                        }else{
259                                this.inherited(arguments);
260                        }
261                },
262
263                getChildren : function(){
264                        // summary: Overrides dijit._WidgetBase
265                        if(this._items){
266                                return this._items.slice(0);
267                        }else{
268                                return this.inherited(arguments);
269                        }
270                },
271
272                _getSiblingOfChild : function(/* dijit._Widget */child, /* int */dir){
273                        // summary: Overrides dijit._Container
274
275                        if(this._items){
276                                var index = this._items.indexOf(child);
277                                if(index >= 0){
278                                        index = dir > 0 ? index++ : index--;
279                                }
280                                return this._items[index];
281                        }else{
282                                return this.inherited(arguments);
283                        }
284                },
285               
286                generateList: function(/*Array*/items){
287                        // summary:
288                        //              Overrides dojox.mobile._StoreListMixin when the list is a store list.
289                       
290                        if(this._items && !this.append){
291                                // _StoreListMixin calls destroyRecursive to delete existing items, not removeChild,
292                                // so we must remove all logical items (i.e. clear _items) before reloading the store.
293                                // And since the superclass destroys all children returned by getChildren(), and
294                                // this would actually return no children because _items is now empty, we must
295                                // destroy all children manually first.
296                                array.forEach(this.getChildren(), function(child){
297                                        child.destroyRecursive();
298                                });
299                                this._items = [];
300                        }
301                        this.inherited(arguments);
302                }
303        });
304});
Note: See TracBrowser for help on using the repository browser.