1 | define([ "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 | }); |
---|