]> git.proxmox.com Git - extjs.git/blame - extjs/modern/modern/src/dataview/List.js
add extjs 6.0.1 sources
[extjs.git] / extjs / modern / modern / src / dataview / List.js
CommitLineData
6527f429
DM
1/**\r
2 * List is a custom styled DataView which allows Grouping, Indexing, Icons, and a Disclosure.\r
3 *\r
4 * @example miniphone preview\r
5 * Ext.create('Ext.List', {\r
6 * fullscreen: true,\r
7 * itemTpl: '{title}',\r
8 * data: [\r
9 * { title: 'Item 1' },\r
10 * { title: 'Item 2' },\r
11 * { title: 'Item 3' },\r
12 * { title: 'Item 4' }\r
13 * ]\r
14 * });\r
15 *\r
16 * A more advanced example showing a list of people grouped by last name:\r
17 *\r
18 * @example miniphone preview\r
19 * Ext.define('Contact', {\r
20 * extend: 'Ext.data.Model',\r
21 * config: {\r
22 * fields: ['firstName', 'lastName']\r
23 * }\r
24 * });\r
25 *\r
26 * var store = Ext.create('Ext.data.Store', {\r
27 * model: 'Contact',\r
28 * sorters: 'lastName',\r
29 *\r
30 * grouper: {\r
31 * groupFn: function(record) {\r
32 * return record.get('lastName')[0];\r
33 * }\r
34 * },\r
35 *\r
36 * data: [\r
37 * { firstName: 'Peter', lastName: 'Venkman' },\r
38 * { firstName: 'Raymond', lastName: 'Stantz' },\r
39 * { firstName: 'Egon', lastName: 'Spengler' },\r
40 * { firstName: 'Winston', lastName: 'Zeddemore'}\r
41 * ]\r
42 * });\r
43 *\r
44 * Ext.create('Ext.List', {\r
45 * fullscreen: true,\r
46 * itemTpl: '<div class="contact">{firstName} <strong>{lastName}</strong></div>',\r
47 * store: store,\r
48 * grouped: true\r
49 * });\r
50 *\r
51 * If you want to dock items to the bottom or top of a List, you can use the scrollDock configuration on child items in this List. The following example adds a button to the bottom of the List.\r
52 *\r
53 * @example phone preview\r
54 * Ext.define('Contact', {\r
55 * extend: 'Ext.data.Model',\r
56 * config: {\r
57 * fields: ['firstName', 'lastName']\r
58 * }\r
59 * });\r
60 *\r
61 * var store = Ext.create('Ext.data.Store', {\r
62 * model: 'Contact',\r
63 * sorters: 'lastName',\r
64 *\r
65 * grouper: {\r
66 * groupFn: function(record) {\r
67 * return record.get('lastName')[0];\r
68 * }\r
69 * },\r
70 *\r
71 * data: [\r
72 * { firstName: 'Peter', lastName: 'Venkman' },\r
73 * { firstName: 'Raymond', lastName: 'Stantz' },\r
74 * { firstName: 'Egon', lastName: 'Spengler' },\r
75 * { firstName: 'Winston', lastName: 'Zeddemore'}\r
76 * ]\r
77 * });\r
78 *\r
79 * Ext.create('Ext.List', {\r
80 * fullscreen: true,\r
81 * itemTpl: '<div class="contact">{firstName} <strong>{lastName}</strong></div>',\r
82 * store: store,\r
83 * items: [{\r
84 * xtype: 'button',\r
85 * scrollDock: 'bottom',\r
86 * docked: 'bottom',\r
87 * text: 'Load More...'\r
88 * }]\r
89 * });\r
90 */\r
91Ext.define('Ext.dataview.List', {\r
92 alternateClassName: 'Ext.List',\r
93 extend: 'Ext.dataview.DataView',\r
94 xtype: 'list',\r
95\r
96 mixins: ['Ext.mixin.Hookable'],\r
97\r
98 requires: [\r
99 'Ext.data.Store',\r
100 'Ext.dataview.IndexBar',\r
101 'Ext.dataview.ListItemHeader',\r
102 'Ext.dataview.component.ListItem',\r
103 'Ext.dataview.component.SimpleListItem',\r
104 'Ext.util.PositionMap'\r
105 ],\r
106\r
107 /**\r
108 * @event disclose\r
109 * @preventable\r
110 * Fires whenever a disclosure is handled\r
111 * @param {Ext.dataview.List} this The List instance\r
112 * @param {Ext.data.Model} record The record associated to the item\r
113 * @param {HTMLElement} target The element disclosed\r
114 * @param {Number} index The index of the item disclosed\r
115 * @param {Ext.EventObject} e The event object\r
116 */\r
117\r
118 config: {\r
119 /**\r
120 * @cfg {Object} container\r
121 * This config is used to control the internal {@link Ext.Container} created to\r
122 * manage this list's items. One common use for this is to apply a {@link #userCls}\r
123 * to the item container.\r
124 *\r
125 * {\r
126 * xtype: 'list',\r
127 * container: {\r
128 * userCls: 'mylist-cls'\r
129 * },\r
130 * ...\r
131 * }\r
132 *\r
133 * @since 6.0.1\r
134 */\r
135 container: {\r
136 lazy: true,\r
137 $value: {\r
138 xtype: 'container',\r
139 scrollable: {}\r
140 }\r
141 },\r
142\r
143 /**\r
144 * @cfg layout\r
145 * Hide layout config in DataView. It only causes confusion.\r
146 * @accessor\r
147 * @private\r
148 */\r
149 layout: 'fit',\r
150\r
151 /**\r
152 * @cfg {Boolean/Object} indexBar\r
153 * `true` to render an alphabet IndexBar docked on the right.\r
154 * This can also be a config object that will be passed to {@link Ext.IndexBar}.\r
155 * @accessor\r
156 */\r
157 indexBar: false,\r
158\r
159 icon: null,\r
160\r
161 /**\r
162 * @cfg {Boolean} preventSelectionOnDisclose `true` to prevent the item selection when the user\r
163 * taps a disclose icon.\r
164 * @accessor\r
165 */\r
166 preventSelectionOnDisclose: true,\r
167\r
168 /**\r
169 * @cfg baseCls\r
170 * @inheritdoc\r
171 */\r
172 baseCls: Ext.baseCSSPrefix + 'list',\r
173\r
174 /**\r
175 * @cfg {Boolean} pinHeaders\r
176 * Whether or not to pin headers on top of item groups while scrolling for an iPhone native list experience.\r
177 * @accessor\r
178 */\r
179 pinHeaders: true,\r
180\r
181 /**\r
182 * @cfg {Boolean} grouped\r
183 * Whether or not to group items in the provided Store with a header for each item.\r
184 * @accessor\r
185 */\r
186 grouped: null,\r
187\r
188 /**\r
189 * @cfg {Boolean/Function/Object} onItemDisclosure\r
190 * `true` to display a disclosure icon on each list item.\r
191 * The list will still fire the disclose event, and the event can be stopped before itemtap.\r
192 * By setting this config to a function, the function passed will be called when the disclosure\r
193 * is tapped.\r
194 * Finally you can specify an object with a 'scope' and 'handler'\r
195 * property defined. This will also be bound to the tap event listener\r
196 * and is useful when you want to change the scope of the handler.\r
197 * @accessor\r
198 */\r
199 onItemDisclosure: null,\r
200\r
201 /**\r
202 * @cfg {String} disclosureProperty\r
203 * A property to check on each record to display the disclosure on a per record basis. This\r
204 * property must be false to prevent the disclosure from being displayed on the item.\r
205 * @accessor\r
206 */\r
207 disclosureProperty: 'disclosure',\r
208\r
209 /**\r
210 * @cfg {Boolean} useComponents\r
211 * Flag the use a component based DataView implementation. This allows the full use of components in the\r
212 * DataView at the cost of some performance.\r
213 *\r
214 * @accessor\r
215 * @private\r
216 */\r
217\r
218 /**\r
219 * @cfg {Object} itemConfig\r
220 * A configuration object that is passed to every item created by a component based DataView. Because each\r
221 * item that a List renders is a Component, we can pass configuration options to each component to\r
222 * easily customize how each child component behaves.\r
223 * @accessor\r
224 * @private\r
225 */\r
226\r
227 /**\r
228 * @cfg {Number} maxItemCache\r
229 * Maintains a cache of reusable components when using a component based DataView. Improving performance at\r
230 * the cost of memory.\r
231 * Note this is currently only used when useComponents is true.\r
232 * @accessor\r
233 * @private\r
234 */\r
235\r
236 /**\r
237 * @cfg {String} defaultType\r
238 * The xtype used for the component based DataView. Defaults to dataitem.\r
239 * Note this is only used when useComponents is true.\r
240 * @accessor\r
241 */\r
242 defaultType: undefined,\r
243\r
244 /**\r
245 * @cfg {Object} itemMap\r
246 * @private\r
247 */\r
248 itemMap: {},\r
249\r
250 /**\r
251 * @cfg {Number} itemHeight\r
252 * This allows you to set the default item height and is used to roughly calculate the amount\r
253 * of items needed to fill the list. By default items are around 50px high.\r
254 */\r
255 itemHeight: null,\r
256\r
257 /**\r
258 * @cfg {Boolean} variableHeights\r
259 * This configuration allows you optimize the list by not having it read the DOM heights of list items.\r
260 * Instead it will assume (and set) the height to be the {@link #itemHeight}.\r
261 * @private\r
262 */\r
263 variableHeights: false,\r
264\r
265 /**\r
266 * @cfg {Boolean} refreshHeightOnUpdate\r
267 * Set this to false if you make many updates to your list (like in an interval), but updates\r
268 * won't affect the item's height. Doing this will increase the performance of these updates.\r
269 */\r
270 refreshHeightOnUpdate: true,\r
271\r
272 /**\r
273 * @cfg {Boolean} infinite\r
274 * Set this to false to render all items in this list, and render them relatively.\r
275 * Note that this configuration can not be dynamically changed after the list has instantiated.\r
276 */\r
277 infinite: false,\r
278\r
279 /**\r
280 * @cfg {Boolean} useSimpleItems\r
281 * Set this to true if you just want to have the list create simple items that use the itemTpl.\r
282 * These simple items still support headers, grouping and disclosure functionality but avoid\r
283 * container layouts and deeply nested markup. For many Lists using this configuration will\r
284 * drastically increase the scrolling and render performance.\r
285 */\r
286 useSimpleItems: true,\r
287\r
288 /**\r
289 * @cfg {Object} scrollable\r
290 * @private\r
291 */\r
292 scrollable: null,\r
293\r
294 /**\r
295 * @cfg {Number} bufferSize\r
296 * The amount of items we render additionally besides the ones currently visible.\r
297 * We try to prevent the rendering of items while scrolling until the next time you stop scrolling.\r
298 * If you scroll close to the end of the buffer, we start rendering individual items to always\r
299 * have the {@link #minimumBufferSize} prepared.\r
300 */\r
301 bufferSize: 20,\r
302\r
303 minimumBufferDistance: 5,\r
304\r
305 /**\r
306 * @cfg {Boolean} striped\r
307 * Set this to true if you want the items in the list to be zebra striped, alternating their\r
308 * background color.\r
309 */\r
310 striped: false\r
311 },\r
312\r
313 topRenderedIndex: 0,\r
314 topVisibleIndex: 0,\r
315 visibleCount: 0,\r
316\r
317 //<debug>\r
318 constructor: function(config) {\r
319 this.callParent([config]);\r
320 var layout = this.getLayout();\r
321 if (layout && !layout.isFit) {\r
322 Ext.Logger.error('The base layout for a DataView must always be a Fit Layout');\r
323 }\r
324 },\r
325 //</debug>\r
326\r
327 // We create complex instance arrays and objects in beforeInitialize so that we can use these inside of the initConfig process.\r
328 beforeInitialize: function() {\r
329 var me = this,\r
330 container = me.container,\r
331 baseCls = me.getBaseCls(),\r
332 scrollViewElement, pinnedHeader;\r
333\r
334 Ext.apply(me, {\r
335 listItems: [],\r
336 headerItems: [],\r
337 updatedItems: [],\r
338 headerMap: [],\r
339 recordMap: {},\r
340 scrollDockItems: {\r
341 top: [],\r
342 bottom: []\r
343 }\r
344 });\r
345\r
346 me.translationMethod = 'csstransform';\r
347\r
348 // Create the inner container that will actually hold all the list items\r
349 if (!container) {\r
350 container = me.container = me.createContainer();\r
351 }\r
352\r
353 // We add the container after creating it manually because when you add the container,\r
354 // the items config is initialized. When this happens, any scrollDock items will be added,\r
355 // which in turn tries to add these items to the container\r
356 me.add(container);\r
357\r
358 // We make this List's scrollable the inner containers scrollable\r
359 scrollViewElement = me.scrollViewElement = container.bodyElement;\r
360 me.scrollElement = container.innerElement;\r
361\r
362 // Create the pinnedHeader instance thats being used when grouping is enabled\r
363 // and insert it into the scrollElement\r
364 pinnedHeader = me.pinnedHeader = Ext.factory({\r
365 xtype: 'listitemheader',\r
366 html: '&nbsp;',\r
367 translatable: {\r
368 translationMethod: me.translationMethod\r
369 },\r
370 cls: [baseCls + '-header', baseCls + '-header-swap']\r
371 });\r
372 pinnedHeader.translate(0, -10000);\r
373 pinnedHeader.$position = -10000;\r
374 scrollViewElement.insertFirst(pinnedHeader.renderElement);\r
375\r
376 container.getScrollable().on({\r
377 scroll: 'onScroll',\r
378 refresh: 'onScrollerRefresh',\r
379 scope: me\r
380 });\r
381 },\r
382\r
383 /**\r
384 * @private\r
385 */\r
386 createContainer: function () {\r
387 var config = Ext.merge({\r
388 scrollable: {\r
389 autoRefresh: this.getInfinite() ? null : true\r
390 }\r
391 }, this.getContainer());\r
392\r
393 return Ext.create(config);\r
394 },\r
395\r
396 getScrollable: function() {\r
397 return this.container.getScrollable();\r
398 },\r
399\r
400 // We override DataView's initialize method with an empty function\r
401 initialize: function() {\r
402 var me = this,\r
403 container = me.container,\r
404 scrollViewElement = me.scrollViewElement,\r
405 indexBar = me.getIndexBar(),\r
406 triggerEvent = me.getTriggerEvent(),\r
407 triggerCtEvent = me.getTriggerCtEvent();\r
408\r
409 if (indexBar) {\r
410 scrollViewElement.appendChild(indexBar.renderElement);\r
411 }\r
412\r
413 if (triggerEvent) {\r
414 me.on(triggerEvent, me.onItemTrigger, me);\r
415 }\r
416 if (triggerCtEvent) {\r
417 me.on(triggerCtEvent, me.onContainerTrigger, me);\r
418 }\r
419\r
420 container.element.on({\r
421 delegate: '.' + me.getBaseCls() + '-disclosure',\r
422 tap: 'handleItemDisclosure',\r
423 scope: me\r
424 });\r
425\r
426 container.element.on({\r
427 resize: 'onContainerResize',\r
428 scope: me\r
429 });\r
430\r
431 // Android 2.x not a direct child\r
432 container.innerElement.on({\r
433 touchstart: 'onItemTouchStart',\r
434 touchend: 'onItemTouchEnd',\r
435 tap: 'onItemTap',\r
436 taphold: 'onItemTapHold',\r
437 singletap: 'onItemSingleTap',\r
438 doubletap: 'onItemDoubleTap',\r
439 swipe: 'onItemSwipe',\r
440 delegate: '.' + Ext.baseCSSPrefix + 'list-item',\r
441 scope: me\r
442 });\r
443\r
444 if (me.getStore()) {\r
445 if (me.isPainted()) {\r
446 me.refresh();\r
447 } else {\r
448 me.on({\r
449 painted: 'refresh',\r
450 single: true\r
451 });\r
452 }\r
453 }\r
454 },\r
455\r
456 getRefItems: function(deep) {\r
457 var result = [],\r
458 candidates = this.callParent([deep]),\r
459 len = candidates.length,\r
460 i, candidate;\r
461\r
462 // We must only return list items that are part of the rendered block.\r
463 // Check that their position is not -10000\r
464 for (i = 0; i < len; i++) {\r
465 candidate = candidates[i];\r
466 if (!candidate.hasOwnProperty('$position') || candidate.$position > -1) {\r
467 result[result.length] = candidate;\r
468 }\r
469 }\r
470 \r
471 return result;\r
472 },\r
473\r
474 onScroll: function(scroller, x, y) {\r
475 var me = this,\r
476 pinnedHeader = me.pinnedHeader,\r
477 store = me.getStore(),\r
478 storeCount = store && store.getCount(),\r
479 grouped = me.isGrouping(),\r
480 infinite = me.getInfinite();\r
481\r
482 // This method was originally written as an interceptor for doTranslate, so the\r
483 // coordinates are expected to be in CSS translation form (negative number ==\r
484 // positive scroll position). It was changed to be a scroll event listener in v5\r
485 // which uses scroll position coordinates.\r
486 if (x) {\r
487 x = -x;\r
488 }\r
489 if (y) {\r
490 y = -y;\r
491 }\r
492\r
493 if (!storeCount) {\r
494 me.showEmptyText();\r
495 me.showEmptyScrollDock();\r
496\r
497 pinnedHeader.$position = -10000;\r
498 pinnedHeader.translate(0, -10000);\r
499 }\r
500 else if (infinite && me.itemsCount) {\r
501 me.handleItemUpdates(y);\r
502 me.handleItemHeights();\r
503 me.handleItemTransforms();\r
504\r
505 if (!me.onIdleBound) {\r
506 Ext.AnimationQueue.onIdle(me.onAnimationIdle, me);\r
507 me.onIdleBound = true;\r
508 }\r
509 }\r
510\r
511 if (grouped && me.groups && me.groups.length && me.getPinHeaders()) {\r
512 me.handlePinnedHeader(y);\r
513 }\r
514\r
515 // This is a template method that can be intercepted by plugins to do things when scrolling\r
516 me.onScrollBinder(x, y);\r
517 },\r
518\r
519 onScrollerRefresh: function(scroller) {\r
520 var position = scroller.getPosition();\r
521 this.onScroll(scroller, position.x, position.y);\r
522 },\r
523\r
524 onScrollBinder: function(){},\r
525\r
526 handleItemUpdates: function(y) {\r
527 var me = this,\r
528 listItems = me.listItems,\r
529 itemsCount = listItems.length,\r
530 info = me.getListItemInfo(),\r
531 itemMap = me.getItemMap(),\r
532 bufferSize = me.getBufferSize(),\r
533 lastIndex = me.getStore().getCount() - 1,\r
534 minimumBufferDistance = me.getMinimumBufferDistance(),\r
535 currentTopVisibleIndex = me.topVisibleIndex,\r
536 topRenderedIndex = me.topRenderedIndex,\r
537 updateCount, i, item, topVisibleIndex, bufferDistance, itemIndex;\r
538\r
539 // This is the index of the item that is currently visible at the top\r
540 me.topVisibleIndex = topVisibleIndex = Math.max(0, itemMap.findIndex(-y) || 0);\r
541\r
542 if (currentTopVisibleIndex !== topVisibleIndex) {\r
543 // When we are scrolling up\r
544 if (currentTopVisibleIndex > topVisibleIndex) {\r
545 bufferDistance = topVisibleIndex - topRenderedIndex;\r
546 if (bufferDistance < minimumBufferDistance) {\r
547 updateCount = Math.min(itemsCount, minimumBufferDistance - bufferDistance);\r
548\r
549 if (updateCount == itemsCount) {\r
550 me.topRenderedIndex = topRenderedIndex = Math.max(0, topVisibleIndex - (bufferSize - minimumBufferDistance));\r
551 // Update all\r
552 for (i = 0; i < updateCount; i++) {\r
553 itemIndex = topRenderedIndex + i;\r
554 item = listItems[i];\r
555 me.updateListItem(item, itemIndex, info);\r
556 }\r
557 }\r
558 else {\r
559 for (i = 0; i < updateCount; i++) {\r
560 itemIndex = topRenderedIndex - i - 1;\r
561 if (itemIndex < 0) {\r
562 break;\r
563 }\r
564\r
565 item = listItems.pop();\r
566 listItems.unshift(item);\r
567 me.updateListItem(item, itemIndex, info);\r
568 me.topRenderedIndex--;\r
569 }\r
570 }\r
571 }\r
572 }\r
573 // When we are scrolling down\r
574 else {\r
575 bufferDistance = bufferSize - (topVisibleIndex - topRenderedIndex);\r
576\r
577 if (bufferDistance < minimumBufferDistance) {\r
578 updateCount = Math.min(itemsCount, minimumBufferDistance - bufferDistance);\r
579\r
580 if (updateCount == itemsCount) {\r
581 me.topRenderedIndex = topRenderedIndex = Math.min(lastIndex - itemsCount, topVisibleIndex - minimumBufferDistance);\r
582 // Update all\r
583 for (i = 0; i < updateCount; i++) {\r
584 itemIndex = topRenderedIndex + i;\r
585 item = listItems[i];\r
586 me.updateListItem(item, itemIndex, info);\r
587 }\r
588 }\r
589 else {\r
590 for (i = 0; i < updateCount; i++) {\r
591 itemIndex = topRenderedIndex + itemsCount + i;\r
592 if (itemIndex > lastIndex) {\r
593 break;\r
594 }\r
595\r
596 item = listItems.shift();\r
597 listItems.push(item);\r
598 me.updateListItem(item, itemIndex, info);\r
599 me.topRenderedIndex++;\r
600 }\r
601 }\r
602 }\r
603 }\r
604 }\r
605 },\r
606\r
607 onAnimationIdle: function() {\r
608 var me = this,\r
609 info = me.getListItemInfo(),\r
610 bufferSize = me.getBufferSize(),\r
611 topVisibleIndex = me.topVisibleIndex,\r
612 topRenderedIndex = me.topRenderedIndex,\r
613 lastIndex = me.getStore().getCount() - 1,\r
614 listItems = me.listItems,\r
615 itemsCount = listItems.length,\r
616 topBufferDistance, bottomBufferDistance,\r
617 i, ln, item, itemIndex;\r
618\r
619 topBufferDistance = topVisibleIndex - topRenderedIndex;\r
620 bottomBufferDistance = topRenderedIndex + bufferSize - topVisibleIndex;\r
621\r
622 if (topBufferDistance < bottomBufferDistance) {\r
623 // This means there are more items below the visible list. The user\r
624 // has probably just scrolled up. In this case we move some items\r
625 // from the bottom to the top only if the list is scrolled down a bit\r
626 if (topVisibleIndex > 0) {\r
627 ln = bottomBufferDistance - topBufferDistance;\r
628\r
629 for (i = 0; i < ln; i++) {\r
630 itemIndex = topRenderedIndex - i - 1;\r
631 if (itemIndex < 0) {\r
632 break;\r
633 }\r
634\r
635 item = listItems.pop();\r
636 listItems.unshift(item);\r
637 me.updateListItem(item, itemIndex, info);\r
638 me.topRenderedIndex--;\r
639 }\r
640 }\r
641 }\r
642 else {\r
643 ln = topBufferDistance - bottomBufferDistance;\r
644 for (i = 0; i < ln; i++) {\r
645 itemIndex = topRenderedIndex + itemsCount + i;\r
646 if (itemIndex > lastIndex) {\r
647 break;\r
648 }\r
649\r
650 item = listItems.shift();\r
651 listItems.push(item);\r
652 me.updateListItem(item, itemIndex, info);\r
653 me.topRenderedIndex++;\r
654 }\r
655 }\r
656\r
657 me.handleItemHeights();\r
658 me.handleItemTransforms();\r
659\r
660 me.onIdleBound = false;\r
661 },\r
662\r
663 handleItemHeights: function() {\r
664 var me = this,\r
665 updatedItems = me.updatedItems,\r
666 ln = updatedItems.length,\r
667 itemMap = me.getItemMap(),\r
668 useSimpleItems = me.getUseSimpleItems(),\r
669 minimumHeight = itemMap.getMinimumHeight(),\r
670 headerIndices = me.headerIndices,\r
671 headerMap = me.headerMap,\r
672 variableHeights = me.getVariableHeights(),\r
673 itemIndex, i, j, jln, item, height, scrollDockHeight;\r
674\r
675 for (i = 0; i < ln; i++) {\r
676 item = updatedItems[i];\r
677 itemIndex = item.$dataIndex;\r
678\r
679 // itemIndex may not be set yet if the store is still being loaded\r
680 if (itemIndex !== null) {\r
681 if (variableHeights) {\r
682 height = useSimpleItems ? item.element.getHeight() : item.element.getFirstChild().getHeight();\r
683 height = Math.max(height, minimumHeight);\r
684 } else {\r
685 height = minimumHeight;\r
686 }\r
687\r
688 item.$ownItemHeight = height;\r
689\r
690 jln = me.scrollDockItems.top.length;\r
691 if (item.isFirst) {\r
692 me.totalScrollDockTopHeight = 0;\r
693 for (j = 0; j < jln; j++) {\r
694 scrollDockHeight = me.scrollDockItems.top[j].$scrollDockHeight;\r
695 height += scrollDockHeight;\r
696 me.totalScrollDockTopHeight += scrollDockHeight;\r
697 }\r
698 }\r
699\r
700 jln = me.scrollDockItems.bottom.length;\r
701 if (item.isLast) {\r
702 for (j = 0; j < jln; j++) {\r
703 scrollDockHeight = me.scrollDockItems.bottom[j].$scrollDockHeight;\r
704 height += scrollDockHeight;\r
705 }\r
706 }\r
707\r
708 if (headerIndices && headerIndices[itemIndex]) {\r
709 height += me.headerHeight;\r
710 }\r
711\r
712 itemMap.setItemHeight(itemIndex, height);\r
713 item.$height = height;\r
714 }\r
715 }\r
716\r
717 itemMap.update();\r
718\r
719 headerMap.length = 0;\r
720 for (i in headerIndices) {\r
721 if (headerIndices.hasOwnProperty(i)) {\r
722 headerMap.push(itemMap.map[i]);\r
723 }\r
724 }\r
725\r
726 me.updatedItems.length = 0;\r
727\r
728 me.refreshScroller(true);\r
729 },\r
730\r
731 handleItemTransforms: function() {\r
732 var me = this,\r
733 listItems = me.listItems,\r
734 itemsCount = listItems.length,\r
735 itemMap = me.getItemMap(),\r
736 scrollDockItems = me.scrollDockItems,\r
737 grouped = me.isGrouping(),\r
738 item, transY, i, jln, j;\r
739\r
740 for (i = 0; i < itemsCount; i++) {\r
741 item = listItems[i];\r
742 transY = itemMap.map[item.$dataIndex];\r
743\r
744 if (!item.$hidden && item.$position !== transY) {\r
745 item.$position = transY;\r
746\r
747 jln = scrollDockItems.top.length;\r
748 if (item.isFirst && jln) {\r
749 for (j = 0; j < jln; j++) {\r
750 scrollDockItems.top[j].translate(0, transY);\r
751 transY += scrollDockItems.top[j].$scrollDockHeight;\r
752 }\r
753 }\r
754\r
755 if (grouped && me.headerIndices && me.headerIndices[item.$dataIndex]) {\r
756 item.getHeader().translate(0, transY);\r
757 transY += me.headerHeight;\r
758 }\r
759\r
760 item.translate(0, transY);\r
761 transY += item.$ownItemHeight;\r
762\r
763 jln = scrollDockItems.bottom.length;\r
764 if (item.isLast && jln) {\r
765 for (j = 0; j < jln; j++) {\r
766 scrollDockItems.bottom[j].translate(0, transY);\r
767 transY += scrollDockItems.bottom[j].$scrollDockHeight;\r
768 }\r
769 }\r
770 }\r
771 }\r
772 },\r
773\r
774 handlePinnedHeader: function(y) {\r
775 var me = this,\r
776 pinnedHeader = me.pinnedHeader,\r
777 itemMap = me.getItemMap(),\r
778 groups = me.groups,\r
779 headerMap = me.headerMap,\r
780 headerHeight = me.headerHeight,\r
781 store = me.getStore(),\r
782 totalScrollDockTopHeight = me.totalScrollDockTopHeight,\r
783 record, closestHeader, pushedHeader, transY, headerString;\r
784\r
785 closestHeader = itemMap.binarySearch(headerMap, -y);\r
786 record = groups.getAt(closestHeader).getAt(0);\r
787\r
788 if (record) {\r
789 pushedHeader = y + headerMap[closestHeader + 1] - headerHeight;\r
790 // Top of the list or above (hide the floating header offscreen)\r
791 if (y >= 0 || (closestHeader === 0 && totalScrollDockTopHeight + y >= 0) || (closestHeader === 0 && -y <= headerMap[closestHeader])) {\r
792 transY = -10000;\r
793 }\r
794 // Scroll the floating header a bit\r
795 else if (pushedHeader < 0) {\r
796 transY = pushedHeader;\r
797 }\r
798 // Stick to the top of the screen\r
799 else {\r
800 transY = Math.max(0, y);\r
801 }\r
802\r
803 headerString = store.getGrouper().getGroupString(record);\r
804\r
805 if (pinnedHeader.$currentHeader != headerString) {\r
806 pinnedHeader.setHtml(headerString);\r
807 pinnedHeader.$currentHeader = headerString;\r
808 }\r
809\r
810 if (pinnedHeader.$position != transY) {\r
811 pinnedHeader.translate(0, transY);\r
812 pinnedHeader.$position = transY;\r
813 }\r
814 }\r
815 },\r
816\r
817 createItem: function(config) {\r
818 var me = this,\r
819 container = me.container,\r
820 listItems = me.listItems,\r
821 infinite = me.getInfinite(),\r
822 scrollElement = me.scrollElement,\r
823 item, header, itemCls;\r
824\r
825 config.$initParent = me;\r
826 item = Ext.factory(config);\r
827 delete config.$initParent;\r
828 item.dataview = me;\r
829 item.$height = config.minHeight;\r
830\r
831 if (!infinite) {\r
832 itemCls = me.getBaseCls() + '-item-relative';\r
833 item.addCls(itemCls);\r
834 }\r
835\r
836 header = item.getHeader && item.getHeader();\r
837 if (header) {\r
838 if (!infinite) {\r
839 header.addCls(itemCls);\r
840 } else {\r
841 header.setTranslatable({\r
842 translationMethod: this.translationMethod\r
843 });\r
844 header.translate(0, -10000);\r
845\r
846 scrollElement.insertFirst(header.renderElement);\r
847 }\r
848 }\r
849\r
850 container.doAdd(item);\r
851 listItems.push(item);\r
852\r
853 return item;\r
854 },\r
855\r
856 setItemsCount: function(itemsCount, itemConfig) {\r
857 var me = this,\r
858 listItems = me.listItems,\r
859 config = itemConfig || me.getListItemConfig(),\r
860 difference = itemsCount - listItems.length,\r
861 i;\r
862\r
863 // This loop will create new items if the new itemsCount is higher than the amount of items we currently have\r
864 for (i = 0; i < difference; i++) {\r
865 me.createItem(config);\r
866 }\r
867\r
868 // This loop will destroy unneeded items if the new itemsCount is lower than the amount of items we currently have\r
869 for (i = difference; i < 0; i++) {\r
870 listItems.pop().destroy();\r
871 }\r
872\r
873 me.itemsCount = itemsCount;\r
874\r
875 // Finally we update all the list items with the correct content\r
876 me.updateAllListItems();\r
877\r
878 //Android Stock bug where redraw is needed to show empty list\r
879 if (Ext.browser.is.AndroidStock && me.container.element && itemsCount === 0 && difference !== 0) {\r
880 me.container.element.redraw();\r
881 }\r
882\r
883 return me.listItems;\r
884 },\r
885\r
886 updateListItem: function(item, index, info) {\r
887 var me = this,\r
888 recordMap = me.recordMap,\r
889 oldRecord = item.getRecord(),\r
890 store = info.store,\r
891 record = store.getAt(index),\r
892 headerIndices = me.headerIndices,\r
893 footerIndices = me.footerIndices,\r
894 header = item.getHeader && item.getHeader(),\r
895 scrollDockItems = me.scrollDockItems,\r
896 updatedItems = me.updatedItems,\r
897 infinite = me.getInfinite(),\r
898 storeCount = store.getCount(),\r
899 grouper = store.getGrouper(),\r
900 itemCls = [],\r
901 headerCls = [],\r
902 itemRemoveCls = [info.headerCls, info.footerCls, info.firstCls, info.lastCls, info.selectedCls, info.stripeCls],\r
903 headerRemoveCls = [info.headerCls, info.footerCls, info.firstCls, info.lastCls],\r
904 ln, i, scrollDockItem, viewModel;\r
905\r
906 // When we update a list item, the header and scrolldocks can make it have to be retransformed.\r
907 // For that reason we want to always set the position to -10000 so that the next time we translate\r
908 // all the pieces are transformed to the correct location\r
909 if (infinite) {\r
910 item.$position = -10000;\r
911 }\r
912\r
913 // We begin by hiding/showing the item and its header depending on a record existing at this index\r
914 if (!record) {\r
915 item.setRecord(null);\r
916 if (oldRecord) {\r
917 delete recordMap[oldRecord.internalId];\r
918 }\r
919 if (infinite) {\r
920 item.translate(0, -10000);\r
921 } else {\r
922 item.hide();\r
923 }\r
924\r
925 if (header) {\r
926 if (infinite) {\r
927 header.translate(0, -10000);\r
928 } else {\r
929 header.hide();\r
930 }\r
931 }\r
932 item.$hidden = true;\r
933 return;\r
934 } else if (item.$hidden) {\r
935 if (!infinite) {\r
936 item.show();\r
937 }\r
938 item.$hidden = false;\r
939 }\r
940\r
941 if (infinite) {\r
942 updatedItems.push(item);\r
943 }\r
944\r
945 // If this item was previously used for the first record in the store, and now it will not be, then we hide\r
946 // any scrollDockTop items and change the isFirst flag\r
947 if (item.isFirst && index !== 0) {\r
948 ln = scrollDockItems.top.length;\r
949 for (i = 0; i < ln; i++) {\r
950 scrollDockItem = scrollDockItems.top[i];\r
951 if (infinite) {\r
952 scrollDockItem.translate(0, -10000);\r
953 }\r
954 }\r
955 item.isFirst = false;\r
956 }\r
957\r
958 // If this item was previously used for the last record in the store, and now it will not be, then we hide\r
959 // any scrollDockBottom items and change the istLast flag\r
960 if (item.isLast && index !== storeCount - 1) {\r
961 ln = scrollDockItems.bottom.length;\r
962 for (i = 0; i < ln; i++) {\r
963 scrollDockItem = scrollDockItems.bottom[i];\r
964 if (infinite) {\r
965 scrollDockItem.translate(0, -10000);\r
966 }\r
967 }\r
968 item.isLast = false;\r
969 }\r
970\r
971 // If the item is already bound to this record then we shouldn't have to do anything\r
972 if (item.$dataIndex !== index) {\r
973 item.$dataIndex = index;\r
974 me.fireEvent('itemindexchange', me, record, index, item);\r
975 }\r
976\r
977 // This is where we actually update the item with the record\r
978 if (oldRecord === record) {\r
979 item.updateRecord(record);\r
980 } else {\r
981 if (oldRecord) {\r
982 delete recordMap[oldRecord.internalId];\r
983 }\r
984 recordMap[record.internalId] = item;\r
985 item.setRecord(record);\r
986\r
987 viewModel = item.getViewModel();\r
988 if (viewModel) {\r
989 viewModel.set('record', record);\r
990 }\r
991 }\r
992\r
993 if (me.isSelected(record)) {\r
994 itemCls.push(info.selectedCls);\r
995 }\r
996\r
997 if (info.grouped) {\r
998 if (headerIndices[index]) {\r
999 itemCls.push(info.headerCls);\r
1000 headerCls.push(info.headerCls);\r
1001 header.setHtml(grouper.getGroupString(record));\r
1002\r
1003 if (!infinite) {\r
1004 header.renderElement.insertBefore(item.renderElement);\r
1005 }\r
1006 header.show();\r
1007 } else {\r
1008 if (infinite) {\r
1009 header.translate(0, -10000);\r
1010 } else {\r
1011 header.hide();\r
1012 }\r
1013 }\r
1014 if (footerIndices[index]) {\r
1015 itemCls.push(info.footerCls);\r
1016 headerCls.push(info.footerCls);\r
1017 }\r
1018 }\r
1019\r
1020 if (header && !info.grouped) {\r
1021 if (infinite) {\r
1022 header.translate(0, -10000);\r
1023 } else {\r
1024 header.hide();\r
1025 }\r
1026 }\r
1027\r
1028 if (index === 0) {\r
1029 item.isFirst = true;\r
1030 itemCls.push(info.firstCls);\r
1031 headerCls.push(info.firstCls);\r
1032\r
1033 if (!info.grouped) {\r
1034 itemCls.push(info.headerCls);\r
1035 headerCls.push(info.headerCls);\r
1036 }\r
1037\r
1038 if (!infinite) {\r
1039 for (i = 0, ln = scrollDockItems.top.length; i < ln; i++) {\r
1040 scrollDockItem = scrollDockItems.top[i];\r
1041 if (info.grouped) {\r
1042 scrollDockItem.renderElement.insertBefore(header.renderElement);\r
1043 } else {\r
1044 scrollDockItem.renderElement.insertBefore(item.renderElement);\r
1045 }\r
1046 }\r
1047 }\r
1048 }\r
1049\r
1050 if (index === storeCount - 1) {\r
1051 item.isLast = true;\r
1052 itemCls.push(info.lastCls);\r
1053 headerCls.push(info.lastCls);\r
1054\r
1055 if (!info.grouped) {\r
1056 itemCls.push(info.footerCls);\r
1057 headerCls.push(info.footerCls);\r
1058 }\r
1059\r
1060 if (!infinite) {\r
1061 for (i = 0, ln = scrollDockItems.bottom.length; i < ln; i++) {\r
1062 scrollDockItem = scrollDockItems.bottom[i];\r
1063 scrollDockItem.renderElement.insertAfter(item.renderElement);\r
1064 }\r
1065 }\r
1066 }\r
1067\r
1068 if (info.striped && index % 2 === 1) {\r
1069 itemCls.push(info.stripeCls);\r
1070 }\r
1071\r
1072 item.renderElement.replaceCls(itemRemoveCls, itemCls);\r
1073 if (header) {\r
1074 header.renderElement.replaceCls(headerRemoveCls, headerCls);\r
1075 }\r
1076 },\r
1077\r
1078 updateAllListItems: function() {\r
1079 var me = this,\r
1080 store, items, info, topRenderedIndex, i, ln;\r
1081\r
1082 if (!me.initialized) {\r
1083 return;\r
1084 }\r
1085\r
1086 store = me.getStore();\r
1087 items = me.listItems;\r
1088 info = me.getListItemInfo();\r
1089 topRenderedIndex = me.topRenderedIndex;\r
1090\r
1091 if (store) {\r
1092 for (i = 0, ln = items.length; i < ln; i++) {\r
1093 me.updateListItem(items[i], topRenderedIndex + i, info);\r
1094 }\r
1095 }\r
1096\r
1097 if (me.isPainted()) {\r
1098 if (me.getInfinite() && store && store.getCount()) {\r
1099 me.handleItemHeights();\r
1100 }\r
1101 me.refreshScroller();\r
1102 }\r
1103 },\r
1104\r
1105 doRefresh: function() {\r
1106 var me = this,\r
1107 infinite = me.getInfinite(),\r
1108 scroller = me.container.getScrollable(),\r
1109 storeCount = me.getStore().getCount();\r
1110\r
1111 if (infinite) {\r
1112 me.getItemMap().populate(storeCount, this.topRenderedIndex);\r
1113 }\r
1114\r
1115 if (me.getGrouped()) {\r
1116 me.refreshHeaderIndices();\r
1117 }\r
1118\r
1119 // This will refresh the items on the screen with the new data\r
1120 if (storeCount) {\r
1121 me.hideScrollDockItems();\r
1122 me.hideEmptyText();\r
1123 if (!infinite) {\r
1124 me.setItemsCount(storeCount);\r
1125 if (me.getScrollToTopOnRefresh()) {\r
1126 scroller.scrollTo(0, 0);\r
1127 }\r
1128 } else {\r
1129 if (me.getScrollToTopOnRefresh()) {\r
1130 me.topRenderedIndex = 0;\r
1131 me.topVisibleIndex = 0;\r
1132 scroller.scrollTo(null, 0);\r
1133 }\r
1134 me.updateAllListItems();\r
1135 }\r
1136 } else {\r
1137 me.onStoreClear();\r
1138 }\r
1139 },\r
1140\r
1141 updateStore: function(store, oldStore) {\r
1142 var me = this,\r
1143 container = me.container;\r
1144\r
1145 me.callParent([store, oldStore]);\r
1146\r
1147 if (me._fireResizeOnNextLoad && me.hasLoadedStore) {\r
1148 me._fireResizeOnNextLoad = false;\r
1149 me.onContainerResize(container, { height: container.element.getHeight() });\r
1150 }\r
1151 },\r
1152\r
1153 onLoad: function(store) {\r
1154 var me = this,\r
1155 container = me.container;\r
1156\r
1157 me.callParent([store]);\r
1158\r
1159 if (me._fireResizeOnNextLoad) {\r
1160 me._fireResizeOnNextLoad = false;\r
1161 me.onContainerResize(container, { height: container.element.getHeight() });\r
1162 }\r
1163 },\r
1164\r
1165 onContainerResize: function(container, size) {\r
1166 var me = this,\r
1167 store = me.getStore(),\r
1168 currentVisibleCount, newVisibleCount, minHeight, listItems, itemMap, itemConfig;\r
1169\r
1170 if (!me.headerHeight) {\r
1171 me.headerHeight = parseInt(me.pinnedHeader.renderElement.getHeight(), 10);\r
1172 }\r
1173\r
1174 if (me.getInfinite()) {\r
1175 itemMap = me.getItemMap();\r
1176 minHeight = itemMap.getMinimumHeight();\r
1177\r
1178 if (!store || (!store.getCount() && !store.isLoaded())) {\r
1179 // If the store is not yet loaded we can't measure the height of the first item\r
1180 // to determine minHeight\r
1181 // TODO: refactor\r
1182 me._fireResizeOnNextLoad = true;\r
1183 return;\r
1184 }\r
1185\r
1186 if (!minHeight) {\r
1187 listItems = me.listItems;\r
1188\r
1189 // If there was no itemHeight/minHeight specified, we measure and cache\r
1190 // the height of the first item for purposes of buffered rendering\r
1191 // caluclations.\r
1192 // TODO: this won't handle variable row heights\r
1193\r
1194 if (!listItems.length) {\r
1195 // make sure the list contains at least one item so that we can measure\r
1196 // its height from the dom. Don't worry about ending up with the wrong\r
1197 // number of items - it will be corrected when we invoke setItemsCount\r
1198 // shortly\r
1199 itemConfig = me.getListItemConfig();\r
1200 me.createItem(itemConfig);\r
1201 me.updateListItem(listItems[0], 0, me.getListItemInfo());\r
1202 me.visibleCount++;\r
1203\r
1204 }\r
1205\r
1206 minHeight = listItems[0].element.getHeight();\r
1207 // cache the minimum height on the itemMap for next time\r
1208 itemMap.setMinimumHeight(minHeight);\r
1209 me.getItemMap().populate(me.getStore().getCount(), me.topRenderedIndex);\r
1210 }\r
1211\r
1212 currentVisibleCount = me.visibleCount;\r
1213 newVisibleCount = Math.ceil(size.height / minHeight);\r
1214\r
1215 if (newVisibleCount != currentVisibleCount) {\r
1216 me.visibleCount = newVisibleCount;\r
1217 me.setItemsCount(newVisibleCount + me.getBufferSize(), itemConfig);\r
1218 // This is a private event used by some plugins\r
1219 me.fireEvent('updatevisiblecount', this, newVisibleCount, currentVisibleCount);\r
1220 }\r
1221 } else if (me.listItems.length && me.getGrouped() && me.getPinHeaders()) {\r
1222 // Whenever the container resizes, headers might be in different locations. For this reason\r
1223 // we refresh the header position map\r
1224 me.updateHeaderMap();\r
1225 }\r
1226 },\r
1227\r
1228 refreshScroller: function(skipOnRefresh) {\r
1229 var me = this,\r
1230 scroller = me.container.getScrollable(),\r
1231 infinite = me.getInfinite(),\r
1232 height, scrollSize;\r
1233\r
1234 if (infinite) {\r
1235 height = me.getItemMap().getTotalHeight();\r
1236 scrollSize = scroller.getSize();\r
1237\r
1238 if (height != scrollSize.y) {\r
1239 scroller.setSize({\r
1240 // When using a TouchScroller we can pass the x scrollSize to prevent\r
1241 // the scroller from reading the DOM to determine the x size.\r
1242 // When using a DomScroller we pass null so that only the y size gets set\r
1243 // (DomScroller does not need to read the dom to determine missing dimensions)\r
1244 x: scroller.isTouchScroller ? scrollSize.x : null,\r
1245 y: height\r
1246 });\r
1247 }\r
1248\r
1249 if (!skipOnRefresh) {\r
1250 me.onScrollerRefresh(scroller);\r
1251 }\r
1252 } else {\r
1253 if (me.getGrouped() && me.getPinHeaders()) {\r
1254 me.updateHeaderMap();\r
1255 }\r
1256 scroller.refresh();\r
1257 }\r
1258 },\r
1259\r
1260 updateHeaderMap: function() {\r
1261 var me = this,\r
1262 headerMap = me.headerMap,\r
1263 headerIndices = me.headerIndices,\r
1264 header, i;\r
1265\r
1266 headerMap.length = 0;\r
1267 for (i in headerIndices) {\r
1268 if (headerIndices.hasOwnProperty(i)) {\r
1269 header = me.getItemAt(i).getHeader();\r
1270 headerMap.push(header.renderElement.dom.offsetTop);\r
1271 }\r
1272 }\r
1273 },\r
1274\r
1275 applyVariableHeights: function(value) {\r
1276 if (!this.getInfinite()) {\r
1277 return true;\r
1278 }\r
1279 return value;\r
1280 },\r
1281\r
1282 applyDefaultType: function(defaultType) {\r
1283 if (!defaultType) {\r
1284 defaultType = this.getUseSimpleItems() ? 'simplelistitem' : 'listitem';\r
1285 }\r
1286 return defaultType;\r
1287 },\r
1288\r
1289 applyItemMap: function(itemMap) {\r
1290 return Ext.factory(itemMap, Ext.util.PositionMap, this.getItemMap());\r
1291 },\r
1292\r
1293 updateItemHeight: function(itemHeight) {\r
1294 this.getItemMap().setMinimumHeight(itemHeight);\r
1295 },\r
1296\r
1297 applyIndexBar: function(indexBar) {\r
1298 return Ext.factory(indexBar, Ext.dataview.IndexBar, this.getIndexBar());\r
1299 },\r
1300\r
1301 updatePinHeaders: function(pinnedHeaders) {\r
1302 if (this.isPainted()) {\r
1303 this.pinnedHeader.translate(0, pinnedHeaders ? this.pinnedHeader.$position : -10000);\r
1304 }\r
1305 },\r
1306\r
1307 updateItemTpl: function(newTpl) {\r
1308 var me = this,\r
1309 listItems = me.listItems,\r
1310 ln = listItems.length || 0,\r
1311 i, listItem;\r
1312\r
1313 for (i = 0; i < ln; i++) {\r
1314 listItem = listItems[i];\r
1315 listItem.setTpl(newTpl);\r
1316 }\r
1317\r
1318 me.updateAllListItems();\r
1319 },\r
1320\r
1321 updateItemCls: function(newCls, oldCls) {\r
1322 var items = this.listItems,\r
1323 ln = items.length,\r
1324 i, item;\r
1325\r
1326 for (i = 0; i < ln; i++) {\r
1327 item = items[i];\r
1328 item.removeCls(oldCls);\r
1329 item.addCls(newCls);\r
1330 }\r
1331 },\r
1332\r
1333 updateIndexBar: function(indexBar, oldIndexBar) {\r
1334 var me = this,\r
1335 scrollViewElement = me.scrollViewElement;\r
1336\r
1337 if (oldIndexBar) {\r
1338 oldIndexBar.un({\r
1339 index: 'onIndex',\r
1340 scope: me\r
1341 });\r
1342\r
1343 if (!indexBar) {\r
1344 me.element.removeCls(me.getBaseCls() + '-indexed');\r
1345 }\r
1346\r
1347 if (scrollViewElement) {\r
1348 scrollViewElement.removeChild(oldIndexBar.renderElement);\r
1349 }\r
1350 }\r
1351\r
1352 if (indexBar) {\r
1353 indexBar.on({\r
1354 index: 'onIndex',\r
1355 scope: me\r
1356 });\r
1357\r
1358 if (!oldIndexBar) {\r
1359 me.element.addCls(me.getBaseCls() + '-indexed');\r
1360 }\r
1361\r
1362 if (scrollViewElement) {\r
1363 scrollViewElement.appendChild(indexBar.renderElement);\r
1364 }\r
1365 }\r
1366 },\r
1367\r
1368 updateGrouped: function(grouped) {\r
1369 if (this.initialized) {\r
1370 this.handleGroupChange();\r
1371 }\r
1372 },\r
1373\r
1374 onStoreGroupChange: function() {\r
1375 if (this.initialized) {\r
1376 this.handleGroupChange();\r
1377 }\r
1378 },\r
1379\r
1380 onStoreAdd: function() {\r
1381 this.doRefresh();\r
1382 },\r
1383\r
1384 onStoreRemove: function() {\r
1385 this.doRefresh();\r
1386 },\r
1387\r
1388 onStoreUpdate: function(store, record, type, modifiedFieldNames, info) {\r
1389 var me = this,\r
1390 index, item;\r
1391\r
1392 if (me.getInfinite() || info.indexChanged) {\r
1393 me.doRefresh();\r
1394 } else {\r
1395 index = store.indexOf(record);\r
1396 item = me.listItems[index];\r
1397 if (item) {\r
1398 me.updateListItem(item, index, me.getListItemInfo());\r
1399 }\r
1400 }\r
1401 },\r
1402\r
1403 onStoreClear: function() {\r
1404 var me = this,\r
1405 scroller = me.container.getScrollable(),\r
1406 infinite = me.getInfinite();\r
1407\r
1408 if (me.pinnedHeader) {\r
1409 me.pinnedHeader.translate(0, -10000);\r
1410 }\r
1411\r
1412 me.getItemMap().populate(0, 0);\r
1413\r
1414 if (!infinite) {\r
1415 me.setItemsCount(0);\r
1416 } else {\r
1417 me.topRenderedIndex = 0;\r
1418 me.topVisibleIndex = 0;\r
1419 me.updateAllListItems();\r
1420 }\r
1421\r
1422 scroller.scrollTo(null, 0);\r
1423 me.refreshScroller();\r
1424 },\r
1425\r
1426 showEmptyScrollDock: function() {\r
1427 var me = this,\r
1428 infinite = me.getInfinite(),\r
1429 scrollDockItems = me.scrollDockItems,\r
1430 offset = 0,\r
1431 i, ln, item;\r
1432\r
1433 for (i = 0, ln = scrollDockItems.top.length; i < ln; i++) {\r
1434 item = scrollDockItems.top[i];\r
1435 if (infinite) {\r
1436 item.translate(0, offset);\r
1437 offset += item.$scrollDockHeight;\r
1438 } else {\r
1439 this.scrollElement.appendChild(item.renderElement);\r
1440 }\r
1441 }\r
1442\r
1443 for (i = 0, ln = scrollDockItems.bottom.length; i < ln; i++) {\r
1444 item = scrollDockItems.bottom[i];\r
1445 if (infinite) {\r
1446 item.translate(0, offset);\r
1447 offset += item.$scrollDockHeight;\r
1448 } else {\r
1449 this.scrollElement.appendChild(item.renderElement);\r
1450 }\r
1451 }\r
1452 },\r
1453\r
1454 hideScrollDockItems: function() {\r
1455 var me = this,\r
1456 infinite = me.getInfinite(),\r
1457 scrollDockItems = me.scrollDockItems,\r
1458 i, ln, item;\r
1459\r
1460 if (!infinite) {\r
1461 return;\r
1462 }\r
1463\r
1464 for (i = 0, ln = scrollDockItems.top.length; i < ln; i++) {\r
1465 item = scrollDockItems.top[i];\r
1466 item.translate(0, -10000);\r
1467 }\r
1468\r
1469 for (i = 0, ln = scrollDockItems.bottom.length; i < ln; i++) {\r
1470 item = scrollDockItems.bottom[i];\r
1471 item.translate(0, -10000);\r
1472 }\r
1473 },\r
1474\r
1475 /**\r
1476 * Gets a list item by record.\r
1477 * @param {Ext.data.Model} The record\r
1478 * @return {Ext.dataview.component.(Simple)ListItem} The list item, if found.\r
1479 * `null` if no matching item exists.\r
1480 */\r
1481 getItem: function(record) {\r
1482 var item;\r
1483 if (record) {\r
1484 item = this.recordMap[record.internalId];\r
1485 }\r
1486 return item || null;\r
1487 },\r
1488\r
1489 /**\r
1490 * Returns an item at the specified index.\r
1491 * @param {Number} index Index of the item.\r
1492 * @return {Ext.dom.Element/Ext.dataview.component.DataItem} item Item at the specified index.\r
1493 */\r
1494 getItemAt: function(index) {\r
1495 var listItems = this.listItems,\r
1496 ln = listItems.length,\r
1497 i, listItem;\r
1498\r
1499 for (i = 0; i < ln; i++) {\r
1500 listItem = listItems[i];\r
1501 if (listItem.$dataIndex == index) {\r
1502 return listItem;\r
1503 }\r
1504 }\r
1505 },\r
1506\r
1507 /**\r
1508 * Returns an index for the specified item.\r
1509 * @param {Number} item The item to locate.\r
1510 * @return {Number} Index for the specified item.\r
1511 */\r
1512 getItemIndex: function(item) {\r
1513 return item.$dataIndex;\r
1514 },\r
1515\r
1516 /**\r
1517 * Returns an array of the current items in the DataView.\r
1518 * @return {Ext.dom.Element[]/Ext.dataview.component.DataItem[]} Array of Items.\r
1519 */\r
1520 getViewItems: function() {\r
1521 return this.listItems;\r
1522 },\r
1523\r
1524 getListItemInfo: function() {\r
1525 var me = this,\r
1526 baseCls = me.getBaseCls();\r
1527\r
1528 return {\r
1529 store: me.getStore(),\r
1530 grouped: me.isGrouping(),\r
1531 baseCls: baseCls,\r
1532 selectedCls: me.getSelectedCls(),\r
1533 headerCls: baseCls + '-header-wrap',\r
1534 footerCls: baseCls + '-footer-wrap',\r
1535 firstCls: baseCls + '-item-first',\r
1536 lastCls: baseCls + '-item-last',\r
1537 stripeCls: baseCls + '-item-odd',\r
1538 striped: me.getStriped(),\r
1539 itemMap: me.getItemMap(),\r
1540 defaultItemHeight: me.getItemHeight()\r
1541 };\r
1542 },\r
1543\r
1544 getListItemConfig: function() {\r
1545 var me = this,\r
1546 minimumHeight = me.getItemMap().getMinimumHeight(),\r
1547 config = {\r
1548 xtype: me.getDefaultType(),\r
1549 tpl: me.getItemTpl(),\r
1550 minHeight: minimumHeight,\r
1551 cls: me.getItemCls()\r
1552 };\r
1553\r
1554 if (me.getInfinite()) {\r
1555 config.translatable = {\r
1556 translationMethod: this.translationMethod\r
1557 };\r
1558 }\r
1559\r
1560 if (!me.getVariableHeights()) {\r
1561 config.height = minimumHeight;\r
1562 }\r
1563\r
1564 return Ext.merge(config, me.getItemConfig());\r
1565 },\r
1566\r
1567 refreshHeaderIndices: function() {\r
1568 var me = this,\r
1569 store = me.getStore(),\r
1570 storeLn = store && store.getCount(),\r
1571 groups = store.getGrouper() ? store.getGroups() : null,\r
1572 grouped = me.getGrouped(),\r
1573 headerIndices = me.headerIndices = {},\r
1574 footerIndices = me.footerIndices = {},\r
1575 i, previousIndex, firstGroupedRecord, storeIndex, groupLn;\r
1576\r
1577 if (!grouped || !groups) {\r
1578 return footerIndices;\r
1579 }\r
1580 groupLn = groups.length;\r
1581 me.groups = groups;\r
1582\r
1583 for (i = 0; i < groupLn; i++) {\r
1584 firstGroupedRecord = groups.getAt(i).getAt(0);\r
1585 storeIndex = store.indexOf(firstGroupedRecord);\r
1586 headerIndices[storeIndex] = true;\r
1587\r
1588 previousIndex = storeIndex - 1;\r
1589 if (previousIndex >= 0) {\r
1590 footerIndices[previousIndex] = true;\r
1591 }\r
1592 }\r
1593\r
1594 footerIndices[storeLn - 1] = true;\r
1595\r
1596 return headerIndices;\r
1597 },\r
1598\r
1599 onIndex: function(indexBar, index) {\r
1600 var me = this,\r
1601 key = index.toLowerCase(),\r
1602 store = me.getStore(),\r
1603 groups = store.getGroups(),\r
1604 ln = groups.length,\r
1605 group, groupKey, i, closest;\r
1606\r
1607 for (i = 0; i < ln; i++) {\r
1608 group = groups.getAt(i);\r
1609 groupKey = group.getGroupKey().toLowerCase();\r
1610 if (groupKey >= key) {\r
1611 closest = group;\r
1612 break;\r
1613 }\r
1614 else {\r
1615 closest = group;\r
1616 }\r
1617 }\r
1618\r
1619 if (closest) {\r
1620 this.scrollToRecord(closest.getAt(0));\r
1621 }\r
1622 },\r
1623\r
1624 /**\r
1625 *\r
1626 * Scrolls the list so that the specified record is at the top.\r
1627 *\r
1628 * @param record {Ext.data.Model} Record in the lists store to scroll to\r
1629 * @param animate {Boolean} Determines if scrolling is animated to a cut\r
1630 * @param overscroll {Boolean} Determines if list can be overscrolled\r
1631 */\r
1632 scrollToRecord: function(record, animate, overscroll) {\r
1633 var me = this,\r
1634 scroller = me.container.getScrollable(),\r
1635 store = me.getStore(),\r
1636 index = store.indexOf(record),\r
1637 header;\r
1638\r
1639 //stop the scroller from scrolling\r
1640 scroller.stopAnimation();\r
1641\r
1642 //make sure the new offsetTop is not out of bounds for the scroller\r
1643 var elementHeight = scroller.getElement().getHeight(),\r
1644 scrollHeight = scroller.getSize().y,\r
1645 maxOffset = scrollHeight - elementHeight,\r
1646 offset, item;\r
1647\r
1648 if (me.getInfinite()) {\r
1649 offset = me.getItemMap().map[index];\r
1650 }\r
1651 else {\r
1652 item = me.listItems[index];\r
1653 header = item.getHeader && item.getHeader();\r
1654\r
1655 if (header && header.isPainted()) {\r
1656 offset = header.renderElement.dom.offsetTop;\r
1657 }\r
1658 else {\r
1659 offset = item.renderElement.dom.offsetTop;\r
1660 }\r
1661 }\r
1662\r
1663 if (!overscroll) {\r
1664 offset = Math.min(offset, maxOffset);\r
1665 }\r
1666\r
1667 scroller.scrollTo(0, offset, !!animate);\r
1668 },\r
1669\r
1670 onItemAdd: function(item) {\r
1671 var me = this,\r
1672 config = item.config;\r
1673\r
1674 if (config.scrollDock) {\r
1675 if (config.scrollDock == 'bottom') {\r
1676 me.scrollDockItems.bottom.push(item);\r
1677 } else {\r
1678 me.scrollDockItems.top.push(item);\r
1679 }\r
1680\r
1681 if (me.getInfinite()) {\r
1682 item.on({\r
1683 resize: 'onScrollDockItemResize',\r
1684 scope: this\r
1685 });\r
1686\r
1687 item.addCls(me.getBaseCls() + '-scrolldockitem');\r
1688 item.setTranslatable({\r
1689 translationMethod: this.translationMethod\r
1690 });\r
1691 item.translate(0, -10000);\r
1692 item.$scrollDockHeight = 0;\r
1693 }\r
1694\r
1695 me.container.doAdd(item);\r
1696 } else {\r
1697 me.callParent(arguments);\r
1698 }\r
1699 },\r
1700\r
1701 /**\r
1702 * Returns all the items that are docked in the scroller in this list.\r
1703 * @return {Array} An array of the scrollDock items\r
1704 */\r
1705 getScrollDockedItems: function() {\r
1706 return this.scrollDockItems.bottom.slice().concat(this.scrollDockItems.top.slice());\r
1707 },\r
1708\r
1709 onScrollDockItemResize: function(dockItem, size) {\r
1710 var me = this,\r
1711 items = me.listItems,\r
1712 ln = items.length,\r
1713 i, item;\r
1714\r
1715 Ext.getCmp(dockItem.id).$scrollDockHeight = size.height;\r
1716\r
1717 for (i = 0; i < ln; i++) {\r
1718 item = items[i];\r
1719 if (item.isLast) {\r
1720 me.updatedItems.push(item);\r
1721 if (me.isPainted()) {\r
1722 me.refreshScroller();\r
1723 }\r
1724 break;\r
1725 }\r
1726 }\r
1727 },\r
1728\r
1729 onItemTouchStart: function(e) {\r
1730 this.container.innerElement.on({\r
1731 touchmove: 'onItemTouchMove',\r
1732 delegate: '.' + Ext.baseCSSPrefix + 'list-item',\r
1733 single: true,\r
1734 scope: this\r
1735 });\r
1736 this.callParent(this.parseEvent(e));\r
1737 },\r
1738\r
1739 onItemTouchMove: function(e) {\r
1740 this.callParent(this.parseEvent(e));\r
1741 },\r
1742\r
1743 onItemTouchEnd: function(e) {\r
1744 this.container.innerElement.un({\r
1745 touchmove: 'onItemTouchMove',\r
1746 delegate: '.' + Ext.baseCSSPrefix + 'list-item',\r
1747 scope: this\r
1748 });\r
1749 this.callParent(this.parseEvent(e));\r
1750 },\r
1751\r
1752 onItemTap: function(e) {\r
1753 this.callParent(this.parseEvent(e));\r
1754 },\r
1755\r
1756 onItemTapHold: function(e) {\r
1757 this.callParent(this.parseEvent(e));\r
1758 },\r
1759\r
1760 onItemSingleTap: function(e) {\r
1761 this.callParent(this.parseEvent(e));\r
1762 },\r
1763\r
1764 onItemDoubleTap: function(e) {\r
1765 this.callParent(this.parseEvent(e));\r
1766 },\r
1767\r
1768 onItemSwipe: function(e) {\r
1769 this.callParent(this.parseEvent(e));\r
1770 },\r
1771\r
1772 parseEvent: function(e) {\r
1773 var me = this,\r
1774 target = Ext.fly(e.currentTarget).findParent('.' + Ext.baseCSSPrefix + 'list-item', 8),\r
1775 item = Ext.getCmp(target.id);\r
1776\r
1777 return [me, item, item.$dataIndex, e];\r
1778 },\r
1779\r
1780 applyOnItemDisclosure: function(config) {\r
1781 if (Ext.isFunction(config)) {\r
1782 return {\r
1783 scope: this,\r
1784 handler: config\r
1785 };\r
1786 }\r
1787 return config;\r
1788 },\r
1789\r
1790 handleItemDisclosure: function(e) {\r
1791 var me = this,\r
1792 item = Ext.getCmp(Ext.get(e.currentTarget).up('.x-list-item').id),\r
1793 index = item.$dataIndex,\r
1794 record = me.getStore().getAt(index);\r
1795\r
1796 me.fireAction('disclose', [me, record, item, index, e], 'doDisclose');\r
1797 },\r
1798\r
1799 doDisclose: function(me, record, item, index, e) {\r
1800 var onItemDisclosure = me.getOnItemDisclosure();\r
1801\r
1802 if (onItemDisclosure && onItemDisclosure.handler) {\r
1803 onItemDisclosure.handler.call(onItemDisclosure.scope || me, record, item, index, e);\r
1804 }\r
1805 },\r
1806\r
1807 // apply to the selection model to maintain visual UI cues\r
1808 onItemTrigger: function(me, index, target, record, e) {\r
1809 if (!(this.getPreventSelectionOnDisclose() && Ext.fly(e.target).hasCls(this.getBaseCls() + '-disclosure'))) {\r
1810 this.callParent(arguments);\r
1811 }\r
1812 },\r
1813\r
1814 destroy: function() {\r
1815 var me = this,\r
1816 items = me.listItems,\r
1817 ln = items.length,\r
1818 i;\r
1819 \r
1820 if (me.pinnedHeader) {\r
1821 me.pinnedHeader.destroy();\r
1822 me.pinnedHeader = null;\r
1823 }\r
1824\r
1825 me.callParent();\r
1826\r
1827 if (me.onIdleBound) {\r
1828 Ext.AnimationQueue.unIdle(me.onAnimationIdle, me);\r
1829 }\r
1830\r
1831 for (i = 0; i < ln; i++) {\r
1832 items[i].destroy();\r
1833 }\r
1834 me.recordMap = me.listItems = null;\r
1835 },\r
1836\r
1837 privates: {\r
1838 handleGroupChange: function() {\r
1839 var me = this,\r
1840 grouped = me.isGrouping(),\r
1841 baseCls = this.getBaseCls(),\r
1842 infinite = me.getInfinite(),\r
1843 pinnedHeader = me.pinnedHeader,\r
1844 cls = baseCls + '-grouped',\r
1845 unCls = baseCls + '-ungrouped';\r
1846\r
1847 if (pinnedHeader) {\r
1848 pinnedHeader.translate(0, -10000);\r
1849 }\r
1850\r
1851 if (grouped) {\r
1852 me.addCls(cls);\r
1853 me.removeCls(unCls);\r
1854 } else {\r
1855 me.addCls(unCls);\r
1856 me.removeCls(cls);\r
1857 }\r
1858\r
1859 if (infinite) {\r
1860 me.refreshHeaderIndices();\r
1861 me.handleItemHeights();\r
1862 }\r
1863 me.updateAllListItems();\r
1864 if (infinite) {\r
1865 me.handleItemTransforms();\r
1866 }\r
1867 },\r
1868\r
1869 isGrouping: function() {\r
1870 return Boolean(this.getGrouped() && this.getStore().getGrouper());\r
1871 }\r
1872 }\r
1873});\r