]> git.proxmox.com Git - extjs.git/blame - extjs/packages/core/src/list/Tree.js
add extjs 6.0.1 sources
[extjs.git] / extjs / packages / core / src / list / Tree.js
CommitLineData
6527f429
DM
1/**\r
2 * A lightweight component to display data in a simple tree structure.\r
3 * @since 6.0.0\r
4 */\r
5Ext.define('Ext.list.Tree', {\r
6 extend: 'Ext.Widget',\r
7 xtype: 'treelist',\r
8\r
9 requires: [\r
10 'Ext.list.RootTreeItem'\r
11 ],\r
12\r
13 expanderFirstCls: Ext.baseCSSPrefix + 'treelist-expander-first',\r
14 expanderOnlyCls: Ext.baseCSSPrefix + 'treelist-expander-only',\r
15 highlightPathCls: Ext.baseCSSPrefix + 'treelist-highlight-path',\r
16 microCls: Ext.baseCSSPrefix + 'treelist-micro',\r
17\r
18 uiPrefix: Ext.baseCSSPrefix + 'treelist-',\r
19\r
20 element: {\r
21 reference: 'element',\r
22 cls: Ext.baseCSSPrefix + 'treelist ' + Ext.baseCSSPrefix + 'unselectable',\r
23 listeners: {\r
24 click: 'onClick',\r
25 mouseenter: 'onMouseEnter',\r
26 mouseleave: 'onMouseLeave',\r
27 mouseover: 'onMouseOver'\r
28 },\r
29 children: [{\r
30 reference: 'toolsElement',\r
31 cls: Ext.baseCSSPrefix + 'treelist-toolstrip',\r
32 listeners: {\r
33 click: 'onToolStripClick',\r
34 mouseover: 'onToolStripMouseOver'\r
35 }\r
36 }]\r
37 },\r
38\r
39 cachedConfig: {\r
40 animation: {\r
41 duration: 500,\r
42 easing: 'ease'\r
43 },\r
44\r
45 expanderFirst: true,\r
46\r
47 /**\r
48 * @cfg {Boolean} expanderOnly\r
49 * `true` to expand only on the click of the expander element. Setting this to\r
50 * `false` will allow expansion on click of any part of the element.\r
51 */\r
52 expanderOnly: true\r
53 },\r
54\r
55 config: {\r
56 /**\r
57 * @cfg {Object} [defaults]\r
58 * The default configuration for the widgets created for tree items.\r
59 *\r
60 * @cfg {String} [defaults.xtype="treelistitem"]\r
61 * The type of item to create. By default, items are `{@link Ext.list.TreeItem treelistitem}`\r
62 * instances. This can be customized but this `xtype` must reference a class that\r
63 * ultimately derives from the `{@link Ext.list.AbstractTreeItem}` base class.\r
64 */\r
65 defaults: {\r
66 xtype: 'treelistitem'\r
67 },\r
68\r
69 highlightPath: null,\r
70\r
71 iconSize: null,\r
72\r
73 indent: null,\r
74\r
75 micro: null,\r
76\r
77 overItem: null,\r
78\r
79 /**\r
80 * @cfg {Ext.data.TreeModel} selection\r
81 * \r
82 * The current selected node.\r
83 */\r
84 selection: null, \r
85\r
86 /**\r
87 * @cfg {Boolean} selectOnExpander\r
88 * `true` to select the node when clicking the expander.\r
89 */\r
90 selectOnExpander: false,\r
91\r
92 /**\r
93 * @cfg {Boolean} [singleExpand=false]\r
94 * `true` if only 1 node per branch may be expanded.\r
95 */\r
96 singleExpand: null,\r
97\r
98 /**\r
99 * @cfg {String/Object/Ext.data.TreeStore}\r
100 * The data source to which this component is bound.\r
101 */\r
102 store: null,\r
103\r
104 ui: null\r
105 },\r
106\r
107 twoWayBindable: {\r
108 selection: 1\r
109 },\r
110\r
111 publishes: {\r
112 selection: 1\r
113 },\r
114\r
115 defaultBindProperty: 'store',\r
116\r
117 constructor: function(config) {\r
118 this.callParent([config]);\r
119 // Important to publish the value here, so the vm can\r
120 // will know our intial state.\r
121 this.publishState('selection', this.getSelection());\r
122 },\r
123\r
124 beforeLayout: function () {\r
125 // Only called in classic, ignored in modern\r
126 this.syncIconSize();\r
127 },\r
128\r
129 destroy: function () {\r
130 var me = this;\r
131\r
132 me.destroying = true; // normally set in callParent\r
133\r
134 me.unfloatAll(); \r
135 me.activeFloater = null;\r
136 me.setSelection(null);\r
137 me.setStore(null);\r
138 me.callParent();\r
139 },\r
140\r
141 updateOverItem: function (over, wasOver) {\r
142 var map = {},\r
143 state = 2,\r
144 c, node;\r
145\r
146 // Walk up the node hierarchy starting at the "over" item and set their "over"\r
147 // config appropriately (2 when over that row, 1 when over a descendant).\r
148 //\r
149 for (c = over; c; c = this.getItem(node.parentNode)) {\r
150 node = c.getNode();\r
151 map[node.internalId] = true;\r
152\r
153 c.setOver(state);\r
154\r
155 state = 1;\r
156 }\r
157\r
158 if (wasOver) {\r
159 // If we wasOver something else previously, walk up that node hierarchy and\r
160 // set their "over" to 0... until we encounter some node that we are still\r
161 // "over" (as determined in previous loop).\r
162 //\r
163 for (c = wasOver; c; c = this.getItem(node.parentNode)) {\r
164 node = c.getNode();\r
165 if (map[node.internalId]) {\r
166 break;\r
167 }\r
168\r
169 c.setOver(0);\r
170 }\r
171 }\r
172 },\r
173\r
174 applySelection: function(selection, oldSelection) {\r
175 var store = this.getStore();\r
176 if (!store) {\r
177 selection = null;\r
178 }\r
179 if (selection && selection.get('selectable') === false) {\r
180 selection = oldSelection;\r
181 }\r
182 return selection;\r
183 },\r
184\r
185 updateSelection: function(selection, oldSelection) {\r
186 var me = this,\r
187 item;\r
188\r
189 if (!me.destroying) {\r
190 // getItem has guards around null, so we don't\r
191 // need to check for oldSelection/selection here\r
192 item = me.getItem(oldSelection);\r
193 if (item) {\r
194 item.setSelected(false);\r
195 }\r
196\r
197 item = me.getItem(selection);\r
198 if (item) {\r
199 item.setSelected(true);\r
200 }\r
201 me.fireEvent('selectionchange', me, selection);\r
202 }\r
203 },\r
204\r
205 applyStore: function (store) {\r
206 return store && Ext.StoreManager.lookup(store, 'tree');\r
207 },\r
208\r
209 updateStore: function (store, oldStore) {\r
210 var me = this,\r
211 root;\r
212\r
213 if (oldStore) {\r
214 if (oldStore.getAutoDestroy()) {\r
215 oldStore.destroy();\r
216 } else {\r
217 me.storeListeners.destroy();\r
218 }\r
219 me.removeRoot();\r
220 me.storeListeners = null;\r
221 }\r
222\r
223 if (store) {\r
224 me.storeListeners = store.on({\r
225 destroyable: true,\r
226 scope: me,\r
227 nodeappend: me.onNodeAppend,\r
228 nodecollapse: me.onNodeCollapse,\r
229 nodeexpand: me.onNodeExpand,\r
230 nodeinsert: me.onNodeInsert,\r
231 noderemove: me.onNodeRemove,\r
232 rootchange: me.onRootChange,\r
233 update: me.onNodeUpdate\r
234 });\r
235 \r
236 root = store.getRoot();\r
237 if (root) {\r
238 me.createRootItem(root);\r
239 }\r
240 }\r
241\r
242 if (!me.destroying) {\r
243 me.updateLayout();\r
244 }\r
245 },\r
246\r
247 updateExpanderFirst: function (expanderFirst) {\r
248 this.element.toggleCls(this.expanderFirstCls, expanderFirst);\r
249 },\r
250\r
251 updateExpanderOnly: function (value) {\r
252 this.element.toggleCls(this.expanderOnlyCls, !value);\r
253 },\r
254\r
255 updateHighlightPath: function (updatePath) {\r
256 this.element.toggleCls(this.highlightPathCls, updatePath);\r
257 },\r
258\r
259 updateMicro: function (micro) {\r
260 var me = this;\r
261\r
262 if (!micro) {\r
263 me.unfloatAll();\r
264 me.activeFloater = null;\r
265 }\r
266\r
267 me.element.toggleCls(me.microCls, micro);\r
268 },\r
269\r
270 updateUi: function (ui, oldValue) {\r
271 var el = this.element,\r
272 uiPrefix = this.uiPrefix;\r
273\r
274 if (oldValue) {\r
275 el.removeCls(uiPrefix + oldValue);\r
276 }\r
277 if (ui) {\r
278 el.addCls(uiPrefix + ui);\r
279 }\r
280\r
281 // Ensure that the cached iconSize is read from the style.\r
282 delete this.iconSize;\r
283 this.syncIconSize();\r
284 },\r
285\r
286 /**\r
287 * Get a child {@link Ext.list.AbstractTreeItem item} by node.\r
288 * @param {Ext.data.TreeModel} node The node.\r
289 * @return {Ext.list.AbstractTreeItem} The item. `null` if not found.\r
290 */\r
291 getItem: function (node) {\r
292 var map = this.itemMap,\r
293 ret;\r
294\r
295 if (node && map) {\r
296 ret = map[node.internalId];\r
297 }\r
298\r
299 return ret || null;\r
300 },\r
301\r
302 /**\r
303 * This method is called to populate and return a config object for new nodes. This\r
304 * can be overridden by derived classes to manipulate properties or `xtype` of the\r
305 * returned object. Upon return, the object is passed to `{@link Ext#create}` and the\r
306 * reference is stored as part of this tree.\r
307 *\r
308 * The base class implementation will apply any configured `{@link #defaults}` to the\r
309 * object it returns.\r
310 *\r
311 * @param {Ext.data.TreeModel} node The node backing the item.\r
312 * @param {Ext.list.AbstractTreeItem} parent The parent item. This is never `null` but\r
313 * may be an instance of `{@link Ext.list.RootTreeItem}`.\r
314 * @return {Object} The config object to pass to `{@link Ext#create}` for the item.\r
315 * @template\r
316 */\r
317 getItemConfig: function (node, parent) {\r
318 return Ext.apply({\r
319 parentItem: parent.isRootListItem ? null : parent,\r
320 owner: this,\r
321 node: node,\r
322 indent: this.getIndent()\r
323 }, this.getDefaults());\r
324 },\r
325\r
326 privates: {\r
327 checkForOutsideClick: function(e) {\r
328 var floater = this.activeFloater;\r
329 if (!floater.element.contains(e.target)) {\r
330 this.unfloatAll();\r
331 }\r
332 },\r
333\r
334 collapsingForExpand: false,\r
335\r
336 /**\r
337 * Create a new list item.\r
338 * @param {Ext.data.TreeModel} node The node backing the item.\r
339 * @param {Ext.list.AbstractTreeItem} parent The parent item.\r
340 * @return {Ext.list.AbstractTreeItem} The list item.\r
341 *\r
342 * @private\r
343 */\r
344 createItem: function (node, parent) {\r
345 var item = Ext.create(this.getItemConfig(node, parent)),\r
346 toolEl;\r
347\r
348 if (parent.isRootListItem) {\r
349 toolEl = item.getToolElement();\r
350 if (toolEl) {\r
351 this.toolsElement.appendChild(toolEl);\r
352 toolEl.dom.setAttribute('data-recordId', node.internalId);\r
353 toolEl.isTool = true;\r
354 }\r
355 }\r
356\r
357 return (this.itemMap[node.internalId] = item); // <== assignment\r
358 },\r
359\r
360 /**\r
361 * Create a root item for this list.\r
362 * @param {Ext.data.TreeModel} root The root node.\r
363 *\r
364 * @private\r
365 */\r
366 createRootItem: function (root) {\r
367 var me = this,\r
368 item;\r
369\r
370 me.itemMap = {};\r
371 me.rootItem = item = new Ext.list.RootTreeItem({\r
372 indent: me.getIndent(),\r
373 node: root,\r
374 owner: me\r
375 });\r
376\r
377 me.element.appendChild(item.element);\r
378\r
379 me.itemMap[root.internalId] = item;\r
380 },\r
381\r
382 floatItem: function(item, byHover) {\r
383 var me = this,\r
384 floater;\r
385\r
386 if (item.getFloated()) {\r
387 return;\r
388 }\r
389\r
390 me.unfloatAll();\r
391\r
392 me.activeFloater = floater = item;\r
393 me.floatedByHover = byHover;\r
394\r
395 item.setFloated(true);\r
396\r
397 if (byHover) {\r
398 item.getToolElement().on('mouseleave', me.checkForMouseLeave, me);\r
399 floater.element.on('mouseleave', me.checkForMouseLeave, me);\r
400 } else {\r
401 Ext.on('mousedown', me.checkForOutsideClick, me);\r
402 }\r
403 },\r
404\r
405 /**\r
406 * Handles when this element is clicked.\r
407 * @param {Ext.event.Event} e The event.\r
408 *\r
409 * @private\r
410 */\r
411 onClick: function (e) {\r
412 var item = e.getTarget('[data-recordId]'),\r
413 id;\r
414\r
415 if (item) {\r
416 id = item.getAttribute('data-recordId');\r
417 item = this.itemMap[id];\r
418 if (item) {\r
419 item.onClick(e);\r
420 }\r
421 }\r
422 },\r
423\r
424 onMouseEnter: function (e) {\r
425 this.onMouseOver(e);\r
426 },\r
427\r
428 onMouseLeave: function () {\r
429 this.setOverItem(null);\r
430 },\r
431\r
432 onMouseOver: function (e) {\r
433 var comp = Ext.Component.fromElement(e.getTarget());\r
434\r
435 this.setOverItem(comp && comp.isTreeListItem && comp);\r
436 },\r
437\r
438 checkForMouseLeave: function (e) {\r
439 var floater = this.activeFloater,\r
440 relatedTarget = e.getRelatedTarget();\r
441\r
442 if (floater) {\r
443 if (relatedTarget !== floater.getToolElement().dom && !floater.element.contains(relatedTarget)) {\r
444 this.unfloatAll();\r
445 }\r
446 }\r
447 },\r
448\r
449 /**\r
450 * Handles a node being appended to a parent.\r
451 * @param {Ext.data.TreeModel} parentNode The parent node.\r
452 * @param {Ext.data.TreeModel} node The appended node.\r
453 *\r
454 * @private\r
455 */\r
456 onNodeAppend: function (parentNode, node) {\r
457 // If it's a root we'll handle it on rootchange\r
458 if (parentNode) {\r
459 var item = this.itemMap[parentNode.internalId];\r
460\r
461 if (item) {\r
462 item.nodeInsert(node, null);\r
463 }\r
464 }\r
465 },\r
466\r
467 /**\r
468 * Handles when a node collapses.\r
469 * @param {Ext.data.TreeModel} node The node.\r
470 *\r
471 * @private\r
472 */\r
473 onNodeCollapse: function (node) {\r
474 var item = this.itemMap[node.internalId];\r
475\r
476 if (item) {\r
477 item.nodeCollapse(node, this.collapsingForExpand);\r
478 }\r
479 },\r
480\r
481 /**\r
482 * Handles when a node expands.\r
483 * @param {Ext.data.TreeModel} node The node.\r
484 *\r
485 * @private\r
486 */\r
487 onNodeExpand: function (node) {\r
488 var me = this,\r
489 item = me.itemMap[node.internalId],\r
490 childNodes, len, i, parentNode, child;\r
491\r
492 if (item) {\r
493 if (!item.isRootItem && me.getSingleExpand()) {\r
494 me.collapsingForExpand = true;\r
495 parentNode = (item.getParentItem() || me.rootItem).getNode();\r
496 childNodes = parentNode.childNodes;\r
497 for (i = 0, len = childNodes.length; i < len; ++i) {\r
498 child = childNodes[i];\r
499 if (child !== node) {\r
500 child.collapse();\r
501 }\r
502 }\r
503 me.collapsing = false;\r
504 }\r
505\r
506 item.nodeExpand(node);\r
507 }\r
508 },\r
509\r
510 /**\r
511 * Handles a node being inserted into a parent.\r
512 * @param {Ext.data.TreeModel} parentNode The parent node.\r
513 * @param {Ext.data.TreeModel} node The inserted node.\r
514 * @param {Ext.data.TreeModel} refNode The node this was inserted before.\r
515 *\r
516 * @private\r
517 */\r
518 onNodeInsert: function (parentNode, node, refNode) {\r
519 var item = this.itemMap[parentNode.internalId];\r
520\r
521 if (item) {\r
522 item.nodeInsert(node, refNode);\r
523 }\r
524 },\r
525\r
526 /**\r
527 * Handles a node being removed from a parent.\r
528 * @param {Ext.data.TreeModel} parentNode The parent node.\r
529 * @param {Ext.data.TreeModel} node The removed node.\r
530 * @param {Boolean} isMove `true` if this node is moving inside the tree.\r
531 *\r
532 * @private\r
533 */\r
534 onNodeRemove: function (parentNode, node, isMove) {\r
535 // If it's a move we don't need to do anything, we won't process it\r
536 // as a removal, the addition will handle it all.\r
537 // Also if the node being removed is the root we'll handle it in rootchange\r
538 if (parentNode && !isMove) {\r
539 var item = this.itemMap[parentNode.internalId];\r
540\r
541 if (item) {\r
542 item.nodeRemove(node);\r
543 }\r
544 }\r
545 }, \r
546\r
547 /**\r
548 * Handles when a node updates.\r
549 * @param {Ext.data.TreeStore} store The store.\r
550 * @param {Ext.data.TreeModel} node The node.\r
551 * @param {String} type The update type.\r
552 * @param {String[]} modifiedFieldNames The modified field names, if known.\r
553 *\r
554 * @private\r
555 */\r
556 onNodeUpdate: function (store, node, type, modifiedFieldNames) {\r
557 var item = this.itemMap[node.internalId];\r
558\r
559 if (item) {\r
560 item.nodeUpdate(node, modifiedFieldNames);\r
561 }\r
562 },\r
563\r
564 /**\r
565 * Handles when the root node in the tree changes.\r
566 * @param {Ext.data.TreeModel} root The root.\r
567 *\r
568 * @private\r
569 */\r
570 onRootChange: function (root) {\r
571 this.removeRoot();\r
572\r
573 if (root) {\r
574 this.createRootItem(root);\r
575 }\r
576\r
577 this.updateLayout();\r
578 },\r
579\r
580 /**\r
581 * Removes a list item.\r
582 * @param {Ext.data.TreeModel} node The node backing the item.\r
583 *\r
584 * @private\r
585 */\r
586 removeItem: function (node) {\r
587 var map = this.itemMap;\r
588\r
589 if (map) {\r
590 delete map[node.internalId];\r
591 }\r
592 },\r
593\r
594 removeRoot: function () {\r
595 var me = this,\r
596 rootItem = me.rootItem;\r
597\r
598 if (rootItem) {\r
599 me.element.removeChild(rootItem.element);\r
600 me.rootItem = me.itemMap = Ext.destroy(rootItem);\r
601 }\r
602 },\r
603\r
604 /**\r
605 * Handles when the toolstrip has a click.\r
606 * @param {Ext.event.Event} e The event.\r
607 *\r
608 * @private\r
609 */\r
610 onToolStripClick: function(e) {\r
611 var item = e.getTarget('[data-recordId]'),\r
612 id;\r
613\r
614 if (item) {\r
615 id = item.getAttribute('data-recordId');\r
616 item = this.itemMap[id];\r
617 if (item) {\r
618 if (item === this.activeFloater) {\r
619 this.unfloatAll();\r
620 } else {\r
621 this.floatItem(item, false);\r
622 }\r
623 }\r
624 }\r
625 },\r
626\r
627 /**\r
628 * Handles when the toolstrip has a mouseover.\r
629 * @param {Ext.event.Event} e The event.\r
630 *\r
631 * @private\r
632 */\r
633 onToolStripMouseOver: function(e) {\r
634 var item = e.getTarget('[data-recordId]'),\r
635 id;\r
636\r
637 if (item) {\r
638 id = item.getAttribute('data-recordId');\r
639 item = this.itemMap[id];\r
640 if (item) {\r
641 this.floatItem(item, true);\r
642 }\r
643 }\r
644 },\r
645\r
646 syncIconSize: function() {\r
647 var me = this,\r
648 size = me.iconSize ||\r
649 (me.iconSize = parseInt(me.element.getStyle('background-position'), 10));\r
650\r
651 me.setIconSize(size);\r
652 },\r
653\r
654 unfloatAll: function () {\r
655 var me = this,\r
656 floater = me.activeFloater;\r
657\r
658 if (floater) {\r
659 floater.setFloated(false);\r
660 me.activeFloater = null;\r
661\r
662 if (me.floatedByHover) {\r
663 floater.element.un('mouseleave', me.checkForMouseLeave, me);\r
664 } else {\r
665 Ext.un('mousedown', me.checkForOutsideClick, me);\r
666 }\r
667 }\r
668 },\r
669\r
670 defaultIconSize: 22,\r
671\r
672 updateIconSize: function (value) {\r
673 this.setIndent(value || this.defaultIconSize);\r
674 },\r
675\r
676 updateIndent: function (value) {\r
677 var rootItem = this.rootItem;\r
678\r
679 if (rootItem) {\r
680 rootItem.setIndent(value);\r
681 }\r
682 }\r
683 }\r
684});\r