I would like to know how can i check the sibling nodes of a tree while clicking on a particular node in ExtJs.
I had given id's for each node and i can access the id of a clicked node. then how can i proceed to checking the child nodes automatically ??
somebody please help me..
// or any other way of getting hands on the node you want to work with
var node = treePanel.getNodeById('your-id');
node.eachChild(function(n) {
n.getUI().toggleCheck(true);
});
If you want this to work on the whole subtree of the current node, you'll have to do some recursion.
A little more integrated:
treePanel.on('checkchange', function(node, checked) {
node.eachChild(function(n) {
n.getUI().toggleCheck(checked);
});
});
function nodeCheck(node) {
node.eachChild(function(n) {
if(n.hasChildNodes())
nodeCheck(n)
n.getUI().toggleCheck(false);
});
}
var node = (tree.getSelectionModel().getSelectedNode()) ? tree.getSelectionModel().getSelectedNode() : tree.root;
if(node) nodeCheck(node);
It works well for me ;)
listeners:{
checkchange : function(node, checked) {
node.parentNode.cascadeBy(function(n){n.set('checked', checked);});
}
}
function checkChange(node, checked, Object) {
node.cascadeBy(function(n) {
n.set('checked', checked);
});
}
The answer of Mr C works fine(ExtJS 4.2), but a bug will occur when the childnodes of parentnode has 1 child. Here is my a little improvement. Someone can improve further
function (node, checked) {
if (node.isLeaf()) {
node = node.parentNode;
var siblingStateEqual = true;
if (node.childNodes.length == 1) {
siblingStateEqual = checked;
} else {
node.cascadeBy(function (n) {
if (n != node) {
if (n.get('checked') != checked) {
siblingStateEqual = false;
}
}
});
}
if (siblingStateEqual == checked) {
node.set('checked', checked);
}
}
else {
node.cascadeBy(function (n) {
n.set('checked', checked);
});
}
}
The JSON or XML will need the "checked" property set to true or false when you populate the nodes. I am assuming that you are using an AsyncTreeNode to do this for you. If the tree nodes are created without this checked property present, ExtJS will not render it with the checkbox.
Or, if like me, you need to automatically check/uncheck the parent node when all child leaf nodes are checked/unchecked you can try this:
function (node, checked)
{
if (node.get('leaf'))
{
node = node.parentNode;
var siblingStateEqual = true;
node.cascadeBy(function (n)
{
if (n != node) {
if (n.get('checked') != checked) {
siblingStateEqual = false;
}
}
});
if (siblingStateEqual == checked)
{
node.set('checked', checked);
}
}
else
{
node.cascadeBy(function (n) { n.set('checked', checked); });
}
}
Related
I try to catch Tab key press event. This is what I tried:
this.test.on('keypress', function(t,e) {
if (e.getKey() == Ext.EventObject.ENTER) {
alet('kk');
}
}, this);
Please help to catch Tab key press event in the grid cells.
This my EditorGridPanel override function
Ext.override(Ext.grid.EditorGridPanel, {
initEvents : function(){
Ext.grid.EditorGridPanel.superclass.initEvents.call(this);
this.on("bodyscroll", this.stopEditing, this, [true]);
this.on("columnresize", this.stopEditing, this, [true]);
this.view.scroller.on("mousedown", this.onMouseDownEditClick, this);
this.on('keypress', function(t,e) {
// if (e.getKey() == Ext.EventObject.TAB) {
alert('kk');
// }
}, this);
//Reload datastore without redrawing grid
this.getView().on('beforerefresh', function(view) {
view.scrollTop = view.scroller.dom.scrollTop;
view.scrollHeight = view.scroller.dom.scrollHeight;
});
this.getView().on('refresh', function(view) {
setTimeout(function () {
view.scroller.dom.scrollTop = view.scrollTop + (view.scrollTop == 0 ? 0 : view.scroller.dom.scrollHeight - view.scrollHeight);
}, 100);
});
if(this.clicksToEdit == 'mousedown'){
this.view.scroller.on("mousedown", this.onMouseDownEditClick, this);
}else if(this.clicksToEdit == 1){
this.on("cellclick", this.onCellDblClick, this);
}else {
if(this.clicksToEdit == 'auto' && this.view.mainBody){
this.view.mainBody.on("mousedown", this.onAutoEditClick, this);
}
this.on("celldblclick", this.onCellDblClick, this);
}
},
onMouseDownEditClick : function(e, t){
if(e.button !== 0){
return;
}
var row = this.view.findRowIndex(t);
var col = this.view.findCellIndex(t);
edit_row = row;
edit_col = col;
if(row !== false && col !== false){
this.startEditing(row, col);
}
}
});
I see typo in your code. Instead
alet('kk');
use
alert('kk');
But anyway, did you try to use something like that?
this.test.on('keypress', function(t,e) {
if (e.getKey() == Ext.EventObject.TAB) {
alert('kk');
}
}, this);
I had a similar issue with this, since the 'keypress' event only fires when the event occurs on the EditorGridPanel (not the individual cells).
I put the event listener on the EditorGridPanel's GridView, specifically, for my purposes listening for the 'rowupdated' event (http://docs.sencha.com/extjs/3.4.0/#!/api/Ext.grid.GridView-event-rowupdated) - I hope this helps someone else.
I would like to extend some properties recursive (aka. deep copy).
much like jQuery does. I'm not including jquery only b/c of one thing.
jQuery.extend( true, target, object1 )
is there any elegant way you know of that does it with simple javascript or angularjs?
update
please take a look and try to accomplish the same result
http://plnkr.co/edit/GHabYbyhsqtfBPtplksO?p=preview
i did look into .copy() but the "properties (for objects) are deleted"
Here is an extendDeep function based off of the angular.extend function. If you add this to your $scope, you would then be able to call
$scope.meta = $scope.extendDeep(ajaxResponse1.myMeta, ajaxResponse2.defaultMeta);
and get the answer you are looking for.
$scope.extendDeep = function extendDeep(dst) {
angular.forEach(arguments, function(obj) {
if (obj !== dst) {
angular.forEach(obj, function(value, key) {
if (dst[key] && dst[key].constructor && dst[key].constructor === Object) {
extendDeep(dst[key], value);
} else {
dst[key] = value;
}
});
}
});
return dst;
};
Note: This function has the side-effect of copying values from later arguments into the earlier arguments. For a simple fix to this side effect, you can change dst[key] = value to dst[key] = angular.copy(value).
All the answers here are valid for versions of Angular before 1.4
As of Angular 1.4, you can use angular.merge to do exactly that:
Unlike extend(), merge() recursively descends into object properties of source objects, performing a deep copy.
https://docs.angularjs.org/api/ng/function/angular.merge
function deepExtend(destination, source) {
for (var property in source) {
if (source[property] && source[property].constructor &&
source[property].constructor === Object) {
destination[property] = destination[property] || {};
arguments.callee(destination[property], source[property]);
} else {
destination[property] = source[property];
}
}
return destination;
}
Plunker
Src: https://gist.github.com/gregdangelo/2343158
Building on Ryan's code, you can shorten the object check and you should also NOT extend functions so you don't override object pointers.
var extendDeep = function extendDeep(dst) {
angular.forEach(arguments, function(obj) {
if (obj !== dst) {
angular.forEach(obj, function(value, key) {
if (dst[key] && angular.isObject(dst[key])) {
extendDeep(dst[key], value);
} else if(!angular.isFunction(dst[key])) {
dst[key] = value;
}
});
}
});
return dst;
};
The same solution as Ryan but with support for array merge
function extendDeep(dst) {
angular.forEach(arguments, function (obj) {
if (obj !== dst) {
angular.forEach(obj, function (value, key) {
if (dst[key] && dst[key].constructor && dst[key].constructor === Object) {
extendDeep(dst[key], value);
} else if (dst[key] && dst[key].constructor && dst[key].constructor === Array) {
dst[key].concat(value);
} else if(!angular.isFunction(dst[key])) {
dst[key] = value;
}
}
);
}
}
);
return dst;
}
Angular has a copy method:
angular.copy
I am using ext js to drag and drop data from one dataview to another. I want to know if the drop event happened on top of an existing node or if it was just dropped in the white space of the dataview.
Here's the code for my dropTarget:
...
onDesktopDataViewRender: function (v) {
var dataView = v;
v.dropTarget = Ext.create('Ext.dd.DropTarget', v.el, {
ddGroup: 'FromSearchToDesktop',
notifyDrop: function (source, e, dropData) {
//Want do do something like:
//if(dropped directly on any node) {
// do some logic with that node
//}
//else {
// do the code below
var recordAlreadyExists = false;
v.store.each(function (r) {
if (r.data.ID == dropData.searchData.ID) {
recordAlreadyExists = true;
}
});
if (recordAlreadyExists == false) {
v.store.add(dropData.searchData);
}
//end else
}
});
}
...
Yay! Finally figured this one out.
The quick answer is to create a DropZone for the node. The long answer is how to do that.
In my program, the user can drag items from DataView A to DataView B. After dropping the item in DataView B, the item appears in DataView B. On top of that, the user can drag an item from DataView A, and drop it on a node inside DataView B. The code needs to differentiate between the item being dropped on the DataView and the item being dropped on a node inside the DataView.
Generic instructions:
In DataViewB's onrender event, create a dropTarget with a ddGroup of
"DataViewB"
Inside the notifyDrop function, create a new node.
Also inside the notifyDrop function, create another dropTarget
(this one for the node instead of the DataView) with a ddGroup of "DataViewBNode".
Inside DataViewA's onRender event, create a DragZone with a ddGroup
of "DataViewBNode" (!important!)
Inside of DataViewA's afterrender event, add the dragZone to the
"DataViewB" group.
Now you will be able to drag from DataViewA and drop in the white space of DataViewB to add a node, but you will also be able to drop directly on a node from DataViewB and do a different action.
It's very important that the first ddGroup is for the node, and the one that's added in the afterrender event is for the DataView
Here is the code for DataView A:
onDataViewARender: function (v) {
var dataView = v;
...
v.dragZone = Ext.create('Ext.dd.DragZone', v.getEl(), {
ddGroup: 'DataViewBNode',
getDragData: function (e) {
var sourceEl = e.getTarget(v.itemSelector, 10), d;
if (sourceEl) {
d = sourceEl.cloneNode(true);
d.id = Ext.id();
return v.dragData = {
sourceEl: sourceEl,
repairXY: Ext.fly(sourceEl).getXY(),
ddel: d,
searchData: v.getRecord(sourceEl).data,
store: v.store,
source: 'DataViewA'
}
}
},
getRepairXY: function () {
return this.dragData.repairXY;
}
});
},
onDataViewAAfterRender: function(v) {
var dragZone = v.dragZone;
dragZone.addToGroup('DataViewB');
},
Here is the code for DataViewB
onDataViewBRender: function (v) {
var dataView = v;
v.dropTarget = Ext.create('Ext.dd.DropTarget', v.el, {
ddGroup: 'DataViewB',
notifyDrop: function (source, e, dropData) {
var recordAlreadyExists = false;
v.store.each(function (r) {
if (r.data.ID == dropData.searchData.ID && r.data.Type == dropData.searchData.Type) {
recordAlreadyExists = true;
}
});
if (recordAlreadyExists == false) {
v.store.add(dropData.searchData);
var nodes = v.container.dom.childNodes[0].childNodes;
var index = v.container.dom.childNodes[0].childNodes.length -1;
//
//Here is where you create the dropTarget for the new node
//
nodes[index].dropTarget = Ext.create('Ext.dd.DropTarget', nodes[index], {
ddGroup: 'DataViewBNode',
notifyDrop: function (source, e, dropData) {
console.log('success')
}
});
}
}
});
...
},
I've been searching around and looking into the documentation, but I'm not sure how to activate filtering via the column drop down the way you have it in normal grid panels.
I've tried to implement the ux.Grid.FilterFeatures but when I apply it to the tree panel, my panel doesn't render properly (all blue panel). I thought it might have something to do with its deferred layout, but when I do a treegrid.hide()/treegrid.show()/treegrid.doLayout(), it doesn't make a difference.
Has anyone gotten the filter feature working with the treepanel? Or has anyone got any suggestions on how to rectify this problem?
I modified the filter example in the Ext-Js site to use it in tree grid.
Ext.define('TreeGridFilter', {
extend: 'Ext.grid.feature.Feature'
, alias: 'feature.treeGridFilter'
, collapseOnClear: true // collapse all nodes when clearing/resetting the filter
, allowParentFolders: false // allow nodes not designated as 'leaf' (and their child items) to be matched by the filter
, treeGrid: null
, filterPropertyNames: new Array()
, filterPropertyValues: new Array()
, filterColumnRenderers: new Array()
, init: function (tree) {
var me = this;
treeGrid = me.tree = tree;
var view = me.view;
var headerCt = view.headerCt;
// Listen for header menu being created
headerCt.on('menucreate', me.onMenuCreate, me);
tree.filter = Ext.Function.bind(me.filter, me);
tree.clearFilter = Ext.Function.bind(me.clearFilter, me);
}
,filter: function (value, property, re, columnRenderer) {
var me = this
, tree = me.tree
, matches = [] // array of nodes matching the search criteria
, root = tree.getRootNode() // root node of the tree
, property = property || 'text' // property is optional - will be set to the 'text' propert of the treeStore record by default
, visibleNodes = [] // array of nodes matching the search criteria + each parent non-leaf node up to root
, viewNode;
me.updateValueForName(property, value, columnRenderer);
if (me.filterPropertyNames.length == 0) { // if the search field is empty
me.clearFilter();
return;
}
tree.expandAll(); // expand all nodes for the the following iterative routines
//iterate over all nodes in the tree in order to evalute them against the search criteria
root.cascadeBy(function (node) {
var numberOfFiltersMatched = 0;
for (var index=0; index < me.filterPropertyNames.length; index++)
{
var propertyName = me.filterPropertyNames[index];
var propertyValue = me.filterPropertyValues[index]
var propertyValueOfNode = node.get(propertyName);
if(me.filterColumnRenderers[index] != false){
var renderingFunction = me.filterColumnRenderers[index];
propertyValueOfNode = renderingFunction(propertyValueOfNode); //Using the renderer function of the column
}
var regExpn = new RegExp(propertyValue, "ig") // the regExp could be modified to allow for case-sensitive, starts with, etc.
if(propertyValueOfNode != null && (propertyValueOfNode+'').match(regExpn)) {
numberOfFiltersMatched++;
}
}
if(numberOfFiltersMatched == me.filterPropertyNames.length){
matches.push(node); // add the node to the matches array
}
});
if (me.allowParentFolders === false) { // if me.allowParentFolders is false (default) then remove any non-leaf nodes from the regex match
Ext.each(matches, function (match) {
if (match == null || !match.isLeaf()) {
Ext.Array.remove(matches, match);
}
});
}
Ext.each(matches, function (item, i, arr) { // loop through all matching leaf nodes
root.cascadeBy(function (node) { // find each parent node containing the node from the matches array
if (node.contains(item) == true) {
visibleNodes.push(node); // if it's an ancestor of the evaluated node add it to the visibleNodes array
}
});
if (me.allowParentFolders === true && !item.isLeaf()) { // if me.allowParentFolders is true and the item is a non-leaf item
item.cascadeBy(function (node) { // iterate over its children and set them as visible
visibleNodes.push(node);
});
}
visibleNodes.push(item); // also add the evaluated node itself to the visibleNodes array
});
root.cascadeBy(function (node) { // finally loop to hide/show each node
viewNode = Ext.fly(tree.getView().getNode(node)); // get the dom element assocaited with each node
if (viewNode) { // the first one is undefined ? escape it with a conditional
viewNode.setVisibilityMode(Ext.Element.DISPLAY); // set the visibility mode of the dom node to display (vs offsets)
viewNode.setVisible(Ext.Array.contains(visibleNodes, node));
}
});
}
, clearFilter: function () {
var me = this
, tree = this.tree
, root = tree.getRootNode();
if (me.collapseOnClear) {
tree.collapseAll(); // collapse the tree nodes
}
root.cascadeBy(function (node) { // final loop to hide/show each node
viewNode = Ext.fly(tree.getView().getNode(node)); // get the dom element assocaited with each node
if (viewNode) { // the first one is undefined ? escape it with a conditional and show all nodes
viewNode.show();
}
});
},
onMenuCreate: function(headerCt, menu) {
var me = this;
menu.on('beforeshow', me.onMenuBeforeShow, me);
},
onMenuBeforeShow: function(menu) {
var me = this;
var currentHeaderFilter = menu.activeHeader.filter;
if(currentHeaderFilter == null){
if(me.menuItem == null){
return;
}
me.menuItem.hide();
me.menuSeparator.hide();
}else if(me.menuItem != null){
me.menuItem.show();
me.menuSeparator.show();
}
if(me.menuItem){
var perviousFilterValue = me.getValueForName(menu.activeHeader.dataIndex);
if(perviousFilterValue == null || perviousFilterValue == ''){
me.menuItem.setRawValue('');
}else{
me.menuItem.setRawValue(perviousFilterValue);
}
}else{
me.menuSeparator = menu.add('-');
var filterTextFiels = new Ext.form.TextField({
itemId: 'filterTextBox',
cls : 'find-icon',
listeners: {
'change': this.onFilterTextChange
}
});
me.menuItem = menu.add(filterTextFiels);
}
me.menuItem.activeDataIndex = menu.activeHeader.dataIndex;
me.menuItem.activeRenderer = menu.activeHeader.renderer;
me.menuItem.width = (currentHeaderFilter == null || currentHeaderFilter.width == null) ? 150 : currentHeaderFilter.width;
},
onFilterTextChange : function (searchMenuItem, value) {
treeGrid.filter(value,searchMenuItem.activeDataIndex, null, searchMenuItem.activeRenderer);
},
updateValueForName : function(property, value, columnRenderer){
var propertyIndex = -1;
for (var index=0; index < this.filterPropertyNames.length; index++)
{
if(property == this.filterPropertyNames[index]){
propertyIndex = index;
break;
}
}
if(propertyIndex >= 0){
if(value == null || value == ''){
this.filterPropertyNames.splice(propertyIndex, 1);
this.filterPropertyValues.splice(propertyIndex, 1);
this.filterColumnRenderers.splice(propertyIndex, 1);
}else{
this.filterPropertyValues[propertyIndex] = value;
}
}else{
propertyIndex = this.filterPropertyNames.length;
this.filterPropertyNames[propertyIndex] = property;
this.filterPropertyValues[propertyIndex] = value;
this.filterColumnRenderers[propertyIndex] = columnRenderer;
}
},
getValueForName : function(property){
var propertyIndex = -1;
for (var index=0; index < this.filterPropertyNames.length; index++)
{
if(property == this.filterPropertyNames[index]){
propertyIndex = index;
break;
}
}
if(propertyIndex >= 0){
return this.filterPropertyValues[propertyIndex];
}else{
return null;
}
}
});
This feature can be used in grid as
var treeGridFilter = {
ftype: "treeGridFilter"
};
var treeGrid= Ext.create('Ext.tree.Panel', {
id : 'tree-grid-id',
title: 'View Tree Grid',
features: [treeGridFilter],
store: store,
renderTo: 'grid-div',
columns: [{
text: 'Column One',
dataIndex: 'columnOne',
filter : {
width: 150
}
},{
text: 'Column Two',
dataIndex: 'columnTwo',
filter : {
width: 100
}
}]
});
Tree stores don't have a filtering function, but I had to create one for an earlier project. Here's an example.
So, as a suggestion you could create your own filter drop menus using Ext.grid.header.Container configs in the tree columns (as covered here in the docs) these could be set-up to call some kind of filter function on the treestore like the one I linked to above.
I originally posted this on the Sencha forums here but didn't get any responses (other than my own answer, which I will post soon), so I am going to repost it here and see if I get anymore help.
I've been racking my brain on how to filter a TreeStore in 4.0.7. I've tried the following:
The model
Ext.define('model', {
extend: 'Ext.data.Model',
fields: [
{name: 'text', type: 'string'},
{name: 'leaf', type: 'bool'},
{name: 'expanded', type: 'bool'},
{name: 'id', type: 'string'}
],
hasMany: {model: 'model', name: 'children'}
});
The store
Ext.define('myStore', {
extend: 'Ext.data.TreeStore',
model: 'model',
storeId: 'treestore',
root: {
text: 'root',
children: [{
text: 'leaf1',
id: 'leaf1',
children: [{
text: 'child1',
id: 'child1',
leaf: true
},{
text: 'child2',
id: 'child2',
leaf: true
}]
},{
text: 'leaf2',
id: 'leaf2',
leaf: true
}]
},
proxy: {
type: 'memory',
reader: {
type: 'json'
}
}
});
The tree
var myTree = Ext.create('Ext.tree.Panel', {
id: 'myTree',
selType: 'cellmodel',
selModel: Ext.create('Ext.selection.CellModel', {mode: 'MULTI'}),
rootVisible: false,
store: Ext.create('myStore'),
width: 300
});
The filter
var filter = Ext.create('Ext.util.Filter', {
filterFn: function(item) {
return item.data.text == 'leaf1';
}
});
So I think my problem is... I don't know how to use this filter due to TreeStore not actually inheriting any type of filter functions like a normal store. I've tried:
myTree.store.filters.add(filter);
myTree.store.filters.filter(filter); // This seems to work
// I can get into the filterFn when debugging, but I think item is the "this" of my filter object.
Normally, if I have a grid and I create a filter like above, I can just do myTree.store.filter(filter) and it'll grab each row's item/filter on what I return... but I'm thinking because TreeStore doesn't inherit a filtering function, that's not being passed in.
If someone could provide some clarity as to what I'm doing wrong or any insight on how to set up a filter function/my thinking process, please go ahead. I'd appreciate any help.
Thanks for catching that other one, I fixed up the answer to include the more dynamic treestore filter override that I included below to answer your Q.
It is working fine in 4.1b2, I know there were some changes to the treestore between 4.07 and 4.1 but I think 4.07 still had the tree objects I am using here.
Here's the override:
Ext.override(Ext.data.TreeStore, {
hasFilter: false,
filter: function(filters, value) {
if (Ext.isString(filters)) {
filters = {
property: filters,
value: value
};
}
var me = this,
decoded = me.decodeFilters(filters),
i = 0,
length = decoded.length;
for (; i < length; i++) {
me.filters.replace(decoded[i]);
}
Ext.Array.each(me.filters.items, function(filter) {
Ext.Object.each(me.tree.nodeHash, function(key, node) {
if (filter.filterFn) {
if (!filter.filterFn(node)) node.remove();
} else {
if (node.data[filter.property] != filter.value) node.remove();
}
});
});
me.hasFilter = true;
console.log(me);
},
clearFilter: function() {
var me = this;
me.filters.clear();
me.hasFilter = false;
me.load();
},
isFiltered: function() {
return this.hasFilter;
}
});
It uses the store.tree.nodeHash object to iterate through all nodes against the filters rather than just the first child. It will accept a filter as a function or property/value pair. I suppose the clearFilter method could be worked over though to prevent another ajax call.
This is the answer that I came up with... it's not ideal, so I'm hoping someone can provide a better, more generic approach. Why? Well, if my tree had a parent that had a child that had a child, I'd like to filter on those, but my solution only goes one child deep.
Thanks to this thread, I figured some things out. The only problem with this thread is that it made filtering flat... so child nodes wouldn't appear under their parent nodes. I modified their implementation and came up with this (it only goes 1 child deep, so it wouldn't work if you have a parent that contains a child that has a child):
TreeStore
filterBy : function(fn, scope) {
var me = this,
root = me.getRootNode(),
tmp;
// the snapshot holds a copy of the current unfiltered tree
me.snapshot = me.snapshot || root.copy(null, true);
var hash = {};
tmp = root.copy(null, true);
tmp.cascadeBy(function(node) {
if (fn.call(me, node)) {
if (node.data.parentId == 'root') {
hash[node.data.id] = node.copy(null, true);
hash[node.data.id].childNodes = [];
}
else if (hash[node.data.parentId]) {
hash[node.data.parentId].appendChild(node.data);
}
}
/* original code from mentioned thread
if (fn.call(scope || me, node)) {
node.childNodes = []; // flat structure but with folder icon
nodes.push(node);
}*/
});
delete tmp;
root.removeAll();
var par = '';
for (par in hash) {
root.appendChild(hash[par]);
}
return me;
},
clearFilter: function() {
var me = this;
if (me.isFiltered()) {
var tmp = [];
var i;
for (i = 0; i < me.snapshot.childNodes.length; i++) {
tmp.push(me.snapshot.childNodes[i].copy(null, true));
}
me.getRootNode().removeAll();
me.getRootNode().appendChild(tmp);
delete me.snapshot;
}
return me;
},
isFiltered : function() {
return !!this.snapshot;
}
So this works when I do something like this (using my tree in the first post):
Ext.getCmp('myTree').store.filterBy(function(rec) {
return rec.data.id != 'child1';
});
This code will return every record that doesn't have a child1 id, so under leaf1, it will only have child2 as the node. I can also clear the filter by doing Ext.getCmp('myTree').store.clearFilter().
Now, I realize I just answered my own question, but like I posted above, I'd really like critiquing/advice on what I can make more efficient and generic. If anyone has any tips, I'd love to hear them! Also, if you need help getting this code up and running, let me know.
Sha, I also tried filters, but no luck. Have a look at this thread.
The above override is great, and it solves some of my problems, however, I found a bug that is hard to find with the above code. After spending half a day, I figured out we need to use slice() to copy the array otherwise some nodes get deleted.
Ext.override(Ext.data.TreeStore, {
hasFilter: false,
/**
* Filters the current tree by a function fn
* if the function returns true the node will be in the filtered tree
* a filtered tree has also a flat structure without folders
*/
filterBy: function (fn, scope) {
var me = this,
nodes = [],
root = me.getRootNode(),
tmp;
// the snapshot holds a copy of the current unfiltered tree
me.snapshot = me.snapshot || root.copy(null, true);
tmp = me.snapshot.copy(null, true);
var childNodes = tmp.childNodes.slice();
root.removeAll();
for (var i = 0; i < childNodes.length; i++) {
//Recursively tranverse through the root and adds the childNodes[i] if fn returns true
this.traverseNode(childNodes[i], root, fn);
}
return me;
},
/**
* Recursively tranverse through the root and adds the childNodes[i] if fn returns true
*/
traverseNode: function (node, parentNode, fn) {
var me = this;
if (fn.call(me, node)) {
parentNode.appendChild(node);
return true;
}
if (node.hasChildNodes()) {
var t_childNodes = node.childNodes.slice();
var found = false;
for (var i = 0; i < t_childNodes.length; i++) {
if (this.traverseNode(t_childNodes[i], node, fn) == true) {
found = true;
}
}
if (found == true) {
parentNode.appendChild(node);
return true;
}
}
return false;
},
/**
* Clears all filters a shows the unfiltered tree
*/
clearFilter: function () {
var me = this;
if (me.isFiltered()) {
me.setRootNode(me.snapshot);
delete me.snapshot;
}
return me;
},
/**
* Returns true if the tree is filtered
*/
isFiltered: function () {
return !!this.snapshot;
}
});
I was looking for a way to filter a treestore so that if a filterBy function returned true for any node, I wanted to display the complete node hierarchy of that node including all the parent nodes, grand parent node, etc and child nodes, grand child node, etc. I modified it from the other solutions provided in this question. This solutions works recursively so the treestore can be of any size.
Ext.override(Ext.data.TreeStore, {
hasFilter: false,
/**
* Filters the current tree by a function fn
* if the function returns true the node will be in the filtered tree
* a filtered tree has also a flat structure without folders
*/
filterBy : function(fn, scope) {
var me = this,
nodes = [],
root = me.getRootNode(),
tmp;
// the snapshot holds a copy of the current unfiltered tree
me.snapshot = me.snapshot || root.copy(null, true);
tmp = me.snapshot.copy(null, true);
var childNodes = tmp.childNodes;
root.removeAll();
for( var i=0; i < childNodes.length; i++ ) {
//Recursively tranverse through the root and adds the childNodes[i] if fn returns true
if( this.traverseNode( childNodes[i], root, fn ) == true ) {
i--;
}
}
return me;
},
/**
* Recursively tranverse through the root and adds the childNodes[i] if fn returns true
*/
traverseNode: function( node, parentNode, fn ) {
var me = this;
if( fn.call( me, node ) ) {
parentNode.appendChild( node );
return true;
}
if( node.hasChildNodes() ) {
var childNodes = node.childNodes;
var found = false;
for( var i=0; i < childNodes.length; i++ ) {
if( this.traverseNode( childNodes[i], node, fn ) == true ) {
found = true;
}
}
if( found == true ) {
parentNode.appendChild( node );
return true;
}
}
return false;
},
/**
* Clears all filters a shows the unfiltered tree
*/
clearFilter : function() {
var me = this;
if (me.isFiltered()) {
me.setRootNode(me.snapshot);
delete me.snapshot;
}
return me;
},
/**
* Returns true if the tree is filtered
*/
isFiltered : function() {
return !!this.snapshot;
}
});
So it works with just like a regular store filterBy call.
searchText = "searchText";
store.filterBy( function(item) {
var keys = item.fields.keys;
for( var i=0; i < keys.length; i++ ) {
var value = item.get( keys[i] );
if( value != null ) {
if( value.toString().toLowerCase().indexOf( searchText ) !== -1 ) {
return true;
}
}
}
return false;
});
I was able to do some basic filtering using onbeforeappend event.
While not as well structured as the above solutions, this provides an easy and straight forward way to apply basic filtering without the need to override base class methods or use external plugins.
I implemented my filtering in the store itself.
In more advanced scenarios this can be done in controller too.
Ext.define('MyApp.store.FilteredTreeStore', {
extend: 'Ext.data.TreeStore',
....
....
listeners: {
beforeappend: function (thisStore, node, eOpts) {
var allowAppend = false;
allowAppend = --your filtering logic here
--returning false will cancel append of the entire sub tree
return allowAppend;
}
}
});