]>
Commit | Line | Data |
---|---|---|
6527f429 DM |
1 | /**\r |
2 | * @class Ext.view.AbstractView\r | |
3 | * This is an abstract superclass and should not be used directly. Please see {@link Ext.view.View}.\r | |
4 | * @private\r | |
5 | */\r | |
6 | Ext.define('Ext.view.AbstractView', {\r | |
7 | extend: 'Ext.Component',\r | |
8 | requires: [\r | |
9 | 'Ext.LoadMask',\r | |
10 | 'Ext.CompositeElementLite',\r | |
11 | 'Ext.selection.DataViewModel',\r | |
12 | 'Ext.view.NavigationModel'\r | |
13 | ],\r | |
14 | mixins: [\r | |
15 | 'Ext.util.StoreHolder'\r | |
16 | ],\r | |
17 | \r | |
18 | inheritableStatics: {\r | |
19 | /**\r | |
20 | * @private\r | |
21 | * @static\r | |
22 | * @inheritable\r | |
23 | */\r | |
24 | getRecord: function(node) {\r | |
25 | return this.getBoundView(node).getRecord(node);\r | |
26 | },\r | |
27 | \r | |
28 | /**\r | |
29 | * @private\r | |
30 | * @static\r | |
31 | * @inheritable\r | |
32 | */\r | |
33 | getBoundView: function(node) {\r | |
34 | return Ext.getCmp(node.getAttribute('data-boundView'));\r | |
35 | }\r | |
36 | },\r | |
37 | \r | |
38 | defaultBindProperty: 'store',\r | |
39 | \r | |
40 | /**\r | |
41 | * @private\r | |
42 | * Used for buffered rendering.\r | |
43 | */\r | |
44 | renderBuffer: document.createElement('div'),\r | |
45 | \r | |
46 | statics: {\r | |
47 | \r | |
48 | /**\r | |
49 | * @cfg {Number} [updateDelay=200] Global config for use when using {@link #throttledUpdate throttled view updating} if the data in the backing {@link Ext.data.Store store}\r | |
50 | * is being changed rapidly, for example receiving changes from the server through a WebSocket connection.\r | |
51 | *\r | |
52 | * To avoid too-frequent view updates overloading the browser with style recalculation, layout and paint requests, updates can be {@link #throttledUpdate throttled} to \r | |
53 | * coalesced, and applied at the interval specified in milliseconds.\r | |
54 | */\r | |
55 | updateDelay: 200,\r | |
56 | \r | |
57 | queueRecordChange: function(view, store, record, operation, modifiedFieldNames) {\r | |
58 | var me = this,\r | |
59 | changeQueue = me.changeQueue || (me.changeQueue = {}),\r | |
60 | recId = record.internalId,\r | |
61 | recChange,\r | |
62 | updated,\r | |
63 | len, i, fieldName, value,\r | |
64 | checkForReversion;\r | |
65 | \r | |
66 | recChange = changeQueue[recId] || (changeQueue[recId] = {\r | |
67 | operation: operation,\r | |
68 | record: record,\r | |
69 | data: {},\r | |
70 | views: []\r | |
71 | });\r | |
72 | \r | |
73 | // Hash of original values\r | |
74 | updated = recChange.data;\r | |
75 | \r | |
76 | // Make sure this view is among those updated when record changes are flushed\r | |
77 | Ext.Array.include(recChange.views, view);\r | |
78 | \r | |
79 | // Note the following condition tests the result of an assignment statement.\r | |
80 | // If we have been informed that specific fields have changed.\r | |
81 | if (modifiedFieldNames && (len = modifiedFieldNames.length)) {\r | |
82 | for (i = 0; i < len; i++) {\r | |
83 | fieldName = modifiedFieldNames[i];\r | |
84 | value = record.data[fieldName];\r | |
85 | \r | |
86 | // More than one update is being performed...\r | |
87 | if (updated.hasOwnProperty(fieldName)) {\r | |
88 | \r | |
89 | // If the update is back to the original value, this may have reverted the record to original state\r | |
90 | if (record.isEqual(updated[fieldName], value)) {\r | |
91 | delete updated[fieldName];\r | |
92 | checkForReversion = true;\r | |
93 | }\r | |
94 | }\r | |
95 | \r | |
96 | // On first update, cache the original value\r | |
97 | else {\r | |
98 | updated[fieldName] = value;\r | |
99 | }\r | |
100 | }\r | |
101 | \r | |
102 | // If the record has been returned to its original state, delete the queue entry.\r | |
103 | // checkForReversion flag saves the expensive (on legacy browsers) call to Ext.Object.getKeys\r | |
104 | if (checkForReversion && !Ext.Object.getKeys(updated).length) {\r | |
105 | delete changeQueue[recId];\r | |
106 | }\r | |
107 | }\r | |
108 | \r | |
109 | // Unpsecified fields have changed. We have to collect the whole data object.\r | |
110 | else {\r | |
111 | Ext.apply(updated, record.data);\r | |
112 | }\r | |
113 | \r | |
114 | // Create a task which will call on to the onFlushTick every updateDelay milliseconds.\r | |
115 | if (!me.flushQueueTask) {\r | |
116 | me.flushQueueTask = Ext.util.TaskManager.newTask({\r | |
117 | // Queue the actual render flush on the next animation frame if available.\r | |
118 | run: Ext.global.requestAnimationFrame ? Ext.Function.createAnimationFrame(me.onFlushTick, me) : Ext.Function.bind(me.onFlushTick, me),\r | |
119 | interval: Ext.view.AbstractView.updateDelay,\r | |
120 | repeat: 1\r | |
121 | });\r | |
122 | }\r | |
123 | me.flushQueueTask.start();\r | |
124 | },\r | |
125 | \r | |
126 | /**\r | |
127 | * @private\r | |
128 | * On every flush (determined by updateDelay setting), ask the animation system to schedule a call to\r | |
129 | * flushChangeQueue at the next animation frame.\r | |
130 | */\r | |
131 | onFlushTick: function() {\r | |
132 | Ext.AnimationQueue.start(this.flushChangeQueue, this);\r | |
133 | },\r | |
134 | \r | |
135 | /**\r | |
136 | * @private\r | |
137 | * Flushes all queued field updates to the UI.\r | |
138 | *\r | |
139 | * Called in the context of the AbstractView class.\r | |
140 | *\r | |
141 | * The queue is shared across all Views so that there is only one global flush operation.\r | |
142 | */\r | |
143 | flushChangeQueue: function() {\r | |
144 | // Maintainer: Note that "me" references AbstractView class\r | |
145 | var me = this,\r | |
146 | dirtyViews,\r | |
147 | len,\r | |
148 | changeQueue,\r | |
149 | recChange,\r | |
150 | recId,\r | |
151 | i, view;\r | |
152 | \r | |
153 | // If there is scrolling going on anywhere, requeue the flush operation.\r | |
154 | if (Ext.isScrolling) {\r | |
155 | me.flushQueueTask.start();\r | |
156 | return;\r | |
157 | }\r | |
158 | \r | |
159 | changeQueue = me.changeQueue;\r | |
160 | \r | |
161 | // Empty the view's changeQueue\r | |
162 | this.changeQueue = {};\r | |
163 | \r | |
164 | for (recId in changeQueue) {\r | |
165 | recChange = changeQueue[recId];\r | |
166 | dirtyViews = recChange.views;\r | |
167 | len = dirtyViews.length;\r | |
168 | \r | |
169 | // Loop through all the views which have outstanding changes.\r | |
170 | for (i = 0; i < len; i++) {\r | |
171 | view = dirtyViews[i];\r | |
172 | \r | |
173 | // View may have been destroyed during the buffered phase.\r | |
174 | if (!view.destroyed) {\r | |
175 | view.handleUpdate(view.dataSource, recChange.record, recChange.operation, Ext.Object.getKeys(recChange.data));\r | |
176 | }\r | |
177 | }\r | |
178 | }\r | |
179 | Ext.AnimationQueue.stop(me.flushChangeQueue, me);\r | |
180 | }\r | |
181 | },\r | |
182 | \r | |
183 | config: {\r | |
184 | /**\r | |
185 | * @cfg {Ext.data.Model} selection\r | |
186 | * The selected model. Typically used with {@link #bind binding}.\r | |
187 | */\r | |
188 | selection: null,\r | |
189 | \r | |
190 | /**\r | |
191 | * @cfg {Ext.data.Store} store\r | |
192 | * The {@link Ext.data.Store} to bind this DataView to.\r | |
193 | * @since 2.3.0\r | |
194 | */\r | |
195 | store: 'ext-empty-store',\r | |
196 | \r | |
197 | // @cmd-auto-dependency { aliasPrefix: 'view.navigation.' }\r | |
198 | /**\r | |
199 | * @private\r | |
200 | * The {@link Ext.view.NavigationModel} [default] alias to use.\r | |
201 | * @since 5.0.1\r | |
202 | */\r | |
203 | navigationModel: {\r | |
204 | type: 'default'\r | |
205 | },\r | |
206 | \r | |
207 | // @cmd-auto-dependency { aliasPrefix: 'selection.' }\r | |
208 | /**\r | |
209 | * @cfg {Object/Ext.selection.DataViewModel} selectionModel\r | |
210 | * The {@link Ext.selection.Model selection model} [dataviewmodel] config or alias to use.\r | |
211 | * @since 5.1.0\r | |
212 | */\r | |
213 | selectionModel: {\r | |
214 | type: 'dataviewmodel'\r | |
215 | }\r | |
216 | },\r | |
217 | \r | |
218 | publishes: ['selection'],\r | |
219 | twoWayBindable: ['selection'],\r | |
220 | \r | |
221 | /**\r | |
222 | * @cfg {Boolean} [throttledUpdate=false]\r | |
223 | * Configure as `true` to have this view participate in the global throttled update queue which flushes store changes to the UI at a maximum rate\r | |
224 | * determined by the {@link #updateDelay} setting.\r | |
225 | */\r | |
226 | throttledUpdate: false,\r | |
227 | \r | |
228 | /**\r | |
229 | * @cfg {String/String[]/Ext.XTemplate} tpl (required)\r | |
230 | * The HTML fragment or an array of fragments that will make up the template used by this DataView. This should\r | |
231 | * be specified in the same format expected by the constructor of {@link Ext.XTemplate}. When a `tpl` is specified,\r | |
232 | * this class assumes that records are rendered in the order they appear in the `{@link #store}`. If a custom `tpl`\r | |
233 | * does not conform to this assumption, index values will be incorrect which may cause the view to misbehave.\r | |
234 | * @since 2.3.0\r | |
235 | */\r | |
236 | \r | |
237 | /**\r | |
238 | * @cfg {Boolean} [deferInitialRefresh=false]\r | |
239 | * Configure as 'true` to defer the initial refresh of the view.\r | |
240 | *\r | |
241 | * This allows the View to execute its render and initial layout more quickly because the process will not be encumbered\r | |
242 | * by the update of the view structure.\r | |
243 | */\r | |
244 | deferInitialRefresh: false,\r | |
245 | \r | |
246 | /**\r | |
247 | * @cfg {String} itemSelector (required)\r | |
248 | * <b>This is a required setting</b>. A simple CSS selector (e.g. `div.some-class` or\r | |
249 | * `span:first-child`) that will be used to determine what nodes this DataView will be\r | |
250 | * working with. The itemSelector is used to map DOM nodes to records. As such, there should\r | |
251 | * only be one root level element that matches the selector for each record. The itemSelector\r | |
252 | * will be automatically configured if the {@link #itemTpl} config is used.\r | |
253 | * \r | |
254 | * new Ext.view.View({\r | |
255 | * renderTo: Ext.getBody(),\r | |
256 | * store: {\r | |
257 | * fields: ['name'],\r | |
258 | * data: [\r | |
259 | * {name: 'Item 1'},\r | |
260 | * {name: 'Item 2'}\r | |
261 | * ]\r | |
262 | * },\r | |
263 | * tpl: [\r | |
264 | * '<ul>',\r | |
265 | * '<tpl for=".">',\r | |
266 | * '<li>{name}</li>',\r | |
267 | * '</tpl>',\r | |
268 | * '</ul>'\r | |
269 | * ],\r | |
270 | * // Match the li, since each one maps to a record\r | |
271 | * itemSelector: 'li'\r | |
272 | * });\r | |
273 | * \r | |
274 | * @since 2.3.0\r | |
275 | */\r | |
276 | \r | |
277 | /**\r | |
278 | * @cfg {String} itemCls\r | |
279 | * Specifies the class to be assigned to each element in the view when used in conjunction with the\r | |
280 | * {@link #itemTpl} configuration.\r | |
281 | * @since 2.3.0\r | |
282 | */\r | |
283 | itemCls: Ext.baseCSSPrefix + 'dataview-item',\r | |
284 | \r | |
285 | /**\r | |
286 | * @cfg {String/String[]/Ext.XTemplate} itemTpl\r | |
287 | * The inner portion of the item template to be rendered. Follows an XTemplate\r | |
288 | * structure and will be placed inside of a tpl.\r | |
289 | */\r | |
290 | \r | |
291 | /**\r | |
292 | * @cfg {String} overItemCls\r | |
293 | * A CSS class to apply to each item in the view on mouseover.\r | |
294 | * Setting this will automatically set {@link #trackOver} to `true`.\r | |
295 | */\r | |
296 | \r | |
297 | //<locale>\r | |
298 | /**\r | |
299 | * @cfg {String} loadingText\r | |
300 | * A string to display during data load operations. If specified, this text will be\r | |
301 | * displayed in a loading div and the view's contents will be cleared while loading, otherwise the view's\r | |
302 | * contents will continue to display normally until the new data is loaded and the contents are replaced.\r | |
303 | * @since 2.3.0\r | |
304 | */\r | |
305 | loadingText: 'Loading...',\r | |
306 | //</locale>\r | |
307 | \r | |
308 | /**\r | |
309 | * @cfg {Boolean/Object} loadMask\r | |
310 | * False to disable a load mask from displaying while the view is loading. This can also be a\r | |
311 | * {@link Ext.LoadMask} configuration object.\r | |
312 | */\r | |
313 | loadMask: true,\r | |
314 | \r | |
315 | /**\r | |
316 | * @cfg {String} loadingCls\r | |
317 | * The CSS class to apply to the loading message element. Defaults to Ext.LoadMask.prototype.msgCls "x-mask-loading".\r | |
318 | */\r | |
319 | \r | |
320 | /**\r | |
321 | * @cfg {Boolean} loadingUseMsg\r | |
322 | * Whether or not to use the loading message.\r | |
323 | * @private\r | |
324 | */\r | |
325 | loadingUseMsg: true,\r | |
326 | \r | |
327 | \r | |
328 | /**\r | |
329 | * @cfg {Number} loadingHeight\r | |
330 | * If specified, gives an explicit height for the data view when it is showing the {@link #loadingText},\r | |
331 | * if that is specified. This is useful to prevent the view's height from collapsing to zero when the\r | |
332 | * loading mask is applied and there are no other contents in the data view.\r | |
333 | */\r | |
334 | \r | |
335 | /**\r | |
336 | * @cfg {String} selectedItemCls\r | |
337 | * A CSS class to apply to each selected item in the view.\r | |
338 | */\r | |
339 | selectedItemCls: Ext.baseCSSPrefix + 'item-selected',\r | |
340 | \r | |
341 | //<locale>\r | |
342 | /**\r | |
343 | * @cfg {String} emptyText\r | |
344 | * The text to display in the view when there is no data to display.\r | |
345 | * Note that when using local data the emptyText will not be displayed unless you set\r | |
346 | * the {@link #deferEmptyText} option to false.\r | |
347 | * @since 2.3.0\r | |
348 | */\r | |
349 | emptyText: "",\r | |
350 | //</locale>\r | |
351 | \r | |
352 | /**\r | |
353 | * @cfg {Boolean} deferEmptyText\r | |
354 | * True to defer emptyText being applied until the store's first load.\r | |
355 | * @since 2.3.0\r | |
356 | */\r | |
357 | deferEmptyText: true,\r | |
358 | \r | |
359 | /**\r | |
360 | * @cfg {Boolean} trackOver\r | |
361 | * When `true` the {@link #overItemCls} will be applied to items when hovered over.\r | |
362 | * This in return will also cause {@link Ext.view.View#highlightitem highlightitem} and\r | |
363 | * {@link Ext.view.View#unhighlightitem unhighlightitem} events to be fired.\r | |
364 | *\r | |
365 | * Enabled automatically when the {@link #overItemCls} config is set.\r | |
366 | *\r | |
367 | * @since 2.3.0\r | |
368 | */\r | |
369 | trackOver: false,\r | |
370 | \r | |
371 | /**\r | |
372 | * @cfg {Boolean} blockRefresh\r | |
373 | * Set this to true to ignore refresh events on the bound store. This is useful if\r | |
374 | * you wish to provide custom transition animations via a plugin\r | |
375 | * @since 3.4.0\r | |
376 | */\r | |
377 | blockRefresh: false,\r | |
378 | \r | |
379 | /**\r | |
380 | * @cfg {Boolean} [disableSelection=false]\r | |
381 | * True to disable selection within the DataView. This configuration will lock the selection model\r | |
382 | * that the DataView uses.\r | |
383 | */\r | |
384 | \r | |
385 | /**\r | |
386 | * @cfg {Boolean} preserveScrollOnRefresh\r | |
387 | * True to preserve scroll position across refresh operations.\r | |
388 | */\r | |
389 | preserveScrollOnRefresh: false,\r | |
390 | \r | |
391 | /**\r | |
392 | * @cfg {Boolean} [preserveScrollOnReload=false]\r | |
393 | * True to preserve scroll position when the store is reloaded.\r | |
394 | *\r | |
395 | * You may want to configure this as `true` if you are using a {@link Ext.data.BufferedStore buffered store}\r | |
396 | * and you require refreshes of the client side data state not to disturb the state of the UI.\r | |
397 | *\r | |
398 | * @since 5.1.1\r | |
399 | */\r | |
400 | preserveScrollOnReload: false,\r | |
401 | \r | |
402 | ariaRole: 'listbox',\r | |
403 | itemAriaRole: 'option',\r | |
404 | \r | |
405 | /**\r | |
406 | * @private\r | |
407 | */\r | |
408 | last: false,\r | |
409 | focusable: true,\r | |
410 | tabIndex: 0,\r | |
411 | \r | |
412 | triggerEvent: 'itemclick',\r | |
413 | triggerCtEvent: 'containerclick',\r | |
414 | \r | |
415 | // Starts as true by default so that pn the leading edge of the first layout a refresh will be triggered.\r | |
416 | // A refresh opereration sets this flag to false.\r | |
417 | // When a refresh is requested using refreshView, the request may be deferred because of hidden or collapsed state.\r | |
418 | // This is done by setting the refreshNeeded flag to true, and the the next layout will trigger refresh.\r | |
419 | refreshNeeded: true,\r | |
420 | \r | |
421 | updateSuspendCounter: 0,\r | |
422 | \r | |
423 | addCmpEvents: Ext.emptyFn,\r | |
424 | \r | |
425 | /**\r | |
426 | * @event beforerefresh\r | |
427 | * Fires before the view is refreshed\r | |
428 | * @param {Ext.view.View} this The DataView object\r | |
429 | */\r | |
430 | \r | |
431 | /**\r | |
432 | * @event refresh\r | |
433 | * Fires when the view is refreshed\r | |
434 | * @param {Ext.view.View} this The DataView object\r | |
435 | */\r | |
436 | \r | |
437 | /**\r | |
438 | * @event viewready\r | |
439 | * Fires when the View's item elements representing Store items has been rendered. No items will be available\r | |
440 | * for selection until this event fires.\r | |
441 | * @param {Ext.view.View} this\r | |
442 | */\r | |
443 | \r | |
444 | /**\r | |
445 | * @event itemupdate\r | |
446 | * Fires when the node associated with an individual record is updated\r | |
447 | * @param {Ext.data.Model} record The model instance\r | |
448 | * @param {Number} index The index of the record\r | |
449 | * @param {HTMLElement} node The node that has just been updated\r | |
450 | */\r | |
451 | \r | |
452 | /**\r | |
453 | * @event itemadd\r | |
454 | * Fires when the nodes associated with an recordset have been added to the underlying store\r | |
455 | * @param {Ext.data.Model[]} records The model instance\r | |
456 | * @param {Number} index The index at which the set of records was inserted\r | |
457 | * @param {HTMLElement[]} node The node that has just been updated\r | |
458 | */\r | |
459 | \r | |
460 | /**\r | |
461 | * @event itemremove\r | |
462 | * Fires when the node associated with an individual record is removed\r | |
463 | * @param {Ext.data.Model[]} records The model instances removed\r | |
464 | * @param {Number} index The index from which the records wer removed\r | |
465 | * @param {HTMLElement[]} item The view items removed\r | |
466 | * @param {Ext.view.View} view The view removing the item\r | |
467 | */\r | |
468 | \r | |
469 | constructor: function(config) {\r | |
470 | if (config && config.selModel) {\r | |
471 | config.selectionModel = config.selModel;\r | |
472 | }\r | |
473 | this.callParent([config]);\r | |
474 | },\r | |
475 | \r | |
476 | initComponent: function(){\r | |
477 | var me = this,\r | |
478 | isDef = Ext.isDefined,\r | |
479 | itemTpl = me.itemTpl,\r | |
480 | memberFn = {},\r | |
481 | store;\r | |
482 | \r | |
483 | if (itemTpl) {\r | |
484 | if (Ext.isArray(itemTpl)) {\r | |
485 | // string array\r | |
486 | if (typeof itemTpl[itemTpl.length - 1] !== 'string') {\r | |
487 | itemTpl = itemTpl.slice(0);\r | |
488 | memberFn = itemTpl.pop();\r | |
489 | }\r | |
490 | itemTpl = itemTpl.join('');\r | |
491 | } else if (Ext.isObject(itemTpl)) {\r | |
492 | // tpl instance\r | |
493 | memberFn = Ext.apply(memberFn, itemTpl.initialConfig);\r | |
494 | itemTpl = itemTpl.html;\r | |
495 | }\r | |
496 | \r | |
497 | if (!me.itemSelector) {\r | |
498 | me.itemSelector = '.' + me.itemCls;\r | |
499 | }\r | |
500 | \r | |
501 | itemTpl = Ext.String.format('<tpl for="."><div class="{0}" role="{2}">{1}</div></tpl>', me.itemCls, itemTpl, me.itemAriaRole);\r | |
502 | me.tpl = new Ext.XTemplate(itemTpl, memberFn);\r | |
503 | }\r | |
504 | \r | |
505 | //<debug>\r | |
506 | if (!isDef(me.tpl) || !isDef(me.itemSelector)) {\r | |
507 | Ext.raise({\r | |
508 | sourceClass: 'Ext.view.View',\r | |
509 | tpl: me.tpl,\r | |
510 | itemSelector: me.itemSelector,\r | |
511 | msg: "DataView requires both tpl and itemSelector configurations to be defined."\r | |
512 | });\r | |
513 | }\r | |
514 | //</debug>\r | |
515 | \r | |
516 | me.callParent();\r | |
517 | me.tpl = me.getTpl('tpl');\r | |
518 | \r | |
519 | //<debug>\r | |
520 | // backwards compat alias for overClass/selectedClass\r | |
521 | // TODO: Consider support for overCls generation Ext.Component config\r | |
522 | if (isDef(me.overCls) || isDef(me.overClass)) {\r | |
523 | if (Ext.isDefined(Ext.global.console)) {\r | |
524 | Ext.global.console.warn('Ext.view.View: Using the deprecated overCls or overClass configuration. Use overItemCls instead.');\r | |
525 | }\r | |
526 | me.overItemCls = me.overCls || me.overClass;\r | |
527 | delete me.overCls;\r | |
528 | delete me.overClass;\r | |
529 | }\r | |
530 | \r | |
531 | if (isDef(me.selectedCls) || isDef(me.selectedClass)) {\r | |
532 | if (Ext.isDefined(Ext.global.console)) {\r | |
533 | Ext.global.console.warn('Ext.view.View: Using the deprecated selectedCls or selectedClass configuration. Use selectedItemCls instead.');\r | |
534 | }\r | |
535 | me.selectedItemCls = me.selectedCls || me.selectedClass;\r | |
536 | delete me.selectedCls;\r | |
537 | delete me.selectedClass;\r | |
538 | }\r | |
539 | //</debug>\r | |
540 | \r | |
541 | if (me.overItemCls) {\r | |
542 | me.trackOver = true;\r | |
543 | }\r | |
544 | \r | |
545 | me.addCmpEvents();\r | |
546 | \r | |
547 | // Look up the configured Store. If none configured, use the fieldless, empty Store defined in Ext.data.Store.\r | |
548 | store = me.store = Ext.data.StoreManager.lookup(me.store || 'ext-empty-store');\r | |
549 | \r | |
550 | // Use the provided store as the data source unless a Feature or plugin has injected a special one\r | |
551 | if (!me.dataSource) {\r | |
552 | me.dataSource = store;\r | |
553 | }\r | |
554 | \r | |
555 | me.bindStore(store, true);\r | |
556 | \r | |
557 | // Must exist before the selection model.\r | |
558 | // Selection model listens to this for navigation events.\r | |
559 | me.getNavigationModel().bindComponent(this);\r | |
560 | \r | |
561 | if (!me.all) {\r | |
562 | me.all = new Ext.CompositeElementLite();\r | |
563 | }\r | |
564 | \r | |
565 | // We track the scroll position\r | |
566 | me.scrollState = {\r | |
567 | top: 0,\r | |
568 | left: 0\r | |
569 | };\r | |
570 | \r | |
571 | me.savedTabIndexAttribute = 'data-savedtabindex-' + me.id;\r | |
572 | },\r | |
573 | \r | |
574 | getElConfig: function() {\r | |
575 | var result = this.mixins.renderable.getElConfig.call(this);\r | |
576 | \r | |
577 | // Subclasses may set focusable to false (BoundList is not focusable)\r | |
578 | if (this.focusable) {\r | |
579 | result.tabIndex = 0;\r | |
580 | }\r | |
581 | return result;\r | |
582 | },\r | |
583 | \r | |
584 | onRender: function() {\r | |
585 | var mask = this.loadMask;\r | |
586 | \r | |
587 | this.callParent(arguments);\r | |
588 | if (mask) {\r | |
589 | this.createMask(mask);\r | |
590 | }\r | |
591 | },\r | |
592 | \r | |
593 | beforeLayout: function() {\r | |
594 | var me = this;\r | |
595 | \r | |
596 | me.callParent(arguments);\r | |
597 | \r | |
598 | // If a refresh is needed, just before the layout is the time to apply it.\r | |
599 | // If there is a deferred refresh timer running, allow that to do the refresh.\r | |
600 | if (me.refreshNeeded && !me.pendingRefresh) {\r | |
601 | // If we have refreshed before, just call a refresh now.\r | |
602 | if (me.refreshCounter) {\r | |
603 | me.refresh();\r | |
604 | }\r | |
605 | else {\r | |
606 | me.doFirstRefresh(me.dataSource);\r | |
607 | }\r | |
608 | }\r | |
609 | },\r | |
610 | \r | |
611 | onMaskBeforeShow: function(){\r | |
612 | var me = this,\r | |
613 | loadingHeight = me.loadingHeight;\r | |
614 | \r | |
615 | if (loadingHeight && loadingHeight > me.getHeight()) {\r | |
616 | me.hasLoadingHeight = true;\r | |
617 | me.oldMinHeight = me.minHeight;\r | |
618 | me.minHeight = loadingHeight;\r | |
619 | me.updateLayout();\r | |
620 | }\r | |
621 | },\r | |
622 | \r | |
623 | onMaskHide: function(){\r | |
624 | var me = this;\r | |
625 | \r | |
626 | if (!me.destroying && me.hasLoadingHeight) {\r | |
627 | me.minHeight = me.oldMinHeight;\r | |
628 | me.updateLayout();\r | |
629 | delete me.hasLoadingHeight;\r | |
630 | }\r | |
631 | },\r | |
632 | \r | |
633 | beforeRender: function() {\r | |
634 | this.callParent(arguments);\r | |
635 | this.getSelectionModel().beforeViewRender(this);\r | |
636 | },\r | |
637 | \r | |
638 | afterRender: function() {\r | |
639 | this.callParent(arguments);\r | |
640 | \r | |
641 | // Subclasses may set focusable to false.\r | |
642 | // BoundList is not focusable.\r | |
643 | // BoundList processes key events from its boundField.\r | |
644 | if (this.focusable) {\r | |
645 | this.focusEl = this.el;\r | |
646 | }\r | |
647 | },\r | |
648 | \r | |
649 | getRefItems: function() {\r | |
650 | var mask = this.loadMask,\r | |
651 | result = [];\r | |
652 | \r | |
653 | if (mask && mask.isComponent) {\r | |
654 | result.push(mask);\r | |
655 | }\r | |
656 | return result;\r | |
657 | },\r | |
658 | \r | |
659 | getSelection: function() {\r | |
660 | return this.getSelectionModel().getSelection();\r | |
661 | },\r | |
662 | \r | |
663 | updateSelection: function(selection) {\r | |
664 | var me = this,\r | |
665 | sm;\r | |
666 | \r | |
667 | if (!me.ignoreNextSelection) {\r | |
668 | me.ignoreNextSelection = true;\r | |
669 | sm = me.getSelectionModel();\r | |
670 | if (selection) {\r | |
671 | sm.select(selection);\r | |
672 | } else {\r | |
673 | sm.deselectAll();\r | |
674 | }\r | |
675 | me.ignoreNextSelection = false;\r | |
676 | }\r | |
677 | },\r | |
678 | \r | |
679 | updateBindSelection: function(selModel, selection) {\r | |
680 | var me = this,\r | |
681 | selected = null;\r | |
682 | \r | |
683 | if (!me.ignoreNextSelection) {\r | |
684 | me.ignoreNextSelection = true;\r | |
685 | if (selection.length) {\r | |
686 | selected = selModel.getLastSelected();\r | |
687 | me.hasHadSelection = true;\r | |
688 | }\r | |
689 | if (me.hasHadSelection) {\r | |
690 | me.setSelection(selected);\r | |
691 | }\r | |
692 | me.ignoreNextSelection = false;\r | |
693 | }\r | |
694 | },\r | |
695 | \r | |
696 | applySelectionModel: function(selModel, oldSelModel) { \r | |
697 | var me = this,\r | |
698 | grid = me.grid,\r | |
699 | mode, ariaAttr, ariaDom;\r | |
700 | \r | |
701 | if (oldSelModel) {\r | |
702 | oldSelModel.un({\r | |
703 | scope: me,\r | |
704 | selectionchange: me.updateBindSelection,\r | |
705 | lastselectedchanged: me.updateBindSelection,\r | |
706 | select: me.ariaSelect,\r | |
707 | deselect: me.ariaDeselect\r | |
708 | });\r | |
709 | \r | |
710 | Ext.destroy(me.selModelRelayer);\r | |
711 | selModel = Ext.Factory.selection(selModel);\r | |
712 | }\r | |
713 | // If this is the initial configuration, pull overriding configs in from this view\r | |
714 | else {\r | |
715 | if (selModel && selModel.isSelectionModel) {\r | |
716 | selModel.locked = me.disableSelection;\r | |
717 | } else {\r | |
718 | if (me.simpleSelect) {\r | |
719 | mode = 'SIMPLE';\r | |
720 | } else if (me.multiSelect) {\r | |
721 | mode = 'MULTI';\r | |
722 | } else {\r | |
723 | mode = 'SINGLE';\r | |
724 | }\r | |
725 | \r | |
726 | if (typeof selModel === 'string') {\r | |
727 | selModel = {\r | |
728 | type: selModel\r | |
729 | };\r | |
730 | }\r | |
731 | selModel = Ext.Factory.selection(Ext.apply({\r | |
732 | allowDeselect: me.allowDeselect || me.multiSelect,\r | |
733 | mode: mode,\r | |
734 | locked: me.disableSelection\r | |
735 | }, selModel));\r | |
736 | }\r | |
737 | }\r | |
738 | \r | |
739 | // Grids should have aria-multiselectable on their ariaEl instead\r | |
740 | if (selModel.mode !== 'SINGLE') {\r | |
741 | ariaDom = (grid || me).ariaEl.dom;\r | |
742 | \r | |
743 | if (ariaDom) {\r | |
744 | ariaDom.setAttribute('aria-multiselectable', true);\r | |
745 | }\r | |
746 | else if (!grid) {\r | |
747 | ariaAttr = me.ariaRenderAttributes || (me.ariaRenderAttributes = {});\r | |
748 | ariaAttr['aria-multiselectable'] = true;\r | |
749 | }\r | |
750 | }\r | |
751 | \r | |
752 | me.selModelRelayer = me.relayEvents(selModel, [\r | |
753 | 'selectionchange', 'beforeselect', 'beforedeselect', 'select', 'deselect', 'focuschange'\r | |
754 | ]);\r | |
755 | \r | |
756 | selModel.on({\r | |
757 | scope: me,\r | |
758 | lastselectedchanged: me.updateBindSelection,\r | |
759 | selectionchange: me.updateBindSelection,\r | |
760 | select: me.ariaSelect,\r | |
761 | deselect: me.ariaDeselect\r | |
762 | });\r | |
763 | \r | |
764 | return selModel;\r | |
765 | },\r | |
766 | \r | |
767 | updateSelectionModel: function(selectionModel) {\r | |
768 | // Keep the legacy property correct\r | |
769 | this.selModel = selectionModel;\r | |
770 | },\r | |
771 | \r | |
772 | applyNavigationModel: function (navigationModel) {\r | |
773 | return Ext.Factory.viewNavigation(navigationModel);\r | |
774 | },\r | |
775 | \r | |
776 | onFocusEnter: function(e) {\r | |
777 | var me = this,\r | |
778 | navigationModel = me.getNavigationModel(),\r | |
779 | focusPosition;\r | |
780 | \r | |
781 | // Disable tabbability of elements within this view.\r | |
782 | me.toggleChildrenTabbability(false);\r | |
783 | \r | |
784 | if (!me.itemFocused && me.all.getCount()) {\r | |
785 | focusPosition = navigationModel.getLastFocused();\r | |
786 | navigationModel.setPosition(focusPosition || 0, e.event, null, !focusPosition);\r | |
787 | \r | |
788 | // We now contain focus is that was successful\r | |
789 | me.itemFocused = navigationModel.getPosition() != null;\r | |
790 | }\r | |
791 | \r | |
792 | // View's main el should be kept untabbable, otherwise pressing\r | |
793 | // Shift-Tab key in the view would move the focus to the main el\r | |
794 | // which will then bounce it back to the last focused item.\r | |
795 | // That would effectively make Shift-Tab unusable.\r | |
796 | if (me.itemFocused) {\r | |
797 | this.el.dom.setAttribute('tabIndex', '-1');\r | |
798 | }\r | |
799 | \r | |
800 | me.callParent([e]);\r | |
801 | },\r | |
802 | \r | |
803 | onFocusLeave: function(e) {\r | |
804 | var me = this;\r | |
805 | \r | |
806 | // Ignore this event if we do not actually contain focus,\r | |
807 | // or if the reason for focus exiting was that we are refreshing.\r | |
808 | if (me.itemFocused && !me.refreshing) {\r | |
809 | \r | |
810 | // Blur the focused cell\r | |
811 | me.getNavigationModel().setPosition(null, e.event, null, true);\r | |
812 | \r | |
813 | me.itemFocused = false;\r | |
814 | me.el.dom.setAttribute('tabIndex', 0);\r | |
815 | }\r | |
816 | \r | |
817 | me.callParent([e]);\r | |
818 | },\r | |
819 | \r | |
820 | ariaSelect: function(selModel, record) {\r | |
821 | var node = this.getNode(record);\r | |
822 | \r | |
823 | if (node) {\r | |
824 | node.setAttribute('aria-selected', true);\r | |
825 | }\r | |
826 | },\r | |
827 | \r | |
828 | ariaDeselect: function(selModel, record) {\r | |
829 | var node = this.getNode(record);\r | |
830 | \r | |
831 | if (node) {\r | |
832 | node.removeAttribute('aria-selected');\r | |
833 | }\r | |
834 | },\r | |
835 | \r | |
836 | onRemoved: function(isDestroying) {\r | |
837 | this.callParent([isDestroying]);\r | |
838 | \r | |
839 | // IE does not fire focusleave on removal from DOM\r | |
840 | if (!isDestroying) {\r | |
841 | this.onFocusLeave({});\r | |
842 | }\r | |
843 | },\r | |
844 | \r | |
845 | /**\r | |
846 | * Refreshes the view by reloading the data from the store and re-rendering the template.\r | |
847 | * @since 2.3.0\r | |
848 | */\r | |
849 | refresh: function() {\r | |
850 | var me = this,\r | |
851 | items = me.all,\r | |
852 | prevItemCount = items.getCount(),\r | |
853 | refreshCounter = me.refreshCounter,\r | |
854 | targetEl,\r | |
855 | dom,\r | |
856 | records,\r | |
857 | selModel = me.getSelectionModel(),\r | |
858 | restoreFocus,\r | |
859 | // If there are items in the view, then honour preserveScrollOnRefresh\r | |
860 | scroller = refreshCounter && items.getCount() && me.preserveScrollOnRefresh && me.getScrollable(),\r | |
861 | scrollPos;\r | |
862 | \r | |
863 | if (!me.rendered || me.destroyed) {\r | |
864 | return;\r | |
865 | }\r | |
866 | \r | |
867 | if (!me.hasListeners.beforerefresh || me.fireEvent('beforerefresh', me) !== false) {\r | |
868 | \r | |
869 | // So that listeners to itemremove events know that its because of a refresh\r | |
870 | me.refreshing = true;\r | |
871 | \r | |
872 | // If focus was in this view, this will restore it\r | |
873 | restoreFocus = me.saveFocusState();\r | |
874 | \r | |
875 | targetEl = me.getTargetEl();\r | |
876 | records = me.getViewRange();\r | |
877 | dom = targetEl.dom;\r | |
878 | \r | |
879 | if (scroller) {\r | |
880 | scrollPos = scroller.getPosition();\r | |
881 | if (!(scrollPos.x || scrollPos.y)) {\r | |
882 | scrollPos = null;\r | |
883 | }\r | |
884 | }\r | |
885 | \r | |
886 | if (refreshCounter) {\r | |
887 | me.clearViewEl();\r | |
888 | me.refreshCounter++;\r | |
889 | } else {\r | |
890 | me.refreshCounter = 1;\r | |
891 | }\r | |
892 | \r | |
893 | // Usually, for an empty record set, this would be blank, but when the Template\r | |
894 | // Creates markup outside of the record loop, this must still be honoured even if there are no\r | |
895 | // records.\r | |
896 | me.tpl.append(targetEl, me.collectData(records, items.startIndex || 0));\r | |
897 | \r | |
898 | // The emptyText is now appended to the View's element\r | |
899 | // after nodes outside the tpl block.\r | |
900 | if (records.length < 1) {\r | |
901 | // Process empty text unless the store is being cleared.\r | |
902 | me.addEmptyText();\r | |
903 | items.clear();\r | |
904 | } else {\r | |
905 | me.collectNodes(targetEl.dom);\r | |
906 | me.updateIndexes(0);\r | |
907 | }\r | |
908 | \r | |
909 | // If focus was in any way in this view, this will restore it\r | |
910 | restoreFocus();\r | |
911 | \r | |
912 | // Some subclasses do not need to do this. TableView does not need to do this - it renders selected class using its tenmplate.\r | |
913 | if (me.refreshSelmodelOnRefresh !== false) {\r | |
914 | selModel.refresh();\r | |
915 | }\r | |
916 | \r | |
917 | me.refreshNeeded = false;\r | |
918 | \r | |
919 | // Ensure layout system knows about new content size.\r | |
920 | // If number of items have changed, force a layout.\r | |
921 | me.refreshSize(items.getCount() !== prevItemCount);\r | |
922 | \r | |
923 | me.fireEvent('refresh', me, records);\r | |
924 | \r | |
925 | if (scroller) {\r | |
926 | scroller.scrollTo(scrollPos);\r | |
927 | }\r | |
928 | \r | |
929 | // Upon first refresh, fire the viewready event.\r | |
930 | // Reconfiguring the grid "renews" this event.\r | |
931 | if (!me.viewReady) {\r | |
932 | // Fire an event when deferred content becomes available.\r | |
933 | me.viewReady = true;\r | |
934 | me.fireEvent('viewready', me);\r | |
935 | }\r | |
936 | \r | |
937 | me.refreshing = false;\r | |
938 | me.refreshScroll();\r | |
939 | \r | |
940 | me.cleanupData();\r | |
941 | }\r | |
942 | },\r | |
943 | \r | |
944 | addEmptyText: function() { \r | |
945 | var me = this,\r | |
946 | store = me.getStore();\r | |
947 | \r | |
948 | if (me.emptyText && !store.isLoading() && (!me.deferEmptyText || me.refreshCounter > 1 || store.isLoaded())) {\r | |
949 | me.emptyEl = Ext.core.DomHelper.insertHtml('beforeEnd', me.getTargetEl().dom, me.emptyText);\r | |
950 | }\r | |
951 | },\r | |
952 | \r | |
953 | getViewRange: function() {\r | |
954 | return this.dataSource.getRange();\r | |
955 | },\r | |
956 | \r | |
957 | /**\r | |
958 | * @private\r | |
959 | * Called by the framework when the view is refreshed, or when rows are added or deleted.\r | |
960 | *\r | |
961 | * These operations may cause the view's dimensions to change, and if the owning container\r | |
962 | * is shrinkwrapping this view, then the layout must be updated to accommodate these new dimensions.\r | |
963 | */\r | |
964 | refreshSize: function(forceLayout) {\r | |
965 | var me = this,\r | |
966 | sizeModel = me.getSizeModel(),\r | |
967 | scroller = me.getScrollable();\r | |
968 | \r | |
969 | if (sizeModel.height.shrinkWrap || sizeModel.width.shrinkWrap || forceLayout) {\r | |
970 | me.updateLayout();\r | |
971 | }\r | |
972 | \r | |
973 | // We need to refresh the Scroller (BufferedRenderer has to do this if present).\r | |
974 | // But the first refresh takes place on the leading edge of the first layout\r | |
975 | // before the Scroller has been initialized, so do it as soon\r | |
976 | // as we reach boxready.\r | |
977 | else if (me.touchScroll && !me.bufferedRenderer) {\r | |
978 | if (scroller) {\r | |
979 | scroller.refresh();\r | |
980 | } else {\r | |
981 | me.on({\r | |
982 | boxready: me.refreshScroll,\r | |
983 | scope: me,\r | |
984 | single: true\r | |
985 | });\r | |
986 | }\r | |
987 | }\r | |
988 | },\r | |
989 | \r | |
990 | afterFirstLayout: function(width, height) {\r | |
991 | var me = this,\r | |
992 | scroller = me.getScrollable();\r | |
993 | \r | |
994 | if (scroller) {\r | |
995 | scroller.on({\r | |
996 | scroll: me.onViewScroll,\r | |
997 | scrollend: me.onViewScrollEnd,\r | |
998 | scope: me,\r | |
999 | onFrame: !!Ext.global.requestAnimationFrame\r | |
1000 | });\r | |
1001 | }\r | |
1002 | me.callParent([width, height]);\r | |
1003 | },\r | |
1004 | \r | |
1005 | clearViewEl: function() {\r | |
1006 | var me = this,\r | |
1007 | targetEl = me.getTargetEl(),\r | |
1008 | nodeContainerIsTarget = me.getNodeContainer() === targetEl;\r | |
1009 | \r | |
1010 | me.clearEmptyEl();\r | |
1011 | // If nodeContainer is the el, just clear the innerHTML. Otherwise, we need\r | |
1012 | // to manually remove each node we know about.\r | |
1013 | me.all.clear(!nodeContainerIsTarget);\r | |
1014 | if (nodeContainerIsTarget) {\r | |
1015 | targetEl.dom.innerHTML = '';\r | |
1016 | }\r | |
1017 | },\r | |
1018 | \r | |
1019 | clearEmptyEl: function() {\r | |
1020 | var emptyEl = this.emptyEl;\r | |
1021 | \r | |
1022 | // emptyEl is likely to be a TextNode if emptyText is not HTML code.\r | |
1023 | // Use native DOM to remove it.\r | |
1024 | if (emptyEl) {\r | |
1025 | Ext.removeNode(emptyEl);\r | |
1026 | }\r | |
1027 | this.emptyEl = null;\r | |
1028 | },\r | |
1029 | \r | |
1030 | onViewScroll: function(scroller, x, y) {\r | |
1031 | this.fireEvent('scroll', this, x, y);\r | |
1032 | },\r | |
1033 | \r | |
1034 | onViewScrollEnd: function(scroller, x, y) {\r | |
1035 | this.fireEvent('scrollend', this, x, y);\r | |
1036 | },\r | |
1037 | \r | |
1038 | /**\r | |
1039 | * Saves the scrollState in a private variable. Must be used in conjunction with restoreScrollState.\r | |
1040 | * @private\r | |
1041 | */\r | |
1042 | saveScrollState: function() {\r | |
1043 | var me = this,\r | |
1044 | state = me.scrollState;\r | |
1045 | \r | |
1046 | if (me.rendered) {\r | |
1047 | state.left = me.getScrollX();\r | |
1048 | state.top = me.getScrollY();\r | |
1049 | }\r | |
1050 | },\r | |
1051 | \r | |
1052 | /**\r | |
1053 | * Restores the scrollState.\r | |
1054 | * Must be used in conjunction with saveScrollState\r | |
1055 | * @private\r | |
1056 | */\r | |
1057 | restoreScrollState: function() {\r | |
1058 | var me = this,\r | |
1059 | state = me.scrollState;\r | |
1060 | \r | |
1061 | if (me.rendered) {\r | |
1062 | me.setScrollX(state.left);\r | |
1063 | me.setScrollY(state.top);\r | |
1064 | }\r | |
1065 | },\r | |
1066 | \r | |
1067 | /**\r | |
1068 | * Function which can be overridden to provide custom formatting for each Record that is used by this\r | |
1069 | * DataView's {@link #tpl template} to render each node.\r | |
1070 | * @param {Object/Object[]} data The raw data object that was used to create the Record.\r | |
1071 | * @param {Number} recordIndex the index number of the Record being prepared for rendering.\r | |
1072 | * @param {Ext.data.Model} record The Record being prepared for rendering.\r | |
1073 | * @return {Array/Object} The formatted data in a format expected by the internal {@link #tpl template}'s overwrite() method.\r | |
1074 | * (either an array if your params are numeric (i.e. {0}) or an object (i.e. {foo: 'bar'}))\r | |
1075 | * @since 2.3.0\r | |
1076 | */\r | |
1077 | prepareData: function(data, index, record) {\r | |
1078 | var associatedData, attr, hasCopied;\r | |
1079 | if (record) {\r | |
1080 | associatedData = record.getAssociatedData();\r | |
1081 | for (attr in associatedData) {\r | |
1082 | if (associatedData.hasOwnProperty(attr)) {\r | |
1083 | // This would be better done in collectData, however\r | |
1084 | // we only need to copy the data object if we have any associations,\r | |
1085 | // so we optimize it by only copying if we must.\r | |
1086 | // We do this so we don't mutate the underlying record.data\r | |
1087 | if (!hasCopied) {\r | |
1088 | data = Ext.Object.chain(data);\r | |
1089 | hasCopied = true;\r | |
1090 | }\r | |
1091 | data[attr] = associatedData[attr];\r | |
1092 | }\r | |
1093 | }\r | |
1094 | }\r | |
1095 | return data;\r | |
1096 | },\r | |
1097 | \r | |
1098 | /**\r | |
1099 | * Function which can be overridden which returns the data object passed to this\r | |
1100 | * DataView's {@link #cfg-tpl template} to render the whole DataView.\r | |
1101 | *\r | |
1102 | * This is usually an Array of data objects, each element of which is processed by an\r | |
1103 | * {@link Ext.XTemplate XTemplate} which uses `'<tpl for=".">'` to iterate over its supplied\r | |
1104 | * data object as an Array. However, <i>named</i> properties may be placed into the data object to\r | |
1105 | * provide non-repeating data such as headings, totals etc.\r | |
1106 | *\r | |
1107 | * @param {Ext.data.Model[]} records An Array of {@link Ext.data.Model}s to be rendered into the DataView.\r | |
1108 | * @param {Number} startIndex the index number of the Record being prepared for rendering.\r | |
1109 | * @return {Object[]} An Array of data objects to be processed by a repeating XTemplate. May also\r | |
1110 | * contain <i>named</i> properties.\r | |
1111 | * @since 2.3.0\r | |
1112 | */\r | |
1113 | collectData: function(records, startIndex){\r | |
1114 | var data = [],\r | |
1115 | i = 0,\r | |
1116 | len = records.length,\r | |
1117 | record;\r | |
1118 | \r | |
1119 | for (; i < len; i++) {\r | |
1120 | record = records[i];\r | |
1121 | data[i] = this.prepareData(record.data, startIndex + i, record);\r | |
1122 | }\r | |
1123 | return data;\r | |
1124 | },\r | |
1125 | \r | |
1126 | cleanupData: Ext.emptyFn,\r | |
1127 | \r | |
1128 | bufferRender: function(records, index) {\r | |
1129 | var me = this,\r | |
1130 | div = me.renderBuffer,\r | |
1131 | result = document.createDocumentFragment(),\r | |
1132 | nodes, len, i;\r | |
1133 | \r | |
1134 | me.tpl.overwrite(div, me.collectData(records, index));\r | |
1135 | nodes = Ext.fly(div).query(me.getItemSelector());\r | |
1136 | for (i = 0, len = nodes.length; i < len; i++) {\r | |
1137 | result.appendChild(nodes[i]);\r | |
1138 | }\r | |
1139 | return {\r | |
1140 | fragment: result,\r | |
1141 | children: nodes\r | |
1142 | };\r | |
1143 | },\r | |
1144 | \r | |
1145 | // Element which contains rows\r | |
1146 | nodeContainerSelector: null,\r | |
1147 | \r | |
1148 | getNodeContainer: function() {\r | |
1149 | var target = this.getTargetEl(),\r | |
1150 | selector = this.nodeContainerSelector;\r | |
1151 | return selector ? target.down(selector, true) : target;\r | |
1152 | },\r | |
1153 | \r | |
1154 | /**\r | |
1155 | * Returns a CSS selector which selects the element which contains record nodes.\r | |
1156 | */\r | |
1157 | getNodeContainerSelector: function() {\r | |
1158 | return this.nodeContainerSelector;\r | |
1159 | },\r | |
1160 | \r | |
1161 | onUpdate: function(store, record, operation, modifiedFieldNames, details) {\r | |
1162 | var me = this,\r | |
1163 | isFiltered = details && details.filtered;\r | |
1164 | \r | |
1165 | // If, due to filtering or buffered rendering, or node collapse, the updated record is not\r | |
1166 | // represented in the rendered structure, this is a no-op.\r | |
1167 | // The correct, new values will be rendered the next time the record becomes visible and is rendered.\r | |
1168 | if (!isFiltered && me.getNode(record)) {\r | |
1169 | \r | |
1170 | // If we are throttling UI updates (See the updateDelay global config), ensure there's a change entry\r | |
1171 | // queued for the record in the global queue.\r | |
1172 | if (me.throttledUpdate) {\r | |
1173 | me.statics().queueRecordChange(me, store, record, operation, modifiedFieldNames);\r | |
1174 | } else {\r | |
1175 | me.handleUpdate.apply(me, arguments);\r | |
1176 | }\r | |
1177 | }\r | |
1178 | },\r | |
1179 | \r | |
1180 | handleUpdate: function(store, record){\r | |
1181 | var me = this,\r | |
1182 | index,\r | |
1183 | node,\r | |
1184 | selModel = me.getSelectionModel();\r | |
1185 | \r | |
1186 | if (me.viewReady) {\r | |
1187 | index = me.dataSource.indexOf(record);\r | |
1188 | \r | |
1189 | // If the record has been removed from the data source since the changes were made, do nothing\r | |
1190 | if (index > -1) {\r | |
1191 | // ensure the node actually exists in the DOM\r | |
1192 | if (me.getNode(record)) {\r | |
1193 | node = me.bufferRender([record], index).children[0];\r | |
1194 | me.all.replaceElement(index, node, true);\r | |
1195 | me.updateIndexes(index, index);\r | |
1196 | // Maintain selection after update\r | |
1197 | selModel.onUpdate(record);\r | |
1198 | me.refreshSizePending = true;\r | |
1199 | if (selModel.isSelected(record)) {\r | |
1200 | me.onItemSelect(record);\r | |
1201 | }\r | |
1202 | if (me.hasListeners.itemupdate) {\r | |
1203 | me.fireEvent('itemupdate', record, index, node);\r | |
1204 | }\r | |
1205 | return node;\r | |
1206 | }\r | |
1207 | }\r | |
1208 | }\r | |
1209 | },\r | |
1210 | \r | |
1211 | /**\r | |
1212 | * @private\r | |
1213 | * Respond to store replace event which is fired by GroupStore group expand/collapse operations.\r | |
1214 | * This saves a layout because a remove and add operation are coalesced in this operation.\r | |
1215 | */\r | |
1216 | onReplace: function(store, startIndex, oldRecords, newRecords) {\r | |
1217 | var me = this,\r | |
1218 | all = me.all,\r | |
1219 | selModel = me.getSelectionModel(),\r | |
1220 | origStart = startIndex,\r | |
1221 | result, item, fragment, children, oldItems, endIndex, restoreFocus;\r | |
1222 | \r | |
1223 | if (me.rendered) {\r | |
1224 | // Insert the new items before the remove block\r | |
1225 | result = me.bufferRender(newRecords, startIndex, true);\r | |
1226 | fragment = result.fragment;\r | |
1227 | children = result.children;\r | |
1228 | item = all.item(startIndex);\r | |
1229 | \r | |
1230 | if (item) {\r | |
1231 | all.item(startIndex).insertSibling(fragment, 'before', true);\r | |
1232 | } else {\r | |
1233 | me.appendNodes(fragment);\r | |
1234 | }\r | |
1235 | \r | |
1236 | all.insert(startIndex, children);\r | |
1237 | \r | |
1238 | if (oldRecords.length) {\r | |
1239 | // If focus was in the view, this will return\r | |
1240 | // a function which will restore that state.\r | |
1241 | // If not, a function which does nothing.\r | |
1242 | restoreFocus = me.saveFocusState();\r | |
1243 | }\r | |
1244 | \r | |
1245 | startIndex += newRecords.length;\r | |
1246 | endIndex = startIndex + oldRecords.length - 1;\r | |
1247 | \r | |
1248 | // Remove the items which correspond to old records\r | |
1249 | oldItems = all.removeRange(startIndex, endIndex, true);\r | |
1250 | \r | |
1251 | // Some subclasses do not need to do this. TableView does not need to do this.\r | |
1252 | if (me.refreshSelmodelOnRefresh !== false) {\r | |
1253 | selModel.refresh();\r | |
1254 | }\r | |
1255 | \r | |
1256 | // Update the row indices (TableView) doesn't do this.\r | |
1257 | me.updateIndexes(startIndex);\r | |
1258 | \r | |
1259 | if (me.hasListeners.itemremove) {\r | |
1260 | me.fireEvent('itemremove', oldRecords, origStart, oldItems, me);\r | |
1261 | }\r | |
1262 | \r | |
1263 | if (me.hasListeners.itemadd) {\r | |
1264 | me.fireEvent('itemadd', newRecords, origStart, children);\r | |
1265 | }\r | |
1266 | \r | |
1267 | // If focus was in this view, this will restore it\r | |
1268 | restoreFocus();\r | |
1269 | \r | |
1270 | me.refreshSize();\r | |
1271 | }\r | |
1272 | },\r | |
1273 | \r | |
1274 | onAdd: function(store, records, index) {\r | |
1275 | var me = this,\r | |
1276 | nodes,\r | |
1277 | selModel = me.getSelectionModel();\r | |
1278 | \r | |
1279 | if (me.rendered) {\r | |
1280 | // If we are adding into an empty view, we must refresh in order that the *full tpl* is applied\r | |
1281 | // which might create boilerplate content *around* the record nodes.\r | |
1282 | if (me.all.getCount() === 0) {\r | |
1283 | me.refresh();\r | |
1284 | nodes = me.all.slice();\r | |
1285 | } else {\r | |
1286 | nodes = me.doAdd(records, index);\r | |
1287 | // Some subclasses do not need to do this. TableView does not need to do this.\r | |
1288 | if (me.refreshSelmodelOnRefresh !== false) {\r | |
1289 | selModel.refresh();\r | |
1290 | }\r | |
1291 | me.updateIndexes(index);\r | |
1292 | \r | |
1293 | // Ensure layout system knows about new content size\r | |
1294 | me.refreshSizePending = true;\r | |
1295 | }\r | |
1296 | \r | |
1297 | if (me.hasListeners.itemadd) {\r | |
1298 | me.fireEvent('itemadd', records, index, nodes);\r | |
1299 | }\r | |
1300 | }\r | |
1301 | \r | |
1302 | },\r | |
1303 | \r | |
1304 | appendNodes: function(nodes) {\r | |
1305 | var all = this.all,\r | |
1306 | count = all.getCount();\r | |
1307 | \r | |
1308 | if (this.nodeContainerSelector) {\r | |
1309 | this.getNodeContainer().appendChild(nodes);\r | |
1310 | } else {\r | |
1311 | // If we don't have a nodeContainerSelector, we may have our\r | |
1312 | // itemSelector nodes wrapped in some other container, so we\r | |
1313 | // can't just append them to the node container, it may be the wrong element\r | |
1314 | all.item(count - 1).insertSibling(nodes, 'after');\r | |
1315 | }\r | |
1316 | },\r | |
1317 | \r | |
1318 | doAdd: function(records, index) {\r | |
1319 | var me = this,\r | |
1320 | result = me.bufferRender(records, index, true),\r | |
1321 | fragment = result.fragment,\r | |
1322 | children = result.children,\r | |
1323 | all = me.all,\r | |
1324 | count = all.getCount(),\r | |
1325 | firstRowIndex = all.startIndex || 0,\r | |
1326 | \r | |
1327 | lastRowIndex = all.endIndex || count - 1;\r | |
1328 | \r | |
1329 | if (count === 0 || index > lastRowIndex) {\r | |
1330 | me.appendNodes(fragment);\r | |
1331 | } else if (index <= firstRowIndex) {\r | |
1332 | all.item(firstRowIndex).insertSibling(fragment, 'before', true);\r | |
1333 | } else {\r | |
1334 | all.item(index).insertSibling(children, 'before', true);\r | |
1335 | }\r | |
1336 | \r | |
1337 | all.insert(index, children);\r | |
1338 | return children;\r | |
1339 | },\r | |
1340 | \r | |
1341 | onRemove: function(store, records, index) {\r | |
1342 | var me = this,\r | |
1343 | rows = me.all,\r | |
1344 | fireItemRemove = me.hasListeners.itemremove,\r | |
1345 | currIdx, i, record, nodes, node, restoreFocus;\r | |
1346 | \r | |
1347 | if (rows.getCount()) {\r | |
1348 | if (me.dataSource.getCount() === 0) {\r | |
1349 | // Refresh so emptyText can be applied if necessary\r | |
1350 | if (fireItemRemove) {\r | |
1351 | me.fireEvent('itemremove', records, index, me.getNodes(index, index + records.length - 1));\r | |
1352 | }\r | |
1353 | me.refresh();\r | |
1354 | } else {\r | |
1355 | // If this view contains focus, this will return\r | |
1356 | // a function which will restore that state.\r | |
1357 | restoreFocus = me.saveFocusState();\r | |
1358 | \r | |
1359 | // Just remove the elements which corresponds to the removed records\r | |
1360 | // The tpl's full HTML will still be in place.\r | |
1361 | if (fireItemRemove) {\r | |
1362 | nodes = [];\r | |
1363 | }\r | |
1364 | for (i = records.length - 1; i >= 0; --i) {\r | |
1365 | record = records[i];\r | |
1366 | currIdx = index + i;\r | |
1367 | if (nodes) {\r | |
1368 | node = rows.item(currIdx);\r | |
1369 | nodes[i] = node ? node.dom : undefined;\r | |
1370 | }\r | |
1371 | \r | |
1372 | if (rows.item(currIdx)) {\r | |
1373 | me.doRemove(record, currIdx);\r | |
1374 | }\r | |
1375 | }\r | |
1376 | \r | |
1377 | if (fireItemRemove) {\r | |
1378 | me.fireEvent('itemremove', records, index, nodes, me);\r | |
1379 | }\r | |
1380 | \r | |
1381 | // If focus was in this view, this will restore it\r | |
1382 | restoreFocus();\r | |
1383 | me.updateIndexes(index);\r | |
1384 | }\r | |
1385 | \r | |
1386 | // Ensure layout system knows about new content size\r | |
1387 | me.refreshSizePending = true;\r | |
1388 | }\r | |
1389 | },\r | |
1390 | \r | |
1391 | doRemove: function(record, index) {\r | |
1392 | this.all.removeElement(index, true);\r | |
1393 | },\r | |
1394 | \r | |
1395 | /**\r | |
1396 | * @private\r | |
1397 | * Called prior to an operation which mey remove focus from this view by some kind of DOM operation.\r | |
1398 | *\r | |
1399 | * If this view contains focus, this method returns a function which, when called after\r | |
1400 | * the disruptive DOM operation will restore focus to the same record, or, if the record has\r | |
1401 | * been removed to the same item index..\r | |
1402 | *\r | |
1403 | * @returns {Function} A function that will restore focus if focus was within this view,\r | |
1404 | * or a function which does nothing is focus is not in this view.\r | |
1405 | */\r | |
1406 | saveFocusState: function() {\r | |
1407 | var me = this,\r | |
1408 | store = me.dataSource || me.store,\r | |
1409 | navModel = me.getNavigationModel(),\r | |
1410 | lastFocusedIndex = navModel.recordIndex,\r | |
1411 | lastFocusedRec = navModel.record;\r | |
1412 | \r | |
1413 | // Check if we really have focus.\r | |
1414 | // Some NavigationModels record position with focus outside of the view.\r | |
1415 | // This happens in BoundLists when focus stays in the bound field.\r | |
1416 | if (me.el.contains(Ext.Element.getActiveElement())) {\r | |
1417 | // Blur the focused descendant, but do not trigger focusLeave.\r | |
1418 | me.el.dom.focus();\r | |
1419 | \r | |
1420 | // The following function will attempt to refocus back to the same record if it is still there,\r | |
1421 | // or the same item index.\r | |
1422 | return function() {\r | |
1423 | // If we still have data, attempt to refocus at the same record, or the same item index..\r | |
1424 | if (store.getCount()) {\r | |
1425 | \r | |
1426 | // Adjust expectations of where we are able to refocus according to what kind of destruction\r | |
1427 | // might have been wrought on this view's DOM during focus save.\r | |
1428 | lastFocusedIndex = Math.min(lastFocusedIndex, me.all.getCount() - 1);\r | |
1429 | navModel.setPosition(store.contains(lastFocusedRec) ? lastFocusedRec : lastFocusedIndex, null, null, true);\r | |
1430 | }\r | |
1431 | };\r | |
1432 | }\r | |
1433 | return Ext.emptyFn;\r | |
1434 | },\r | |
1435 | \r | |
1436 | /**\r | |
1437 | * Refreshes an individual node's data from the store.\r | |
1438 | * @param {Ext.data.Model/Number} record The record or index of the record to update.\r | |
1439 | * @since 2.3.0\r | |
1440 | */\r | |
1441 | refreshNode: function(record) {\r | |
1442 | if (Ext.isNumber(record)) {\r | |
1443 | record = this.store.getAt(record);\r | |
1444 | }\r | |
1445 | this.onUpdate(this.dataSource, record);\r | |
1446 | },\r | |
1447 | \r | |
1448 | updateIndexes: function(startIndex, endIndex) {\r | |
1449 | var nodes = this.all.elements,\r | |
1450 | node,\r | |
1451 | records = this.getViewRange(),\r | |
1452 | i,\r | |
1453 | myId = this.id;\r | |
1454 | \r | |
1455 | startIndex = startIndex || 0;\r | |
1456 | endIndex = endIndex || ((endIndex === 0) ? 0 : (nodes.length - 1));\r | |
1457 | for (i = startIndex; i <= endIndex; i++) {\r | |
1458 | node = nodes[i];\r | |
1459 | node.setAttribute('data-recordIndex', i);\r | |
1460 | node.setAttribute('data-recordId', records[i].internalId);\r | |
1461 | node.setAttribute('data-boundView', myId);\r | |
1462 | }\r | |
1463 | },\r | |
1464 | \r | |
1465 | /**\r | |
1466 | * Changes the data store bound to this view and refreshes it.\r | |
1467 | * @param {Ext.data.Store} store The store to bind to this view\r | |
1468 | * @since 3.4.0\r | |
1469 | */\r | |
1470 | bindStore: function(store, initial) {\r | |
1471 | var me = this,\r | |
1472 | selModel = me.getSelectionModel(),\r | |
1473 | navModel = me.getNavigationModel();\r | |
1474 | \r | |
1475 | selModel.bindStore(store);\r | |
1476 | selModel.bindComponent(store ? me : null);\r | |
1477 | me.mixins.storeholder.bindStore.apply(me, arguments);\r | |
1478 | \r | |
1479 | // Navigation model must bind to new store\r | |
1480 | navModel.setStore(store);\r | |
1481 | \r | |
1482 | // If we have already achieved our first layout, refresh immediately.\r | |
1483 | // If we bind to the Store before the first layout, then beforeLayout will\r | |
1484 | // call doFirstRefresh\r | |
1485 | if (store && me.componentLayoutCounter) {\r | |
1486 | // If not the initial bind, we enforce noDefer.\r | |
1487 | me.doFirstRefresh(store, !initial);\r | |
1488 | }\r | |
1489 | },\r | |
1490 | \r | |
1491 | /**\r | |
1492 | * @private\r | |
1493 | * Perform the first refresh of the View from a newly bound store.\r | |
1494 | *\r | |
1495 | * This is called when this View has been sized for the first time.\r | |
1496 | */\r | |
1497 | doFirstRefresh: function(store, noDefer) {\r | |
1498 | var me = this;\r | |
1499 | \r | |
1500 | // If we are configured to defer, and *NOT* called from the defer call below\r | |
1501 | if (me.deferInitialRefresh && !noDefer) {\r | |
1502 | Ext.defer(me.doFirstRefresh, 1, me, [store, true]);\r | |
1503 | }\r | |
1504 | \r | |
1505 | else {\r | |
1506 | // 4.1.0: If we have a store, and the Store is *NOT* already loading (a refresh is on the way), then\r | |
1507 | // on first layout, refresh regardless of record count.\r | |
1508 | // Template may contain boilerplate HTML outside of record iteration loop.\r | |
1509 | // Also, emptyText is appended by the refresh method.\r | |
1510 | if (store && !store.isLoading()) {\r | |
1511 | me.refresh();\r | |
1512 | }\r | |
1513 | }\r | |
1514 | },\r | |
1515 | \r | |
1516 | onUnbindStore: function(store) {\r | |
1517 | this.setMaskBind(null);\r | |
1518 | \r | |
1519 | if (this.dataSource === store) {\r | |
1520 | this.dataSource = null;\r | |
1521 | }\r | |
1522 | },\r | |
1523 | \r | |
1524 | onBindStore: function(store, oldStore) {\r | |
1525 | var me = this;\r | |
1526 | \r | |
1527 | // A BufferedStore has to know to reload the most recent visible zone if its View is preserveScrollOnReload\r | |
1528 | if (me.store.isBufferedStore) {\r | |
1529 | me.store.preserveScrollOnReload = me.preserveScrollOnReload;\r | |
1530 | }\r | |
1531 | if (oldStore && oldStore.isBufferedStore) {\r | |
1532 | delete oldStore.preserveScrollOnReload;\r | |
1533 | }\r | |
1534 | \r | |
1535 | me.setMaskBind(store);\r | |
1536 | \r | |
1537 | // When unbinding the data store, the dataSource will be nulled out if it's the same as the data store.\r | |
1538 | // Restore it here.\r | |
1539 | if (!me.dataSource) {\r | |
1540 | me.dataSource = store;\r | |
1541 | }\r | |
1542 | },\r | |
1543 | \r | |
1544 | setMaskBind: function(store) {\r | |
1545 | var mask = this.loadMask;\r | |
1546 | \r | |
1547 | \r | |
1548 | if (this.rendered && mask && store && !mask.bindStore) {\r | |
1549 | mask = this.createMask();\r | |
1550 | }\r | |
1551 | \r | |
1552 | if (mask && mask.bindStore) {\r | |
1553 | mask.bindStore(store);\r | |
1554 | }\r | |
1555 | },\r | |
1556 | \r | |
1557 | getStoreListeners: function() {\r | |
1558 | var me = this;\r | |
1559 | return {\r | |
1560 | refresh: me.onDataRefresh,\r | |
1561 | replace: me.onReplace,\r | |
1562 | add: me.onAdd,\r | |
1563 | remove: me.onRemove,\r | |
1564 | update: me.onUpdate,\r | |
1565 | clear: me.onDataRefresh,\r | |
1566 | beginupdate: me.onBeginUpdate,\r | |
1567 | endupdate: me.onEndUpdate\r | |
1568 | };\r | |
1569 | },\r | |
1570 | \r | |
1571 | onBeginUpdate: function() {\r | |
1572 | ++this.updateSuspendCounter;\r | |
1573 | Ext.suspendLayouts();\r | |
1574 | },\r | |
1575 | \r | |
1576 | onEndUpdate: function() {\r | |
1577 | var me = this;\r | |
1578 | \r | |
1579 | if (me.updateSuspendCounter) {\r | |
1580 | --me.updateSuspendCounter;\r | |
1581 | }\r | |
1582 | \r | |
1583 | Ext.resumeLayouts(true);\r | |
1584 | if (me.refreshSizePending) {\r | |
1585 | me.refreshSize(true);\r | |
1586 | me.refreshSizePending = false;\r | |
1587 | }\r | |
1588 | },\r | |
1589 | \r | |
1590 | /**\r | |
1591 | * @private\r | |
1592 | * Calls this.refresh if this.blockRefresh is not true\r | |
1593 | * @since 3.4.0\r | |
1594 | */\r | |
1595 | onDataRefresh: function(store) {\r | |
1596 | var me = this,\r | |
1597 | preserveScrollOnRefresh = me.preserveScrollOnRefresh;\r | |
1598 | \r | |
1599 | // If this refresh event is fire from a store load, then use the \r | |
1600 | // preserveScrollOnReLoad setting to decide whether to preserve scroll position\r | |
1601 | if (store.loadCount > me.lastRefreshLoadCount) {\r | |
1602 | me.preserveScrollOnRefresh = me.preserveScrollOnReLoad;\r | |
1603 | }\r | |
1604 | me.refreshView();\r | |
1605 | me.preserveScrollOnRefresh = preserveScrollOnRefresh;\r | |
1606 | me.lastRefreshLoadCount = store.loadCount;\r | |
1607 | },\r | |
1608 | \r | |
1609 | refreshView: function() {\r | |
1610 | var me = this,\r | |
1611 | // If we have an ancestor in a non-boxready state (collapsed or in-transition, or hidden), then block the\r | |
1612 | // refresh because the next layout will trigger the refresh\r | |
1613 | blocked = me.blockRefresh || !me.rendered || me.up('[collapsed],[isCollapsingOrExpanding],[hidden]');\r | |
1614 | \r | |
1615 | // If we are blocked in any way due to either a setting, or hidden or collapsed, or animating ancestor, then\r | |
1616 | // the next refresh attempt at the upcoming layout must not defer.\r | |
1617 | if (blocked) {\r | |
1618 | me.refreshNeeded = true;\r | |
1619 | } else {\r | |
1620 | if (me.bufferedRenderer) {\r | |
1621 | me.bufferedRenderer.refreshView();\r | |
1622 | } else {\r | |
1623 | me.refresh();\r | |
1624 | }\r | |
1625 | }\r | |
1626 | },\r | |
1627 | \r | |
1628 | /**\r | |
1629 | * Returns the template node the passed child belongs to, or null if it doesn't belong to one.\r | |
1630 | * @param {HTMLElement} node\r | |
1631 | * @return {HTMLElement} The template node\r | |
1632 | */\r | |
1633 | findItemByChild: function(node){\r | |
1634 | return Ext.fly(node).findParent(this.getItemSelector(), this.getTargetEl());\r | |
1635 | },\r | |
1636 | \r | |
1637 | /**\r | |
1638 | * Returns the template node by the Ext.event.Event or null if it is not found.\r | |
1639 | * @param {Ext.event.Event} e\r | |
1640 | */\r | |
1641 | findTargetByEvent: function(e) {\r | |
1642 | return e.getTarget(this.getItemSelector(), this.getTargetEl());\r | |
1643 | },\r | |
1644 | \r | |
1645 | \r | |
1646 | /**\r | |
1647 | * Gets the currently selected nodes.\r | |
1648 | * @return {HTMLElement[]} An array of HTMLElements\r | |
1649 | * @since 2.3.0\r | |
1650 | */\r | |
1651 | getSelectedNodes: function(){\r | |
1652 | var nodes = [],\r | |
1653 | records = this.getSelectionModel().getSelection(),\r | |
1654 | ln = records.length,\r | |
1655 | i = 0;\r | |
1656 | \r | |
1657 | for (; i < ln; i++) {\r | |
1658 | nodes.push(this.getNode(records[i]));\r | |
1659 | }\r | |
1660 | \r | |
1661 | return nodes;\r | |
1662 | },\r | |
1663 | \r | |
1664 | /**\r | |
1665 | * Gets an array of the records from an array of nodes\r | |
1666 | * @param {HTMLElement[]} nodes The nodes to evaluate\r | |
1667 | * @return {Ext.data.Model[]} records The {@link Ext.data.Model} objects\r | |
1668 | * @since 2.3.0\r | |
1669 | */\r | |
1670 | getRecords: function(nodes) {\r | |
1671 | var records = [],\r | |
1672 | i = 0,\r | |
1673 | len = nodes.length,\r | |
1674 | data = this.dataSource.data;\r | |
1675 | \r | |
1676 | for (; i < len; i++) {\r | |
1677 | records[records.length] = data.getByKey(nodes[i].getAttribute('data-recordId'));\r | |
1678 | }\r | |
1679 | \r | |
1680 | return records;\r | |
1681 | },\r | |
1682 | \r | |
1683 | /**\r | |
1684 | * Gets a record from a node\r | |
1685 | * @param {Ext.dom.Element/HTMLElement} node The node to evaluate\r | |
1686 | *\r | |
1687 | * @return {Ext.data.Model} record The {@link Ext.data.Model} object\r | |
1688 | * @since 2.3.0\r | |
1689 | */\r | |
1690 | getRecord: function(node){\r | |
1691 | return this.dataSource.getByInternalId(Ext.getDom(node).getAttribute('data-recordId'));\r | |
1692 | },\r | |
1693 | \r | |
1694 | \r | |
1695 | /**\r | |
1696 | * Returns true if the passed node is selected, else false.\r | |
1697 | * @param {HTMLElement/Number/Ext.data.Model} node The node, node index or record to check\r | |
1698 | * @return {Boolean} True if selected, else false\r | |
1699 | * @since 2.3.0\r | |
1700 | */\r | |
1701 | isSelected: function(node) {\r | |
1702 | var r = this.getRecord(node);\r | |
1703 | return this.getSelectionModel().isSelected(r);\r | |
1704 | },\r | |
1705 | \r | |
1706 | /**\r | |
1707 | * Selects a record instance by record instance or index.\r | |
1708 | * @param {Ext.data.Model[]/Number} records An array of records or an index\r | |
1709 | * @param {Boolean} keepExisting\r | |
1710 | * @param {Boolean} suppressEvent Set to false to not fire a select event\r | |
1711 | * @deprecated 4.0 Use {@link Ext.selection.Model#select} instead.\r | |
1712 | * @since 2.3.0\r | |
1713 | */\r | |
1714 | select: function(records, keepExisting, suppressEvent) {\r | |
1715 | this.getSelectionModel().select(records, keepExisting, suppressEvent);\r | |
1716 | },\r | |
1717 | \r | |
1718 | /**\r | |
1719 | * Deselects a record instance by record instance or index.\r | |
1720 | * @param {Ext.data.Model[]/Number} records An array of records or an index\r | |
1721 | * @param {Boolean} suppressEvent Set to false to not fire a deselect event\r | |
1722 | * @since 2.3.0\r | |
1723 | */\r | |
1724 | deselect: function(records, suppressEvent) {\r | |
1725 | this.getSelectionModel().deselect(records, suppressEvent);\r | |
1726 | },\r | |
1727 | \r | |
1728 | /**\r | |
1729 | * Gets a template node.\r | |
1730 | * @param {HTMLElement/String/Number/Ext.data.Model} nodeInfo An HTMLElement template node, index of a template node,\r | |
1731 | * the id of a template node or the record associated with the node.\r | |
1732 | * @return {HTMLElement} The node or null if it wasn't found\r | |
1733 | * @since 2.3.0\r | |
1734 | */\r | |
1735 | getNode: function(nodeInfo) {\r | |
1736 | var me = this,\r | |
1737 | out;\r | |
1738 | \r | |
1739 | if (me.rendered && (nodeInfo || nodeInfo === 0)) {\r | |
1740 | if (Ext.isString(nodeInfo)) {\r | |
1741 | // Id\r | |
1742 | out = document.getElementById(nodeInfo);\r | |
1743 | } else if (nodeInfo.isModel) {\r | |
1744 | // Record\r | |
1745 | out = me.getNodeByRecord(nodeInfo);\r | |
1746 | } else if (Ext.isNumber(nodeInfo)) {\r | |
1747 | // Index\r | |
1748 | out = me.all.elements[nodeInfo];\r | |
1749 | } else {\r | |
1750 | if (nodeInfo.target && nodeInfo.target.nodeType) {\r | |
1751 | // An event. Check that target is a node: <a target="_blank"> must pass unchanged\r | |
1752 | nodeInfo = nodeInfo.target;\r | |
1753 | }\r | |
1754 | out = Ext.fly(nodeInfo).findParent(me.itemSelector, me.getTargetEl()); // already an HTMLElement\r | |
1755 | }\r | |
1756 | }\r | |
1757 | return out || null;\r | |
1758 | },\r | |
1759 | \r | |
1760 | /**\r | |
1761 | * @private\r | |
1762 | */\r | |
1763 | getNodeByRecord: function(record) {\r | |
1764 | var index = this.store.indexOf(record);\r | |
1765 | return this.all.elements[index] || null;\r | |
1766 | },\r | |
1767 | \r | |
1768 | /**\r | |
1769 | * Gets a range nodes.\r | |
1770 | * @param {Number} start (optional) The index of the first node in the range\r | |
1771 | * @param {Number} end (optional) The index of the last node in the range\r | |
1772 | * @return {HTMLElement[]} An array of nodes\r | |
1773 | * @since 2.3.0\r | |
1774 | */\r | |
1775 | getNodes: function(start, end) {\r | |
1776 | var all = this.all;\r | |
1777 | \r | |
1778 | if (end !== undefined) {\r | |
1779 | end++;\r | |
1780 | }\r | |
1781 | return all.slice(start, end);\r | |
1782 | },\r | |
1783 | \r | |
1784 | /**\r | |
1785 | * Finds the index of the passed node.\r | |
1786 | * @param {HTMLElement/String/Number/Ext.data.Model} nodeInfo An HTMLElement template node, index of a template node, the id of a template node\r | |
1787 | * or a record associated with a node.\r | |
1788 | * @return {Number} The index of the node or -1\r | |
1789 | * @since 2.3.0\r | |
1790 | */\r | |
1791 | indexOf: function(node) {\r | |
1792 | node = this.getNode(node);\r | |
1793 | if (!node && node !== 0) {\r | |
1794 | return -1;\r | |
1795 | }\r | |
1796 | if (node.getAttribute('data-recordIndex')) {\r | |
1797 | return Number(node.getAttribute('data-recordIndex'));\r | |
1798 | }\r | |
1799 | return this.all.indexOf(node);\r | |
1800 | },\r | |
1801 | \r | |
1802 | onDestroy: function() {\r | |
1803 | var me = this,\r | |
1804 | count = me.updateSuspendCounter;\r | |
1805 | \r | |
1806 | me.all.clear();\r | |
1807 | me.emptyEl = null;\r | |
1808 | \r | |
1809 | me.callParent();\r | |
1810 | me.bindStore(null);\r | |
1811 | \r | |
1812 | me.store = me.dataSource = me.storeListeners = null;\r | |
1813 | \r | |
1814 | if (me.selModelRelayer) {\r | |
1815 | me.selModelRelayer.destroy();\r | |
1816 | me.selModelRelayer = null;\r | |
1817 | }\r | |
1818 | \r | |
1819 | Ext.destroy(me.navigationModel, me.selectionModel);\r | |
1820 | me.navigationModel = me.selectionModel = me.selModel = null;\r | |
1821 | \r | |
1822 | me.loadMask = null;\r | |
1823 | \r | |
1824 | // We have been destroyed during a begin/end update, which means we're\r | |
1825 | // suspending layouts, must forcibly do it here.\r | |
1826 | while (count--) {\r | |
1827 | Ext.resumeLayouts(true);\r | |
1828 | }\r | |
1829 | },\r | |
1830 | \r | |
1831 | // invoked by the selection model to maintain visual UI cues\r | |
1832 | onItemSelect: function(record) {\r | |
1833 | var node = this.getNode(record);\r | |
1834 | \r | |
1835 | if (node) {\r | |
1836 | Ext.fly(node).addCls(this.selectedItemCls);\r | |
1837 | }\r | |
1838 | },\r | |
1839 | \r | |
1840 | // invoked by the selection model to maintain visual UI cues\r | |
1841 | onItemDeselect: function(record) {\r | |
1842 | var node = this.getNode(record);\r | |
1843 | \r | |
1844 | if (node) {\r | |
1845 | Ext.fly(node).removeCls(this.selectedItemCls);\r | |
1846 | }\r | |
1847 | },\r | |
1848 | \r | |
1849 | getItemSelector: function() {\r | |
1850 | return this.itemSelector;\r | |
1851 | },\r | |
1852 | \r | |
1853 | /**\r | |
1854 | * Adds a CSS Class to a specific item.\r | |
1855 | * @param {HTMLElement/String/Number/Ext.data.Model} itemInfo An HTMLElement, index or instance of a model\r | |
1856 | * representing this item\r | |
1857 | * @param {String} cls\r | |
1858 | */\r | |
1859 | addItemCls: function(itemInfo, cls) {\r | |
1860 | var item = this.getNode(itemInfo);\r | |
1861 | if (item) {\r | |
1862 | Ext.fly(item).addCls(cls);\r | |
1863 | }\r | |
1864 | },\r | |
1865 | \r | |
1866 | /**\r | |
1867 | * Removes a CSS Class from a specific item.\r | |
1868 | * @param {HTMLElement/String/Number/Ext.data.Model} itemInfo An HTMLElement, index or instance of a model\r | |
1869 | * representing this item\r | |
1870 | * @param {String} cls\r | |
1871 | */\r | |
1872 | removeItemCls: function(itemInfo, cls) {\r | |
1873 | var item = this.getNode(itemInfo);\r | |
1874 | if (item) {\r | |
1875 | Ext.fly(item).removeCls(cls);\r | |
1876 | }\r | |
1877 | },\r | |
1878 | \r | |
1879 | setStore: function (newStore) {\r | |
1880 | // Here we want to override the config system setter because setting the store is a special case\r | |
1881 | // that the config system wasn't able to handle.\r | |
1882 | //\r | |
1883 | // For instance, because `bindStore` is the only API for both binding and unbinding a store, we\r | |
1884 | // couldn't unbind the old store using the config system because it would simply unbind the new\r | |
1885 | // store that the setter had just poked onto the instance:\r | |
1886 | //\r | |
1887 | // setStore -> intance.store = newStore\r | |
1888 | // updateStore -> view.unbind(null) (unbinds the newStore)\r | |
1889 | //\r | |
1890 | var me = this;\r | |
1891 | \r | |
1892 | if (me.store !== newStore) {\r | |
1893 | if (me.isConfiguring) {\r | |
1894 | me.store = newStore;\r | |
1895 | } else {\r | |
1896 | me.bindStore(newStore, /*initial*/ false);\r | |
1897 | }\r | |
1898 | }\r | |
1899 | },\r | |
1900 | \r | |
1901 | privates: {\r | |
1902 | toggleChildrenTabbability: function(enableTabbing) {\r | |
1903 | var focusEl = this.getTargetEl();\r | |
1904 | \r | |
1905 | if (enableTabbing) {\r | |
1906 | focusEl.restoreTabbableState(/* skipSelf = */ true);\r | |
1907 | }\r | |
1908 | else {\r | |
1909 | // Do NOT includeSaved\r | |
1910 | // Once an item has had tabbability saved, do not increment its save level\r | |
1911 | focusEl.saveTabbableState({\r | |
1912 | skipSelf: true,\r | |
1913 | includeSaved: false\r | |
1914 | });\r | |
1915 | }\r | |
1916 | },\r | |
1917 | \r | |
1918 | /**\r | |
1919 | * @private\r | |
1920 | * Called by refresh to collect the view item nodes.\r | |
1921 | */\r | |
1922 | collectNodes: function(targetEl) {\r | |
1923 | var all = this.all,\r | |
1924 | options = {\r | |
1925 | role: this.itemAriaRole\r | |
1926 | };\r | |
1927 | \r | |
1928 | all.fill(Ext.fly(targetEl).query(this.getItemSelector()), all.startIndex || 0);\r | |
1929 | \r | |
1930 | // Subclasses may set focusable to false (BoundList is not focusable)\r | |
1931 | if (this.focusable) {\r | |
1932 | options.tabindex = '-1';\r | |
1933 | }\r | |
1934 | \r | |
1935 | all.set(options);\r | |
1936 | },\r | |
1937 | \r | |
1938 | createMask: function(mask) {\r | |
1939 | var me = this,\r | |
1940 | maskStore = me.getStore(),\r | |
1941 | cfg;\r | |
1942 | \r | |
1943 | if (maskStore && !maskStore.isEmptyStore && !maskStore.loadsSynchronously()) {\r | |
1944 | cfg = {\r | |
1945 | target: me,\r | |
1946 | msg: me.loadingText,\r | |
1947 | useMsg: me.loadingUseMsg,\r | |
1948 | // The store gets bound in initComponent, so while\r | |
1949 | // rendering let's push on the store\r | |
1950 | store: maskStore\r | |
1951 | };\r | |
1952 | // Do not overwrite default msgCls if we do not have a loadingCls\r | |
1953 | if (me.loadingCls) {\r | |
1954 | cfg.msgCls = me.loadingCls;\r | |
1955 | }\r | |
1956 | // either a config object \r | |
1957 | if (Ext.isObject(mask)) {\r | |
1958 | cfg = Ext.apply(cfg, mask);\r | |
1959 | }\r | |
1960 | // Attach the LoadMask to a *Component* so that it can be sensitive to resizing during long loads.\r | |
1961 | // If this DataView is floating, then mask this DataView.\r | |
1962 | // Otherwise, mask its owning Container (or this, if there *is* no owning Container).\r | |
1963 | // LoadMask captures the element upon render.\r | |
1964 | me.loadMask = new Ext.LoadMask(cfg);\r | |
1965 | me.loadMask.on({\r | |
1966 | scope: me,\r | |
1967 | beforeshow: me.onMaskBeforeShow,\r | |
1968 | hide: me.onMaskHide\r | |
1969 | });\r | |
1970 | }\r | |
1971 | return me.loadMask;\r | |
1972 | },\r | |
1973 | \r | |
1974 | getOverflowEl: function() {\r | |
1975 | // The desired behavior here is just to inherit from the superclass. However,\r | |
1976 | // the superclass method calls this.getTargetEl, which sends us into an infinte\r | |
1977 | // loop because our getTargetEl may call getScrollerEl(), which calls getOverflowEl()\r | |
1978 | return Ext.Component.prototype.getTargetEl.call(this);\r | |
1979 | },\r | |
1980 | \r | |
1981 | getTargetEl: function() {\r | |
1982 | return this.touchScroll ? this.getScrollerEl() : this.callParent();\r | |
1983 | }\r | |
1984 | }\r | |
1985 | }, function() {\r | |
1986 | // all of this information is available directly\r | |
1987 | // from the SelectionModel itself, the only added methods\r | |
1988 | // to DataView regarding selection will perform some transformation/lookup\r | |
1989 | // between HTMLElement/Nodes to records and vice versa.\r | |
1990 | Ext.deprecate('extjs', '4.0', function() {\r | |
1991 | Ext.view.AbstractView.override({\r | |
1992 | /**\r | |
1993 | * @cfg {Boolean} [multiSelect=false]\r | |
1994 | * True to allow selection of more than one item at a time, false to allow selection of only a single item\r | |
1995 | * at a time or no selection at all, depending on the value of {@link #singleSelect}.\r | |
1996 | * @deprecated 4.0 Use {@link Ext.selection.Model#mode} 'MULTI' instead.\r | |
1997 | * @since 2.3.0\r | |
1998 | */\r | |
1999 | /**\r | |
2000 | * @cfg {Boolean} [singleSelect]\r | |
2001 | * Allows selection of exactly one item at a time. As this is the default selection mode anyway, this config\r | |
2002 | * is completely ignored.\r | |
2003 | * @removed 4.0 Use {@link Ext.selection.Model#mode} 'SINGLE' instead.\r | |
2004 | * @since 2.3.0\r | |
2005 | */\r | |
2006 | /**\r | |
2007 | * @cfg {Boolean} [simpleSelect=false]\r | |
2008 | * True to enable multiselection by clicking on multiple items without requiring the user to hold Shift or Ctrl,\r | |
2009 | * false to force the user to hold Ctrl or Shift to select more than on item.\r | |
2010 | * @deprecated 4.0 Use {@link Ext.selection.Model#mode} 'SIMPLE' instead.\r | |
2011 | * @since 2.3.0\r | |
2012 | */\r | |
2013 | \r | |
2014 | /**\r | |
2015 | * Gets the number of selected nodes.\r | |
2016 | * @return {Number} The node count\r | |
2017 | * @deprecated 4.0 Use {@link Ext.selection.Model#getCount} instead.\r | |
2018 | * @since 2.3.0\r | |
2019 | */\r | |
2020 | getSelectionCount: function(){\r | |
2021 | if (Ext.global.console) {\r | |
2022 | Ext.global.console.warn("DataView: getSelectionCount will be removed, please interact with the Ext.selection.DataViewModel");\r | |
2023 | }\r | |
2024 | return this.selModel.getSelection().length;\r | |
2025 | },\r | |
2026 | \r | |
2027 | /**\r | |
2028 | * Gets an array of the selected records\r | |
2029 | * @return {Ext.data.Model[]} An array of {@link Ext.data.Model} objects\r | |
2030 | * @deprecated 4.0 Use {@link Ext.selection.Model#getSelection} instead.\r | |
2031 | * @since 2.3.0\r | |
2032 | */\r | |
2033 | getSelectedRecords: function(){\r | |
2034 | if (Ext.global.console) {\r | |
2035 | Ext.global.console.warn("DataView: getSelectedRecords will be removed, please interact with the Ext.selection.DataViewModel");\r | |
2036 | }\r | |
2037 | return this.selModel.getSelection();\r | |
2038 | },\r | |
2039 | \r | |
2040 | // documented above\r | |
2041 | // @ignore\r | |
2042 | select: function(records, keepExisting, supressEvents) {\r | |
2043 | if (Ext.global.console) {\r | |
2044 | Ext.global.console.warn("DataView: select will be removed, please access select through a DataView's SelectionModel, ie: view.getSelectionModel().select()");\r | |
2045 | }\r | |
2046 | var sm = this.getSelectionModel();\r | |
2047 | return sm.select.apply(sm, arguments);\r | |
2048 | },\r | |
2049 | \r | |
2050 | /**\r | |
2051 | * Deselects all selected records.\r | |
2052 | * @deprecated 4.0 Use {@link Ext.selection.Model#deselectAll} instead.\r | |
2053 | * @since 2.3.0\r | |
2054 | */\r | |
2055 | clearSelections: function() {\r | |
2056 | if (Ext.global.console) {\r | |
2057 | Ext.global.console.warn("DataView: clearSelections will be removed, please access deselectAll through DataView's SelectionModel, ie: view.getSelectionModel().deselectAll()");\r | |
2058 | }\r | |
2059 | var sm = this.getSelectionModel();\r | |
2060 | return sm.deselectAll();\r | |
2061 | }\r | |
2062 | });\r | |
2063 | });\r | |
2064 | });\r | |
2065 | \r |