/**
* Used as a view by {@link Ext.tree.Panel TreePanel}.
*/
Ext.define('Ext.tree.View', {
extend: 'Ext.view.Table',
alias: 'widget.treeview',
requires: [
'Ext.data.NodeStore'
],
loadingCls: Ext.baseCSSPrefix + 'grid-tree-loading',
expandedCls: Ext.baseCSSPrefix + 'grid-tree-node-expanded',
leafCls: Ext.baseCSSPrefix + 'grid-tree-node-leaf',
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
* @inheritdoc
*/
loadMask: false,
/**
* @cfg {Boolean} rootVisible
* False to hide the root node.
*/
rootVisible: true,
/**
* @cfg {Boolean} deferInitialRefresh
* Must be false for Tree Views because the root node must be rendered in order to be updated with its child nodes.
*/
deferInitialRefresh: false,
/**
* @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,
stripeRows: false,
// fields that will trigger a change in the ui that aren't likely to be bound to a column
uiFields: ['expanded', 'loaded', 'checked', 'expandable', 'leaf', 'icon', 'iconCls', 'loading', 'qtip', 'qtitle'],
initComponent: function() {
var me = this,
treeStore = me.panel.getStore();
if (me.initialConfig.animate === undefined) {
me.animate = Ext.enableFx;
}
me.store = new Ext.data.NodeStore({
treeStore: treeStore,
recursive: true,
rootVisible: me.rootVisible,
listeners: {
beforeexpand: me.onBeforeExpand,
expand: me.onExpand,
beforecollapse: me.onBeforeCollapse,
collapse: me.onCollapse,
write: me.onStoreWrite,
datachanged: me.onStoreDataChanged,
scope: me
}
});
if (me.node) {
me.setRootNode(me.node);
}
me.animQueue = {};
me.animWraps = {};
me.addEvents(
/**
* @event afteritemexpand
* Fires after an item has been visually expanded and is visible in the tree.
* @param {Ext.data.NodeInterface} node The node that was expanded
* @param {Number} index The index of the node
* @param {HTMLElement} item The HTML element for the node that was expanded
*/
'afteritemexpand',
/**
* @event afteritemcollapse
* Fires after an item has been visually collapsed and is no longer visible in the tree.
* @param {Ext.data.NodeInterface} node The node that was collapsed
* @param {Number} index The index of the node
* @param {HTMLElement} item The HTML element for the node that was collapsed
*/
'afteritemcollapse'
);
me.callParent(arguments);
me.on({
element: 'el',
scope: me,
delegate: me.expanderSelector,
mouseover: me.onExpanderMouseOver,
mouseout: me.onExpanderMouseOut
});
me.on({
element: 'el',
scope: me,
delegate: me.checkboxSelector,
click: me.onCheckboxChange
});
},
getMaskStore: function(){
return this.panel.getStore();
},
afterComponentLayout: function(){
this.callParent(arguments);
var stretcher = this.stretcher;
if (stretcher) {
stretcher.setWidth((this.getWidth() - Ext.getScrollbarSize().width));
}
},
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;
},
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');
},
/**
* @private
*/
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 += '
| ';
}
nodeEl = Ext.get(node);
tmpEl = nodeEl.insertSibling({
tag: 'tr',
html: [
'',
'',
' | '
].join('')
}, 'after');
return {
record: record,
node: node,
el: tmpEl,
expanding: false,
collapsing: false,
animating: false,
animateEl: tmpEl.down('div'),
targetEl: tmpEl.down('tbody')
};
},
/**
* @private
* Returns the animation wrapper element for the specified parent node, used to wrap the child nodes as
* they slide up or down during expand/collapse.
*
* @param parent The parent node to be expanded or collapsed
*
* @param [bubble=true] If the passed parent node does not already have a wrap element created, by default
* this function will bubble up to each parent node looking for a valid wrap element to reuse, returning
* the first one it finds. This is the appropriate behavior, e.g., for the collapse direction, so that the
* entire expanded set of branch nodes can collapse as a single unit.
*
* However for expanding each parent node should instead always create its own animation wrap if one
* doesn't exist, so that its children can expand independently of any other nodes -- this is crucial
* when executing the "expand all" behavior. If multiple nodes attempt to reuse the same ancestor wrap
* element concurrently during expansion it will lead to problems as the first animation to complete will
* delete the wrap el out from under other running animations. For that reason, when expanding you should
* always pass `bubble: false` to be on the safe side.
*
* If the passed parent has no wrap (or there is no valid ancestor wrap after bubbling), this function
* will return null and the calling code should then call {@link #createAnimWrap} if needed.
*
* @return {Ext.Element} The wrapping element as created in {@link #createAnimWrap}, or null
*/
getAnimWrap: function(parent, bubble) {
if (!this.animate) {
return null;
}
var wraps = this.animWraps,
wrap = wraps[parent.internalId];
if (bubble !== false) {
while (!wrap && parent) {
parent = parent.parentNode;
if (parent) {
wrap = wraps[parent.internalId];
}
}
}
return wrap;
},
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) {
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;
},
endBulkUpdate: function(){
this.bulkUpdate = false;
},
onRemove : function(ds, record, index) {
var me = this,
bulk = me.bulkUpdate;
if (me.viewReady) {
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,
all = me.all,
animWrap = me.getAnimWrap(record),
item = all.item(index),
node = item ? item.dom : null;
if (!node || !animWrap || !animWrap.collapsing) {
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, false);
if (!animWrap) {
animWrap = me.animWraps[parent.internalId] = 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(),
node = me.getNode(parent),
index = node ? me.indexOf(node) : -1,
animWrap,
animateEl,
targetEl;
if (me.singleExpand) {
me.ensureSingleExpand(parent);
}
// The item is not visible yet
if (index === -1) {
return;
}
animWrap = me.getAnimWrap(parent, false);
if (!animWrap) {
me.isExpandingOrCollapsing = false;
me.fireEvent('afteritemexpand', parent, index, node);
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.refreshSize();
delete me.animWraps[animWrap.record.internalId];
delete queue[id];
}
},
callback: function() {
me.isExpandingOrCollapsing = false;
me.fireEvent('afteritemexpand', parent, index, node);
}
});
animWrap.isAnimating = true;
},
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 = me.animWraps[parent.internalId] = 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(),
node = me.getNode(parent),
index = node ? me.indexOf(node) : -1,
animWrap = me.getAnimWrap(parent),
animateEl, targetEl;
// The item has already been removed by a parent node
if (index === -1) {
return;
}
if (!animWrap) {
me.isExpandingOrCollapsing = false;
me.fireEvent('afteritemcollapse', parent, index, node);
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();
me.refreshSize();
delete me.animWraps[animWrap.record.internalId];
delete queue[id];
}
},
callback: function() {
me.isExpandingOrCollapsing = false;
me.fireEvent('afteritemcollapse', parent, index, node);
}
});
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.isLeaf()) {
row.rowCls = (row.rowCls || '') + ' ' + this.leafCls;
}
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] True to expand nodes all the way down the tree hierarchy.
* @param {Function} [callback] The function to run after the expand is completed
* @param {Object} [scope] 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] True to collapse nodes all the way up the tree hierarchy.
* @param {Function} [callback] The function to run after the collapse is completed
* @param {Object} [scope] 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} record
* @param {Boolean} [deep] True to collapse nodes all the way up the tree hierarchy.
* @param {Function} [callback] The function to run after the expand/collapse is completed
* @param {Object} [scope] The scope of the callback function.
*/
toggle: function(record, deep, callback, scope) {
var me = this,
doAnimate = !!this.animate;
// Block toggling if we are already animating an expand or collapse operation.
if (!doAnimate || !this.isExpandingOrCollapsing) {
if (!record.isLeaf()) {
this.isExpandingOrCollapsing = doAnimate;
}
if (record.isExpanded()) {
me.collapse(record, deep, callback, scope);
} else {
me.expand(record, deep, callback, scope);
}
}
},
onItemDblClick: function(record, item, index) {
var me = this,
editingPlugin = me.editingPlugin;
me.callParent(arguments);
if (me.toggleOnDblClick && record.isExpandable() && !(editingPlugin && editingPlugin.clicksToEdit === 2)) {
me.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) && record.isExpandable()) {
this.toggle(record, e.ctrlKey);
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();
}
});
}
},
shouldUpdateCell: function(column, changedFieldNames){
if (changedFieldNames) {
var i = 0,
len = changedFieldNames.length;
for (; i < len; ++i) {
if (Ext.Array.contains(this.uiFields, changedFieldNames[i])) {
return true;
}
}
}
return this.callParent(arguments);
},
/**
* Re-fires the NodeStore's "write" event as a TreeStore event
* @private
* @param {Ext.data.NodeStore} store
* @param {Ext.data.Operation} operation
*/
onStoreWrite: function(store, operation) {
var treeStore = this.panel.store;
treeStore.fireEvent('write', treeStore, operation);
},
/**
* Re-fires the NodeStore's "datachanged" event as a TreeStore event
* @private
* @param {Ext.data.NodeStore} store
* @param {Ext.data.Operation} operation
*/
onStoreDataChanged: function(store, operation) {
var treeStore = this.panel.store;
treeStore.fireEvent('datachanged', treeStore);
}
});