/** * Used as a view by {@link Ext.tree.Panel TreePanel}. */ Ext.define('Ext.tree.View', { extend: 'Ext.view.Table', alias: 'widget.treeview', loadingCls: Ext.baseCSSPrefix + 'grid-tree-loading', expandedCls: Ext.baseCSSPrefix + 'grid-tree-node-expanded', expanderSelector: '.' + Ext.baseCSSPrefix + 'tree-expander', checkboxSelector: '.' + Ext.baseCSSPrefix + 'tree-checkbox', expanderIconOverCls: Ext.baseCSSPrefix + 'tree-expander-over', // Class to add to the node wrap element used to hold nodes when a parent is being // collapsed or expanded. During the animation, UI interaction is forbidden by testing // for an ancestor node with this class. nodeAnimWrapCls: Ext.baseCSSPrefix + 'tree-animator-wrap', blockRefresh: true, /** * @cfg {Boolean} rootVisible * False to hide the root node. */ rootVisible: true, /** * @cfg {Boolean} animate * True to enable animated expand/collapse (defaults to the value of {@link Ext#enableFx Ext.enableFx}) */ expandDuration: 250, collapseDuration: 250, toggleOnDblClick: true, initComponent: function() { var me = this; if (me.initialConfig.animate === undefined) { me.animate = Ext.enableFx; } me.store = Ext.create('Ext.data.NodeStore', { recursive: true, rootVisible: me.rootVisible, listeners: { beforeexpand: me.onBeforeExpand, expand: me.onExpand, beforecollapse: me.onBeforeCollapse, collapse: me.onCollapse, scope: me } }); if (me.node) { me.setRootNode(me.node); } me.animQueue = {}; me.callParent(arguments); }, processUIEvent: function(e) { // If the clicked node is part of an animation, ignore the click. // This is because during a collapse animation, the associated Records // will already have been removed from the Store, and the event is not processable. if (e.getTarget('.' + this.nodeAnimWrapCls, this.el)) { return false; } return this.callParent(arguments); }, onClear: function(){ this.store.removeAll(); }, setRootNode: function(node) { var me = this; me.store.setNode(node); me.node = node; if (!me.rootVisible) { node.expand(); } }, onRender: function() { var me = this, el; me.callParent(arguments); el = me.el; el.on({ scope: me, delegate: me.expanderSelector, mouseover: me.onExpanderMouseOver, mouseout: me.onExpanderMouseOut }); el.on({ scope: me, delegate: me.checkboxSelector, click: me.onCheckboxChange }); }, onCheckboxChange: function(e, t) { var me = this, item = e.getTarget(me.getItemSelector(), me.getTargetEl()); if (item) { me.onCheckChange(me.getRecord(item)); } }, onCheckChange: function(record){ var checked = record.get('checked'); if (Ext.isBoolean(checked)) { checked = !checked; record.set('checked', checked); this.fireEvent('checkchange', record, checked); } }, getChecked: function() { var checked = []; this.node.cascadeBy(function(rec){ if (rec.get('checked')) { checked.push(rec); } }); return checked; }, isItemChecked: function(rec){ return rec.get('checked'); }, createAnimWrap: function(record, index) { var thHtml = '', headerCt = this.panel.headerCt, headers = headerCt.getGridColumns(), i = 0, len = headers.length, item, node = this.getNode(record), tmpEl, nodeEl; for (; i < len; i++) { item = headers[i]; thHtml += '<th style="width: ' + (item.hidden ? 0 : item.getDesiredWidth()) + 'px; height: 0px;"></th>'; } nodeEl = Ext.get(node); tmpEl = nodeEl.insertSibling({ tag: 'tr', html: [ '<td colspan="' + headerCt.getColumnCount() + '">', '<div class="' + this.nodeAnimWrapCls + '">', '<table class="' + Ext.baseCSSPrefix + 'grid-table" style="width: ' + headerCt.getFullWidth() + 'px;"><tbody>', thHtml, '</tbody></table>', '</div>', '</td>' ].join('') }, 'after'); return { record: record, node: node, el: tmpEl, expanding: false, collapsing: false, animating: false, animateEl: tmpEl.down('div'), targetEl: tmpEl.down('tbody') }; }, getAnimWrap: function(parent) { if (!this.animate) { return null; } // We are checking to see which parent is having the animation wrap while (parent) { if (parent.animWrap) { return parent.animWrap; } parent = parent.parentNode; } return null; }, doAdd: function(nodes, records, index) { // If we are adding records which have a parent that is currently expanding // lets add them to the animation wrap var me = this, record = records[0], parent = record.parentNode, a = me.all.elements, relativeIndex = 0, animWrap = me.getAnimWrap(parent), targetEl, children, len; if (!animWrap || !animWrap.expanding) { me.resetScrollers(); return me.callParent(arguments); } // We need the parent that has the animWrap, not the nodes parent parent = animWrap.record; // If there is an anim wrap we do our special magic logic targetEl = animWrap.targetEl; children = targetEl.dom.childNodes; // We subtract 1 from the childrens length because we have a tr in there with the th'es len = children.length - 1; // The relative index is the index in the full flat collection minus the index of the wraps parent relativeIndex = index - me.indexOf(parent) - 1; // If we are adding records to the wrap that have a higher relative index then there are currently children // it means we have to append the nodes to the wrap if (!len || relativeIndex >= len) { targetEl.appendChild(nodes); } // If there are already more children then the relative index it means we are adding child nodes of // some expanded node in the anim wrap. In this case we have to insert the nodes in the right location else { // +1 because of the tr with th'es that is already there Ext.fly(children[relativeIndex + 1]).insertSibling(nodes, 'before', true); } // We also have to update the CompositeElementLite collection of the DataView Ext.Array.insert(a, index, nodes); // If we were in an animation we need to now change the animation // because the targetEl just got higher. if (animWrap.isAnimating) { me.onExpand(parent); } }, beginBulkUpdate: function(){ this.bulkUpdate = true; this.ownerCt.changingScrollbars = true; }, endBulkUpdate: function(){ var me = this, ownerCt = me.ownerCt; me.bulkUpdate = false; me.ownerCt.changingScrollbars = true; me.resetScrollers(); }, onRemove : function(ds, record, index) { var me = this, bulk = me.bulkUpdate; me.doRemove(record, index); if (!bulk) { me.updateIndexes(index); } if (me.store.getCount() === 0){ me.refresh(); } if (!bulk) { me.fireEvent('itemremove', record, index); } }, doRemove: function(record, index) { // If we are adding records which have a parent that is currently expanding // lets add them to the animation wrap var me = this, parent = record.parentNode, all = me.all, animWrap = me.getAnimWrap(record), node = all.item(index).dom; if (!animWrap || !animWrap.collapsing) { me.resetScrollers(); return me.callParent(arguments); } animWrap.targetEl.appendChild(node); all.removeElement(index); }, onBeforeExpand: function(parent, records, index) { var me = this, animWrap; if (!me.rendered || !me.animate) { return; } if (me.getNode(parent)) { animWrap = me.getAnimWrap(parent); if (!animWrap) { animWrap = parent.animWrap = me.createAnimWrap(parent); animWrap.animateEl.setHeight(0); } else if (animWrap.collapsing) { // If we expand this node while it is still expanding then we // have to remove the nodes from the animWrap. animWrap.targetEl.select(me.itemSelector).remove(); } animWrap.expanding = true; animWrap.collapsing = false; } }, onExpand: function(parent) { var me = this, queue = me.animQueue, id = parent.getId(), animWrap, animateEl, targetEl, queueItem; if (me.singleExpand) { me.ensureSingleExpand(parent); } animWrap = me.getAnimWrap(parent); if (!animWrap) { me.resetScrollers(); return; } animateEl = animWrap.animateEl; targetEl = animWrap.targetEl; animateEl.stopAnimation(); // @TODO: we are setting it to 1 because quirks mode on IE seems to have issues with 0 queue[id] = true; animateEl.slideIn('t', { duration: me.expandDuration, listeners: { scope: me, lastframe: function() { // Move all the nodes out of the anim wrap to their proper location animWrap.el.insertSibling(targetEl.query(me.itemSelector), 'before'); animWrap.el.remove(); me.resetScrollers(); delete animWrap.record.animWrap; delete queue[id]; } } }); animWrap.isAnimating = true; }, resetScrollers: function(){ if (!this.bulkUpdate) { var panel = this.panel; panel.determineScrollbars(); panel.invalidateScroller(); } }, onBeforeCollapse: function(parent, records, index) { var me = this, animWrap; if (!me.rendered || !me.animate) { return; } if (me.getNode(parent)) { animWrap = me.getAnimWrap(parent); if (!animWrap) { animWrap = parent.animWrap = me.createAnimWrap(parent, index); } else if (animWrap.expanding) { // If we collapse this node while it is still expanding then we // have to remove the nodes from the animWrap. animWrap.targetEl.select(this.itemSelector).remove(); } animWrap.expanding = false; animWrap.collapsing = true; } }, onCollapse: function(parent) { var me = this, queue = me.animQueue, id = parent.getId(), animWrap = me.getAnimWrap(parent), animateEl, targetEl; if (!animWrap) { me.resetScrollers(); return; } animateEl = animWrap.animateEl; targetEl = animWrap.targetEl; queue[id] = true; // @TODO: we are setting it to 1 because quirks mode on IE seems to have issues with 0 animateEl.stopAnimation(); animateEl.slideOut('t', { duration: me.collapseDuration, listeners: { scope: me, lastframe: function() { animWrap.el.remove(); delete animWrap.record.animWrap; me.resetScrollers(); delete queue[id]; } } }); animWrap.isAnimating = true; }, /** * Checks if a node is currently undergoing animation * @private * @param {Ext.data.Model} node The node * @return {Boolean} True if the node is animating */ isAnimating: function(node) { return !!this.animQueue[node.getId()]; }, collectData: function(records) { var data = this.callParent(arguments), rows = data.rows, len = rows.length, i = 0, row, record; for (; i < len; i++) { row = rows[i]; record = records[i]; if (record.get('qtip')) { row.rowAttr = 'data-qtip="' + record.get('qtip') + '"'; if (record.get('qtitle')) { row.rowAttr += ' ' + 'data-qtitle="' + record.get('qtitle') + '"'; } } if (record.isExpanded()) { row.rowCls = (row.rowCls || '') + ' ' + this.expandedCls; } if (record.isLoading()) { row.rowCls = (row.rowCls || '') + ' ' + this.loadingCls; } } return data; }, /** * Expands a record that is loaded in the view. * @param {Ext.data.Model} record The record to expand * @param {Boolean} deep (optional) True to expand nodes all the way down the tree hierarchy. * @param {Function} callback (optional) The function to run after the expand is completed * @param {Object} scope (optional) The scope of the callback function. */ expand: function(record, deep, callback, scope) { return record.expand(deep, callback, scope); }, /** * Collapses a record that is loaded in the view. * @param {Ext.data.Model} record The record to collapse * @param {Boolean} deep (optional) True to collapse nodes all the way up the tree hierarchy. * @param {Function} callback (optional) The function to run after the collapse is completed * @param {Object} scope (optional) The scope of the callback function. */ collapse: function(record, deep, callback, scope) { return record.collapse(deep, callback, scope); }, /** * Toggles a record between expanded and collapsed. * @param {Ext.data.Model} recordInstance */ toggle: function(record) { this[record.isExpanded() ? 'collapse' : 'expand'](record); }, onItemDblClick: function(record, item, index) { this.callParent(arguments); if (this.toggleOnDblClick) { this.toggle(record); } }, onBeforeItemMouseDown: function(record, item, index, e) { if (e.getTarget(this.expanderSelector, item)) { return false; } return this.callParent(arguments); }, onItemClick: function(record, item, index, e) { if (e.getTarget(this.expanderSelector, item)) { this.toggle(record); return false; } return this.callParent(arguments); }, onExpanderMouseOver: function(e, t) { e.getTarget(this.cellSelector, 10, true).addCls(this.expanderIconOverCls); }, onExpanderMouseOut: function(e, t) { e.getTarget(this.cellSelector, 10, true).removeCls(this.expanderIconOverCls); }, /** * Gets the base TreeStore from the bound TreePanel. */ getTreeStore: function() { return this.panel.store; }, ensureSingleExpand: function(node) { var parent = node.parentNode; if (parent) { parent.eachChild(function(child) { if (child !== node && child.isExpanded()) { child.collapse(); } }); } } });