]>
Commit | Line | Data |
---|---|---|
6527f429 DM |
1 | /**\r |
2 | * NestedList provides a miller column interface to navigate between nested sets\r | |
3 | * and provide a clean interface with limited screen real-estate.\r | |
4 | *\r | |
5 | * @example miniphone preview\r | |
6 | * var data = {\r | |
7 | * text: 'Groceries',\r | |
8 | * items: [{\r | |
9 | * text: 'Drinks',\r | |
10 | * items: [{\r | |
11 | * text: 'Water',\r | |
12 | * items: [{\r | |
13 | * text: 'Sparkling',\r | |
14 | * leaf: true\r | |
15 | * }, {\r | |
16 | * text: 'Still',\r | |
17 | * leaf: true\r | |
18 | * }]\r | |
19 | * }, {\r | |
20 | * text: 'Coffee',\r | |
21 | * leaf: true\r | |
22 | * }, {\r | |
23 | * text: 'Espresso',\r | |
24 | * leaf: true\r | |
25 | * }, {\r | |
26 | * text: 'Redbull',\r | |
27 | * leaf: true\r | |
28 | * }, {\r | |
29 | * text: 'Coke',\r | |
30 | * leaf: true\r | |
31 | * }, {\r | |
32 | * text: 'Diet Coke',\r | |
33 | * leaf: true\r | |
34 | * }]\r | |
35 | * }, {\r | |
36 | * text: 'Fruit',\r | |
37 | * items: [{\r | |
38 | * text: 'Bananas',\r | |
39 | * leaf: true\r | |
40 | * }, {\r | |
41 | * text: 'Lemon',\r | |
42 | * leaf: true\r | |
43 | * }]\r | |
44 | * }, {\r | |
45 | * text: 'Snacks',\r | |
46 | * items: [{\r | |
47 | * text: 'Nuts',\r | |
48 | * leaf: true\r | |
49 | * }, {\r | |
50 | * text: 'Pretzels',\r | |
51 | * leaf: true\r | |
52 | * }, {\r | |
53 | * text: 'Wasabi Peas',\r | |
54 | * leaf: true\r | |
55 | * }]\r | |
56 | * }]\r | |
57 | * };\r | |
58 | *\r | |
59 | * Ext.define('ListItem', {\r | |
60 | * extend: 'Ext.data.Model',\r | |
61 | * config: {\r | |
62 | * fields: [{\r | |
63 | * name: 'text',\r | |
64 | * type: 'string'\r | |
65 | * }]\r | |
66 | * }\r | |
67 | * });\r | |
68 | *\r | |
69 | * var store = Ext.create('Ext.data.TreeStore', {\r | |
70 | * model: 'ListItem',\r | |
71 | * defaultRootProperty: 'items',\r | |
72 | * root: data\r | |
73 | * });\r | |
74 | *\r | |
75 | * var nestedList = Ext.create('Ext.NestedList', {\r | |
76 | * fullscreen: true,\r | |
77 | * title: 'Groceries',\r | |
78 | * displayField: 'text',\r | |
79 | * store: store\r | |
80 | * });\r | |
81 | */\r | |
82 | Ext.define('Ext.dataview.NestedList', {\r | |
83 | alternateClassName: 'Ext.NestedList',\r | |
84 | extend: 'Ext.Container',\r | |
85 | xtype: 'nestedlist',\r | |
86 | requires: [\r | |
87 | 'Ext.dataview.List',\r | |
88 | 'Ext.TitleBar',\r | |
89 | 'Ext.Button',\r | |
90 | 'Ext.XTemplate',\r | |
91 | 'Ext.data.StoreManager',\r | |
92 | 'Ext.data.TreeStore',\r | |
93 | 'Ext.data.NodeStore'\r | |
94 | ],\r | |
95 | \r | |
96 | config: {\r | |
97 | /**\r | |
98 | * @cfg\r | |
99 | * @inheritdoc\r | |
100 | */\r | |
101 | baseCls: Ext.baseCSSPrefix + 'nested-list',\r | |
102 | \r | |
103 | /**\r | |
104 | * @cfg {String/Object/Boolean} cardSwitchAnimation\r | |
105 | * Animation to be used during transitions of cards.\r | |
106 | * @removed 2.0.0 please use {@link Ext.layout.Card#animation}\r | |
107 | */\r | |
108 | \r | |
109 | /**\r | |
110 | * @cfg {String} backText\r | |
111 | * The label to display for the back button.\r | |
112 | * @accessor\r | |
113 | */\r | |
114 | backText: 'Back',\r | |
115 | \r | |
116 | /**\r | |
117 | * @cfg {Boolean} useTitleAsBackText\r | |
118 | * `true` to use title as a label for back button.\r | |
119 | * @accessor\r | |
120 | */\r | |
121 | useTitleAsBackText: true,\r | |
122 | \r | |
123 | /**\r | |
124 | * @cfg {Boolean} updateTitleText\r | |
125 | * Update the title with the currently selected category.\r | |
126 | * @accessor\r | |
127 | */\r | |
128 | updateTitleText: true,\r | |
129 | \r | |
130 | /**\r | |
131 | * @cfg {String} displayField\r | |
132 | * Display field to use when setting item text and title.\r | |
133 | * This configuration is ignored when overriding {@link #getItemTextTpl} or\r | |
134 | * {@link #getTitleTextTpl} for the item text or title.\r | |
135 | * @accessor\r | |
136 | */\r | |
137 | displayField: 'text',\r | |
138 | \r | |
139 | /**\r | |
140 | * @cfg {String} loadingText\r | |
141 | * Loading text to display when a subtree is loading.\r | |
142 | * @accessor\r | |
143 | */\r | |
144 | loadingText: 'Loading...',\r | |
145 | \r | |
146 | /**\r | |
147 | * @cfg {String} emptyText\r | |
148 | * Empty text to display when a subtree is empty.\r | |
149 | * @accessor\r | |
150 | */\r | |
151 | emptyText: 'No items available.',\r | |
152 | \r | |
153 | /**\r | |
154 | * @cfg {Boolean/Function} onItemDisclosure\r | |
155 | * Maps to the {@link Ext.List#onItemDisclosure} configuration for individual lists.\r | |
156 | * @accessor\r | |
157 | */\r | |
158 | onItemDisclosure: false,\r | |
159 | \r | |
160 | /**\r | |
161 | * @cfg {Boolean} allowDeselect\r | |
162 | * Set to `true` to allow the user to deselect leaf items via interaction.\r | |
163 | * @accessor\r | |
164 | */\r | |
165 | allowDeselect: false,\r | |
166 | \r | |
167 | /**\r | |
168 | * @deprecated 2.0.0 Please set the {@link #toolbar} configuration to `false` instead\r | |
169 | * @cfg {Boolean} useToolbar `true` to show the header toolbar.\r | |
170 | * @accessor\r | |
171 | */\r | |
172 | useToolbar: null,\r | |
173 | \r | |
174 | /**\r | |
175 | * @cfg {Ext.Toolbar/Object/Boolean} toolbar\r | |
176 | * The configuration to be used for the toolbar displayed in this nested list.\r | |
177 | * @accessor\r | |
178 | */\r | |
179 | toolbar: {\r | |
180 | docked: 'top',\r | |
181 | xtype: 'titlebar',\r | |
182 | ui: 'light',\r | |
183 | inline: true\r | |
184 | },\r | |
185 | \r | |
186 | /**\r | |
187 | * @cfg {String} title The title of the toolbar\r | |
188 | * @accessor\r | |
189 | */\r | |
190 | title: '',\r | |
191 | \r | |
192 | /**\r | |
193 | * @cfg {String} layout\r | |
194 | * @hide\r | |
195 | * @accessor\r | |
196 | */\r | |
197 | layout: {\r | |
198 | type: 'card',\r | |
199 | animation: {\r | |
200 | type: 'slide',\r | |
201 | duration: 250,\r | |
202 | direction: 'left'\r | |
203 | }\r | |
204 | },\r | |
205 | \r | |
206 | /**\r | |
207 | * @cfg {Ext.data.TreeStore/String} store The tree store to be used for this nested list.\r | |
208 | */\r | |
209 | store: null,\r | |
210 | \r | |
211 | /**\r | |
212 | * @cfg {Ext.Container} detailContainer The container of the `detailCard`.\r | |
213 | * A detailContainer is a reference to the container where a detail card\r | |
214 | * displays.\r | |
215 | *\r | |
216 | * See http://docs.sencha.com/touch/2-2/#!/guide/nested_list-section-4\r | |
217 | * and http://en.wikipedia.org/wiki/Miller_columns\r | |
218 | *\r | |
219 | * The two possible values for a detailContainer are undefined (default),\r | |
220 | * which indicates that a detailCard appear in the same container, or you\r | |
221 | * can specify a new container location. The default condition uses the\r | |
222 | * current List container.\r | |
223 | *\r | |
224 | * The following example shows creating a location for a detailContainer:\r | |
225 | *\r | |
226 | * var detailContainer = Ext.create('Ext.Container', {\r | |
227 | * layout: 'card'\r | |
228 | * });\r | |
229 | *\r | |
230 | * var nestedList = Ext.create('Ext.NestedList', {\r | |
231 | * store: treeStore,\r | |
232 | * detailCard: true,\r | |
233 | * detailContainer: detailContainer\r | |
234 | * });\r | |
235 | *\r | |
236 | * The default value is typically used for phone devices in portrait mode\r | |
237 | * where the small screen size dictates that the detailCard replace the\r | |
238 | * current container.\r | |
239 | * @accessor\r | |
240 | */\r | |
241 | detailContainer: undefined,\r | |
242 | \r | |
243 | /**\r | |
244 | * @cfg {Ext.Component} detailCard provides the information for a leaf\r | |
245 | * in a Miller column list. In a Miller column, users follow a\r | |
246 | * hierarchial tree structure to a leaf, which provides information\r | |
247 | * about the item in the list. The detailCard lists the information at\r | |
248 | * the leaf.\r | |
249 | *\r | |
250 | * See http://docs.sencha.com/touch/2-2/#!/guide/nested_list-section-3\r | |
251 | * and http://en.wikipedia.org/wiki/Miller_columns\r | |
252 | *\r | |
253 | * @accessor\r | |
254 | */\r | |
255 | detailCard: null,\r | |
256 | \r | |
257 | /**\r | |
258 | * @cfg {Object} backButton The configuration for the back button used in the nested list.\r | |
259 | */\r | |
260 | backButton: {\r | |
261 | ui: 'back',\r | |
262 | hidden: true\r | |
263 | },\r | |
264 | \r | |
265 | /**\r | |
266 | * @cfg {Object} listConfig An optional config object which is merged with the default\r | |
267 | * configuration used to create each nested list.\r | |
268 | */\r | |
269 | listConfig: null,\r | |
270 | \r | |
271 | /**\r | |
272 | * @cfg {Boolean} useSimpleItems\r | |
273 | * Set this to false if you want the lists in this NestedList to create complex container list items.\r | |
274 | */\r | |
275 | useSimpleItems: true,\r | |
276 | \r | |
277 | /**\r | |
278 | * @cfg {Number} itemHeight\r | |
279 | * This allows you to set the default item height and is used to roughly calculate the amount\r | |
280 | * of items needed to fill the list. By default items are around 50px high. If you set this\r | |
281 | * configuration in combination with setting the {@link #variableHeights} to false you\r | |
282 | * can improve the scrolling speed\r | |
283 | */\r | |
284 | itemHeight: null,\r | |
285 | \r | |
286 | /**\r | |
287 | * @cfg {Boolean} variableHeights\r | |
288 | * This configuration allows you optimize the picker by not having it read the DOM heights of list items.\r | |
289 | * Instead it will assume (and set) the height to be the {@link #itemHeight}.\r | |
290 | */\r | |
291 | variableHeights: false,\r | |
292 | \r | |
293 | /**\r | |
294 | * @private\r | |
295 | */\r | |
296 | lastNode: null,\r | |
297 | \r | |
298 | /**\r | |
299 | * @private\r | |
300 | */\r | |
301 | lastActiveList: null,\r | |
302 | \r | |
303 | ui: null,\r | |
304 | \r | |
305 | clearSelectionOnListChange: true\r | |
306 | },\r | |
307 | \r | |
308 | /**\r | |
309 | * @event itemtap\r | |
310 | * Fires when a node is tapped on.\r | |
311 | * @param {Ext.dataview.NestedList} this\r | |
312 | * @param {Ext.dataview.List} list The Ext.dataview.List that is currently active.\r | |
313 | * @param {Number} index The index of the item tapped.\r | |
314 | * @param {Ext.dom.Element} target The element tapped.\r | |
315 | * @param {Ext.data.Record} record The record tapped.\r | |
316 | * @param {Ext.event.Event} e The event object.\r | |
317 | */\r | |
318 | \r | |
319 | /**\r | |
320 | * @event itemdoubletap\r | |
321 | * Fires when a node is double tapped on.\r | |
322 | * @param {Ext.dataview.NestedList} this\r | |
323 | * @param {Ext.dataview.List} list The Ext.dataview.List that is currently active.\r | |
324 | * @param {Number} index The index of the item that was tapped.\r | |
325 | * @param {Ext.dom.Element} target The element tapped.\r | |
326 | * @param {Ext.data.Record} record The record tapped.\r | |
327 | * @param {Ext.event.Event} e The event object.\r | |
328 | */\r | |
329 | \r | |
330 | /**\r | |
331 | * @event containertap\r | |
332 | * Fires when a tap occurs and it is not on a template node.\r | |
333 | * @param {Ext.dataview.NestedList} this\r | |
334 | * @param {Ext.dataview.List} list The Ext.dataview.List that is currently active.\r | |
335 | * @param {Ext.event.Event} e The raw event object.\r | |
336 | */\r | |
337 | \r | |
338 | /**\r | |
339 | * @event selectionchange\r | |
340 | * Fires when the selected nodes change.\r | |
341 | * @param {Ext.dataview.NestedList} this\r | |
342 | * @param {Ext.dataview.List} list The Ext.dataview.List that is currently active.\r | |
343 | * @param {Array} selections Array of the selected nodes.\r | |
344 | */\r | |
345 | \r | |
346 | /**\r | |
347 | * @event beforeselectionchange\r | |
348 | * Fires before a selection is made.\r | |
349 | * @param {Ext.dataview.NestedList} this\r | |
350 | * @param {Ext.dataview.List} list The Ext.dataview.List that is currently active.\r | |
351 | * @param {HTMLElement} node The node to be selected.\r | |
352 | * @param {Array} selections Array of currently selected nodes.\r | |
353 | * @deprecated 2.0.0 Please listen to the {@link #selectionchange} event with an order of `before` instead.\r | |
354 | */\r | |
355 | \r | |
356 | /**\r | |
357 | * @event listchange\r | |
358 | * Fires when the user taps a list item.\r | |
359 | * @param {Ext.dataview.NestedList} this\r | |
360 | * @param {Object} listitem The new active list.\r | |
361 | */\r | |
362 | \r | |
363 | /**\r | |
364 | * @event leafitemtap\r | |
365 | * Fires when the user taps a leaf list item.\r | |
366 | * @param {Ext.dataview.NestedList} this\r | |
367 | * @param {Ext.List} list The subList the item is on.\r | |
368 | * @param {Number} index The index of the item tapped.\r | |
369 | * @param {Ext.dom.Element} target The element tapped.\r | |
370 | * @param {Ext.data.Record} record The record tapped.\r | |
371 | * @param {Ext.event.Event} e The event.\r | |
372 | */\r | |
373 | \r | |
374 | /**\r | |
375 | * @event back\r | |
376 | * @preventable\r | |
377 | * Fires when the user taps Back.\r | |
378 | * @param {Ext.dataview.NestedList} this\r | |
379 | * @param {HTMLElement} node The node to be selected.\r | |
380 | * @param {Ext.dataview.List} lastActiveList The Ext.dataview.List that was last active.\r | |
381 | * @param {Boolean} detailCardActive Flag set if the detail card is currently active.\r | |
382 | */\r | |
383 | \r | |
384 | /**\r | |
385 | * @event beforeload\r | |
386 | * Fires before a request is made for a new data object.\r | |
387 | * @param {Ext.dataview.NestedList} this\r | |
388 | * @param {Ext.data.Store} store The store instance.\r | |
389 | * @param {Ext.data.Operation} operation The Ext.data.Operation object that will be passed to the Proxy to\r | |
390 | * load the Store.\r | |
391 | */\r | |
392 | \r | |
393 | /**\r | |
394 | * @event load\r | |
395 | * Fires whenever records have been loaded into the store.\r | |
396 | * @param {Ext.dataview.NestedList} this\r | |
397 | * @param {Ext.data.Store} store The store instance.\r | |
398 | * @param {Ext.util.Grouper[]} records An array of records.\r | |
399 | * @param {Boolean} successful `true` if the operation was successful.\r | |
400 | * @param {Ext.data.Operation} operation The associated operation.\r | |
401 | */\r | |
402 | constructor: function (config) {\r | |
403 | if (Ext.isObject(config)) {\r | |
404 | if (config.getTitleTextTpl) {\r | |
405 | this.getTitleTextTpl = config.getTitleTextTpl;\r | |
406 | }\r | |
407 | if (config.getItemTextTpl) {\r | |
408 | this.getItemTextTpl = config.getItemTextTpl;\r | |
409 | }\r | |
410 | }\r | |
411 | this.callParent([config]);\r | |
412 | },\r | |
413 | \r | |
414 | onItemInteraction: function () {\r | |
415 | if (this.isGoingTo) {\r | |
416 | return false;\r | |
417 | }\r | |
418 | },\r | |
419 | \r | |
420 | applyDetailContainer: function (config) {\r | |
421 | if (!config) {\r | |
422 | config = this;\r | |
423 | }\r | |
424 | \r | |
425 | return config;\r | |
426 | },\r | |
427 | \r | |
428 | updateDetailContainer: function (newContainer, oldContainer) {\r | |
429 | if (newContainer) {\r | |
430 | newContainer.on('beforeactiveitemchange', 'onBeforeDetailContainerChange', this);\r | |
431 | newContainer.on('activeitemchange', 'onDetailContainerChange', this);\r | |
432 | }\r | |
433 | },\r | |
434 | \r | |
435 | onBeforeDetailContainerChange: function () {\r | |
436 | this.isGoingTo = true;\r | |
437 | },\r | |
438 | \r | |
439 | onDetailContainerChange: function () {\r | |
440 | this.isGoingTo = false;\r | |
441 | },\r | |
442 | \r | |
443 | /**\r | |
444 | * Called when an list item has been tapped.\r | |
445 | * @param {Ext.List} list The subList the item is on.\r | |
446 | * @param {Number} index The id of the item tapped.\r | |
447 | * @param {Ext.Element} target The list item tapped.\r | |
448 | * @param {Ext.data.Record} record The record which as tapped.\r | |
449 | * @param {Ext.event.Event} e The event.\r | |
450 | */\r | |
451 | onItemTap: function (list, index, target, record, e) {\r | |
452 | var me = this,\r | |
453 | store = list.getStore(),\r | |
454 | node = store.getAt(index);\r | |
455 | \r | |
456 | me.fireEvent('itemtap', this, list, index, target, record, e);\r | |
457 | if (node.isLeaf()) {\r | |
458 | me.fireEvent('leafitemtap', this, list, index, target, record, e);\r | |
459 | me.goToLeaf(node);\r | |
460 | }\r | |
461 | else {\r | |
462 | this.goToNode(node);\r | |
463 | }\r | |
464 | },\r | |
465 | \r | |
466 | onBeforeSelect: function () {\r | |
467 | this.fireEvent.apply(this, [].concat('beforeselect', this, Array.prototype.slice.call(arguments)));\r | |
468 | },\r | |
469 | \r | |
470 | onContainerTap: function () {\r | |
471 | this.fireEvent.apply(this, [].concat('containertap', this, Array.prototype.slice.call(arguments)));\r | |
472 | },\r | |
473 | \r | |
474 | onSelectionChange: function () {\r | |
475 | this.fireEvent.apply(this, [].concat('selectionchange', this, Array.prototype.slice.call(arguments)));\r | |
476 | },\r | |
477 | \r | |
478 | onItemDoubleTap: function () {\r | |
479 | this.fireEvent.apply(this, [].concat('itemdoubletap', this, Array.prototype.slice.call(arguments)));\r | |
480 | },\r | |
481 | \r | |
482 | onStoreBeforeLoad: function () {\r | |
483 | var loadingText = this.getLoadingText(),\r | |
484 | scroller = this.getScrollable();\r | |
485 | \r | |
486 | if (loadingText) {\r | |
487 | this.setMasked({\r | |
488 | xtype: 'loadmask',\r | |
489 | message: loadingText\r | |
490 | });\r | |
491 | \r | |
492 | //disable scrolling while it is masked\r | |
493 | if (scroller) {\r | |
494 | scroller.setDisabled(true);\r | |
495 | }\r | |
496 | }\r | |
497 | \r | |
498 | this.fireEvent.apply(this, [].concat('beforeload', this, Array.prototype.slice.call(arguments)));\r | |
499 | },\r | |
500 | \r | |
501 | onStoreLoad: function (store, records, successful, operation, parentNode) {\r | |
502 | this.setMasked(false);\r | |
503 | this.fireEvent.apply(this, [].concat('load', this, Array.prototype.slice.call(arguments)));\r | |
504 | \r | |
505 | if (store.indexOf(this.getLastNode()) === -1) {\r | |
506 | this.goToNode(store.getRoot());\r | |
507 | }\r | |
508 | },\r | |
509 | \r | |
510 | /**\r | |
511 | * Called when the backButton has been tapped.\r | |
512 | */\r | |
513 | onBackTap: function () {\r | |
514 | var me = this,\r | |
515 | node = me.getLastNode(),\r | |
516 | detailCard = me.getDetailCard(),\r | |
517 | detailCardActive = detailCard && me.getActiveItem() == detailCard,\r | |
518 | lastActiveList = me.getLastActiveList();\r | |
519 | \r | |
520 | this.fireAction('back', [this, node, lastActiveList, detailCardActive], 'doBack');\r | |
521 | },\r | |
522 | \r | |
523 | doBack: function (me, node, lastActiveList, detailCardActive) {\r | |
524 | var layout = me.getLayout(),\r | |
525 | animation = layout ? layout.getAnimation() : null;\r | |
526 | \r | |
527 | if (detailCardActive && lastActiveList) {\r | |
528 | if (animation) {\r | |
529 | animation.setReverse(true);\r | |
530 | }\r | |
531 | me.setActiveItem(lastActiveList);\r | |
532 | me.setLastNode(node.parentNode);\r | |
533 | me.syncToolbar();\r | |
534 | } else {\r | |
535 | me.goToNode(node.parentNode);\r | |
536 | }\r | |
537 | },\r | |
538 | \r | |
539 | updateData: function (data) {\r | |
540 | if (!this.getStore()) {\r | |
541 | this.setStore(new Ext.data.TreeStore({\r | |
542 | root: data\r | |
543 | }));\r | |
544 | }\r | |
545 | },\r | |
546 | \r | |
547 | applyStore: function (store) {\r | |
548 | if (store) {\r | |
549 | if (Ext.isString(store)) {\r | |
550 | // store id\r | |
551 | store = Ext.data.StoreManager.get(store);\r | |
552 | } else {\r | |
553 | // store instance or store config\r | |
554 | if (!(store instanceof Ext.data.TreeStore)) {\r | |
555 | store = Ext.factory(store, Ext.data.TreeStore, null);\r | |
556 | }\r | |
557 | }\r | |
558 | \r | |
559 | // <debug>\r | |
560 | if (!store) {\r | |
561 | Ext.Logger.warn("The specified Store cannot be found", this);\r | |
562 | }\r | |
563 | //</debug>\r | |
564 | }\r | |
565 | \r | |
566 | return store;\r | |
567 | },\r | |
568 | \r | |
569 | storeListeners: {\r | |
570 | rootchange: 'onStoreRootChange',\r | |
571 | load: 'onStoreLoad',\r | |
572 | beforeload: 'onStoreBeforeLoad'\r | |
573 | },\r | |
574 | \r | |
575 | updateStore: function (newStore, oldStore) {\r | |
576 | var me = this,\r | |
577 | listeners = this.storeListeners;\r | |
578 | \r | |
579 | listeners.scope = me;\r | |
580 | \r | |
581 | if (oldStore && Ext.isObject(oldStore) && oldStore.isStore) {\r | |
582 | if (oldStore.autoDestroy) {\r | |
583 | oldStore.destroy();\r | |
584 | }\r | |
585 | oldStore.un(listeners);\r | |
586 | }\r | |
587 | \r | |
588 | if (newStore) {\r | |
589 | newStore.on(listeners);\r | |
590 | me.goToNode(newStore.getRoot());\r | |
591 | }\r | |
592 | },\r | |
593 | \r | |
594 | onStoreRootChange: function (store, node) {\r | |
595 | this.goToNode(node);\r | |
596 | },\r | |
597 | \r | |
598 | applyDetailCard: function (detailCard, oldDetailCard) {\r | |
599 | return Ext.factory(detailCard, Ext.Component, detailCard === null ? oldDetailCard : undefined);\r | |
600 | },\r | |
601 | \r | |
602 | applyBackButton: function (config) {\r | |
603 | return Ext.factory(config, Ext.Button, this.getBackButton());\r | |
604 | },\r | |
605 | \r | |
606 | updateBackButton: function (newButton, oldButton) {\r | |
607 | if (newButton) {\r | |
608 | var me = this, \r | |
609 | toolbar;\r | |
610 | \r | |
611 | newButton.on('tap', me.onBackTap, me);\r | |
612 | newButton.setText(me.getBackText());\r | |
613 | \r | |
614 | toolbar = me.getToolbar();\r | |
615 | if (me.$backButtonContainer) {\r | |
616 | me.$backButtonContainer.insert(0, newButton);\r | |
617 | } else {\r | |
618 | toolbar.insert(0, newButton);\r | |
619 | }\r | |
620 | } else if (oldButton) {\r | |
621 | oldButton.destroy();\r | |
622 | }\r | |
623 | },\r | |
624 | \r | |
625 | applyToolbar: function (config) {\r | |
626 | if (config && config.splitNavigation) {\r | |
627 | Ext.apply(config, {\r | |
628 | docked: 'top',\r | |
629 | xtype: 'titlebar',\r | |
630 | ui: 'light'\r | |
631 | });\r | |
632 | \r | |
633 | var containerConfig = (config.splitNavigation === true) ? {} : config.splitNavigation;\r | |
634 | \r | |
635 | this.$backButtonContainer = this.add(Ext.apply({\r | |
636 | xtype: 'toolbar',\r | |
637 | docked: 'bottom',\r | |
638 | hidden: true,\r | |
639 | ui: 'dark'\r | |
640 | }, containerConfig));\r | |
641 | }\r | |
642 | \r | |
643 | return Ext.factory(config, Ext.TitleBar, this.getToolbar());\r | |
644 | },\r | |
645 | \r | |
646 | updateToolbar: function (newToolbar, oldToolbar) {\r | |
647 | var me = this;\r | |
648 | if (newToolbar) {\r | |
649 | newToolbar.setTitle(me.getTitle());\r | |
650 | if (!newToolbar.getParent()) {\r | |
651 | me.add(newToolbar);\r | |
652 | }\r | |
653 | } else if (oldToolbar) {\r | |
654 | oldToolbar.destroy();\r | |
655 | }\r | |
656 | },\r | |
657 | \r | |
658 | updateUseToolbar: function (newUseToolbar, oldUseToolbar) {\r | |
659 | if (!newUseToolbar) {\r | |
660 | this.setToolbar(false);\r | |
661 | }\r | |
662 | },\r | |
663 | \r | |
664 | updateTitle: function (newTitle) {\r | |
665 | var me = this,\r | |
666 | toolbar = me.getToolbar();\r | |
667 | \r | |
668 | if (toolbar && me.getUpdateTitleText()) {\r | |
669 | toolbar.setTitle(newTitle);\r | |
670 | }\r | |
671 | },\r | |
672 | \r | |
673 | /**\r | |
674 | * Override this method to provide custom template rendering of individual\r | |
675 | * nodes. The template will receive all data within the Record and will also\r | |
676 | * receive whether or not it is a leaf node.\r | |
677 | * @param {Ext.data.Record} node\r | |
678 | * @return {String}\r | |
679 | */\r | |
680 | getItemTextTpl: function (node) {\r | |
681 | return '{' + this.getDisplayField() + '}';\r | |
682 | },\r | |
683 | \r | |
684 | /**\r | |
685 | * Override this method to provide custom template rendering of titles/back\r | |
686 | * buttons when {@link #useTitleAsBackText} is enabled.\r | |
687 | * @param {Ext.data.Record} node\r | |
688 | * @return {String}\r | |
689 | */\r | |
690 | getTitleTextTpl: function (node) {\r | |
691 | return '{' + this.getDisplayField() + '}';\r | |
692 | },\r | |
693 | \r | |
694 | /**\r | |
695 | * @private\r | |
696 | */\r | |
697 | renderTitleText: function (node, forBackButton) {\r | |
698 | if (!node.titleTpl) {\r | |
699 | node.titleTpl = Ext.create('Ext.XTemplate', this.getTitleTextTpl(node));\r | |
700 | }\r | |
701 | \r | |
702 | if (node.isRoot()) {\r | |
703 | var initialTitle = this.getInitialConfig('title');\r | |
704 | return (forBackButton && initialTitle === '') ? this.getInitialConfig('backText') : initialTitle;\r | |
705 | }\r | |
706 | \r | |
707 | return node.titleTpl.applyTemplate(node.data);\r | |
708 | },\r | |
709 | \r | |
710 | /**\r | |
711 | * Method to handle going to a specific node within this nested list. Node must be part of the\r | |
712 | * internal {@link #store}.\r | |
713 | * @param {Ext.data.NodeInterface} node The specified node to navigate to.\r | |
714 | */\r | |
715 | goToNode: function (node) {\r | |
716 | if (!node) {\r | |
717 | return;\r | |
718 | }\r | |
719 | \r | |
720 | var me = this,\r | |
721 | activeItem = me.getActiveItem(),\r | |
722 | detailCard = me.getDetailCard(),\r | |
723 | detailCardActive = detailCard && me.getActiveItem() == detailCard,\r | |
724 | reverse = me.goToNodeReverseAnimation(node),\r | |
725 | firstList = me.firstList,\r | |
726 | secondList = me.secondList,\r | |
727 | layout = me.getLayout(),\r | |
728 | animation = layout ? layout.getAnimation() : null,\r | |
729 | list;\r | |
730 | \r | |
731 | //if the node is a leaf, throw an error\r | |
732 | if (node.isLeaf()) {\r | |
733 | throw new Error('goToNode: passed a node which is a leaf.');\r | |
734 | }\r | |
735 | \r | |
736 | //if we are currently at the passed node, do nothing.\r | |
737 | if (node === me.getLastNode() && !detailCardActive) {\r | |
738 | return;\r | |
739 | }\r | |
740 | \r | |
741 | if (detailCardActive) {\r | |
742 | if (animation) {\r | |
743 | animation.setReverse(true);\r | |
744 | }\r | |
745 | list = me.getLastActiveList();\r | |
746 | list.getStore().setNode(node);\r | |
747 | node.expand();\r | |
748 | me.setActiveItem(list);\r | |
749 | } else {\r | |
750 | if (animation) {\r | |
751 | animation.setReverse(reverse);\r | |
752 | }\r | |
753 | \r | |
754 | if (firstList && secondList) {\r | |
755 | //firstList and secondList have both been created\r | |
756 | activeItem = me.getActiveItem();\r | |
757 | \r | |
758 | me.setLastActiveList(activeItem);\r | |
759 | list = (activeItem == firstList) ? secondList : firstList;\r | |
760 | \r | |
761 | list.getStore().setNode(node);\r | |
762 | node.expand();\r | |
763 | \r | |
764 | me.setActiveItem(list);\r | |
765 | if (me.getClearSelectionOnListChange()) {\r | |
766 | list.deselectAll();\r | |
767 | }\r | |
768 | }\r | |
769 | else if (firstList) {\r | |
770 | //only firstList has been created\r | |
771 | me.setLastActiveList(me.getActiveItem());\r | |
772 | me.setActiveItem(me.getList(node));\r | |
773 | me.secondList = me.getActiveItem();\r | |
774 | }\r | |
775 | else {\r | |
776 | //no lists have been created\r | |
777 | me.setActiveItem(me.getList(node));\r | |
778 | me.firstList = me.getActiveItem();\r | |
779 | }\r | |
780 | }\r | |
781 | \r | |
782 | me.fireEvent('listchange', me, me.getActiveItem());\r | |
783 | \r | |
784 | me.setLastNode(node);\r | |
785 | \r | |
786 | me.syncToolbar();\r | |
787 | },\r | |
788 | \r | |
789 | \r | |
790 | /**\r | |
791 | * The leaf you want to navigate to. You should pass a node instance.\r | |
792 | * @param {Ext.data.NodeInterface} node The specified node to navigate to.\r | |
793 | */\r | |
794 | goToLeaf: function (node) {\r | |
795 | if (!node.isLeaf()) {\r | |
796 | throw new Error('goToLeaf: passed a node which is not a leaf.');\r | |
797 | }\r | |
798 | \r | |
799 | var me = this,\r | |
800 | card = me.getDetailCard(),\r | |
801 | container = me.getDetailContainer(),\r | |
802 | sharedContainer = container === me,\r | |
803 | layout = me.getLayout(),\r | |
804 | animation = layout ? layout.getAnimation() : false,\r | |
805 | activeItem;\r | |
806 | \r | |
807 | if (card) {\r | |
808 | if (container.getItems().indexOf(card) === -1) {\r | |
809 | container.add(card);\r | |
810 | }\r | |
811 | if (sharedContainer) {\r | |
812 | activeItem = me.getActiveItem();\r | |
813 | if (activeItem instanceof Ext.dataview.List) {\r | |
814 | me.setLastActiveList(activeItem);\r | |
815 | }\r | |
816 | me.setLastNode(node);\r | |
817 | }\r | |
818 | if (animation) {\r | |
819 | animation.setReverse(false);\r | |
820 | }\r | |
821 | container.setActiveItem(card);\r | |
822 | me.syncToolbar();\r | |
823 | }\r | |
824 | },\r | |
825 | \r | |
826 | /**\r | |
827 | * @private\r | |
828 | * Method which updates the {@link #backButton} and {@link #toolbar} with the latest information from\r | |
829 | * the current node.\r | |
830 | */\r | |
831 | syncToolbar: function (forceDetail) {\r | |
832 | var me = this,\r | |
833 | detailCard = me.getDetailCard(),\r | |
834 | node = me.getLastNode(),\r | |
835 | detailActive = forceDetail || (detailCard && (me.getActiveItem() == detailCard)),\r | |
836 | parentNode = (detailActive) ? node : node.parentNode,\r | |
837 | backButton = me.getBackButton();\r | |
838 | \r | |
839 | //show/hide the backButton, and update the backButton text, if one exists\r | |
840 | if (backButton) {\r | |
841 | var toolbar = me.getToolbar(),\r | |
842 | splitNavigation = toolbar.getInitialConfig("splitNavigation");\r | |
843 | \r | |
844 | if (splitNavigation) {\r | |
845 | me.$backButtonContainer[parentNode ? 'show' : 'hide']();\r | |
846 | }\r | |
847 | \r | |
848 | backButton[parentNode ? 'show' : 'hide']();\r | |
849 | if (parentNode && me.getUseTitleAsBackText()) {\r | |
850 | backButton.setText(me.renderTitleText(node.parentNode, true));\r | |
851 | }\r | |
852 | }\r | |
853 | \r | |
854 | if (node) {\r | |
855 | me.setTitle(me.renderTitleText(node));\r | |
856 | }\r | |
857 | },\r | |
858 | \r | |
859 | updateBackText: function (newText) {\r | |
860 | this.getBackButton().setText(newText);\r | |
861 | },\r | |
862 | \r | |
863 | /**\r | |
864 | * @private\r | |
865 | * Returns `true` if the passed node should have a reverse animation from the previous current node.\r | |
866 | * @param {Ext.data.NodeInterface} node\r | |
867 | */\r | |
868 | goToNodeReverseAnimation: function (node) {\r | |
869 | var lastNode = this.getLastNode();\r | |
870 | \r | |
871 | if (!lastNode) {\r | |
872 | return false;\r | |
873 | }\r | |
874 | \r | |
875 | return (!lastNode.contains(node) && lastNode.isAncestor(node)) ? true : false;\r | |
876 | },\r | |
877 | \r | |
878 | /**\r | |
879 | * @private\r | |
880 | * Returns the list config for a specified node.\r | |
881 | * @param {HTMLElement} node The node for the list config.\r | |
882 | */\r | |
883 | getList: function (node) {\r | |
884 | var me = this,\r | |
885 | treeStore = new Ext.data.NodeStore({\r | |
886 | recursive: false,\r | |
887 | node: node,\r | |
888 | rootVisible: false,\r | |
889 | model: me.getStore().getModel(),\r | |
890 | proxy: 'memory'\r | |
891 | });\r | |
892 | \r | |
893 | node.expand();\r | |
894 | \r | |
895 | return Ext.Object.merge({\r | |
896 | xtype: 'list',\r | |
897 | useSimpleItems: me.getUseSimpleItems(),\r | |
898 | pressedDelay: 250,\r | |
899 | autoDestroy: true,\r | |
900 | store: treeStore,\r | |
901 | onItemDisclosure: me.getOnItemDisclosure(),\r | |
902 | allowDeselect: me.getAllowDeselect(),\r | |
903 | itemHeight: me.getItemHeight(),\r | |
904 | variableHeights: me.getVariableHeights(),\r | |
905 | emptyText: me.getEmptyText(),\r | |
906 | listeners: [{\r | |
907 | itemdoubletap: 'onItemDoubleTap',\r | |
908 | itemtap: 'onItemTap',\r | |
909 | beforeselectionchange: 'onBeforeSelect',\r | |
910 | containertap: 'onContainerTap',\r | |
911 | scope: me\r | |
912 | }, {\r | |
913 | selectionchange: 'onSelectionChange',\r | |
914 | itemtouchstart: 'onItemInteraction',\r | |
915 | itemtap: 'onItemInteraction',\r | |
916 | order: 'before',\r | |
917 | scope: me\r | |
918 | }],\r | |
919 | itemTpl: '<span<tpl if="leaf == true"> class="x-list-item-leaf"</tpl>>' + me.getItemTextTpl(node) + '</span>'\r | |
920 | }, me.getListConfig());\r | |
921 | }\r | |
922 | });\r | |
923 | \r |