Hybrid View

  1. #1
    Ext JS Premium Member
    Join Date
    Oct 2009
    Posts
    60
    Vote Rating
    1
    hhangus is on a distinguished road

      2  

    Default Tree filtering

    Tree filtering


    I have implemented an extended Ext.tree.View that provides proper filtering (hiding) of nodes and is extremely fast even on large trees. On my 3yo computer a tree of 1500+ nodes, nested up to 4 deep (maxExpandDepth:2), can be filtered in about 2 seconds.

    The code is below. Use it in a treepanel with the param "viewType:'treefilteringview'"

    You can create multiple filter components that will all be applied additively. So, for example, you could have a combo filter that filters on some category, and a text filter for general search within that selected category.

    NB: This does not filter the TreeStore. Only the view is filtered.

    NB2: Tree filtering is on the Extjs roadmap for an upcoming release so this extension may not be necessary in the long run.


    2014-07-23 - Replaced begin/endBulkUpdate with the global and much more efficient Ext.suspend/resumeLayouts. The speedup is crazy awesome, but v4.1+ only.

    Code:
    /**
     * @class Ext.ux.tree.FilteringView
     * @extends Ext.tree.View
     * Enhances the basic tree.View with filtering capabilities. Any component that implements the functions
     * 'filterFn' and 'reset' can be used with this view. However, it is recommended to use the 
     * enhanced form fields also defined in this extension.
     */
    Ext.define('Ext.ux.tree.FilteringView',{
     extend:'Ext.tree.View',
     alias:'widget.treefilteringview', /** configs **/
     useDataIds:false,//Use node.data.id when hashing to use your own unique ids.
     maxExpandDepth:2,//Max depth to perform expansion of visible nodes.
     hideEmptyFolders:false,//pretty obvious
     /** local vars **/
     filterRegister: new Ext.util.HashMap(),
     filterNodeHash: [],
     filtered:false,
     doNotFilter:false,
     expand:function(node){
      this.callParent(arguments);
      if(this.isFiltered()){ this.applyFilters(node,0); }
     },
     refresh:function(){
      this.callParent(arguments);
      if(this.isFiltered()){ this.applyFilters(null,0); }
     },
     registerFilter: function(filterCmp){
      if(!this.filterRegister.containsKey(filterCmp.id)){ 
       this.filterRegister.add(filterCmp.id, filterCmp); 
      }
     },
     /**
      * Adds nodes to the filterNodeHash indicating whether they should be shown
      * or hidden. Nodes are added/removed based on the return value of the supplied
      * filterCmp.filterFn function (true = show, false = hide).
      */
     applyFilterFn: function(filterCmp) {
      var me = this;
      var root = this.getTreeStore().getRootNode();
      me.registerFilter(filterCmp);
      me.filtered = true;
      //collapse all nodes.  adapted from the collapseAll code in the tree panel.
      Ext.suspendLayouts();
      if (root) {
       if (me.rootVisible) {
        root.collapse(true);
       } else {
        root.collapseChildren(true);
       }
      }
      if(typeof filterCmp.beforeFilter === 'function'){
       filterCmp.beforeFilter();
      }
      root.cascadeBy(function(node){
       if(node.isRoot() && !me.rootVisible){ return; }//skip invisible root
       var nid = (me.useDataIds===true)? node.data.id:node.id;
       if(me.filterNodeHash[nid]==undefined){ 
        me.filterNodeHash[nid] = []; 
       }
       if(filterCmp.filterFn.call(filterCmp,node)){
        me.filterNodeHash[nid][filterCmp.id] = true;
       }else{
        me.filterNodeHash[nid][filterCmp.id] = false;
       }
      },me);
    
      me.applyFilters(root,0);
      Ext.resumeLayouts(true);
      if(typeof filterCmp.afterFilter === 'function'){
       filterCmp.afterFilter();
      }
     },
     /**
     * Runs over nodes starting from 'node' recursively expanding and hidding nodes
     * that are marked hidden by at least one filter in the filterNodeHash.
     * Nodes that have no visible children are collapsed.
     *
     * @params
     * node The node at which to begin filtering.
     * myDepth The depth of the current recursive call. Used to stop expansion
     *  of nodes deeper than the value of maxExpandDepth.
     **/
     applyFilters: function(node){
      if(this.doNotFilter){ return; }
      var me = this;
      var hasVisibleChild=false;
      var node = (node===null || node === undefined)? this.getTreeStore().getRootNode():node;
      var myDepth = node.getDepth();
      /** 
      * Don't filter when we expand the node internally or we 
      * will have several instances of filtering going on at the same time!
      **/
      if(!node.rendered){
       me.doNotFilter=true;
       node.expand();//necessary to be sure Ext.fly will have access to a rendered element
       me.doNotFilter=false;
      }
      node.eachChild(function(childNode){
       var el = Ext.fly(me.getNodeByRecord(childNode));
       el.setVisibilityMode(Ext.Element.DISPLAY);
       if(me.isNodeFiltered(childNode)){
        childNode.collapse(true);
        el.setVisible(false);
       }else{
        hasVisibleChild=true;
        el.setVisible(true);
        if((myDepth+1) < me.maxExpandDepth){
         me.applyFilters(childNode);
        }
       }
      });
      if(!hasVisibleChild){
       node.collapse();
       if(me.hideEmptyFolders){
        Ext.fly(me.getNodeByRecord(node)).setVisible(false);
       }
      }
     },
     /**
     * Clears the specified filter.
     * @params:
     * filterCmp The component registered as a filter.
     * apply  Set false if you don't want the changes applied immediately.
     */
     clearFilter: function(filterCmp,apply){
      if(this.isFiltered()){
       if(typeof filterCmp.beforeClearFilter === 'function'){
        filterCmp.beforeClearFilter();
       }
       for(n in this.filterNodeHash){
        if(this.filterNodeHash[n][filterCmp.id]===false){ this.filterNodeHash[n][filterCmp.id]=true; }
       }
       if(apply!==false){
        this.applyFilters(null,0);
       }
       if(typeof filterCmp.afterClearFilter === 'function'){
        filterCmp.afterClearFilter();
       }
      }
     },
     /**
     * Clears all filters.
     * @params:
     * apply  Set false if you don't want the changes applied immediately.
     */
     clearAllFilters : function(apply) {
      if (this.isFiltered()) {
       this.filterNodeHash = [];
       this.filtered = false;
       if(apply!==false){
        //this just 'refreshes' the tree view back to an unfiltered state.
        //this is orders of magnitude faster than re-applying empty filters.
        if(this.rendered){
         this.refreshSize();
         this.updateIndexes();
        }
       }
      }
     },
     /**
     * Returns true if the tree is filtered
     */
     isFiltered : function() {
      return this.filtered;
     },
     /**
     * Returns true if the specified node is filtered by any of the managed filters
     */
     isNodeFiltered:function(node){
      var me = this;
      var nid = (me.useDataIds===true)? node.data.id:node.id;
      for(var f in me.filterNodeHash[nid]){
       if(me.filterNodeHash[nid][f]===false){
        return true;
       }
      }
      return false;
     }
    });
    
    /**
     * @class Ext.ux.tree.TreeTextFilter
     * @extends Ext.form.Trigger
     * Provides a basic text entry field with a trigger for clearing the field/filter and another
     * to apply the field value and filter.
    **/
    Ext.define('Ext.ux.tree.TreeTextFilter',{
     extend:'Ext.form.Trigger',
     alias:'widget.treetextfilter',
     value:'',
     tree:null,
     trigger1Cls: 'x-form-clear-trigger',
     trigger2Cls: 'x-form-select-trigger',
     initComponent:function(){
      this.callParent(arguments);
      try{
       if(typeof this.tree === 'string'){ this.tree = Ext.getCmp(this.tree); }
      }catch(e){ console.log('Invalid tree provided to this treetextfilter'); }
      
      //Apply filter when user types the 'Enter' key
      this.on('specialkey', function(f, e){
       if(e.getKey() == e.ENTER){
        this.onTrigger2Click();
       }
      }, this);
     },
     onTrigger1Click:function(){
      this.setValue('');
      this.tree.getView().clearFilter(this);
     },
     onTrigger2Click:function(){
      var me = this;
      this.value = this.getRawValue().trim();
      me.tree.getView().applyFilterFn(me);
     },
     /* Override this function to implement custom filtering */
     filterFn:function(node){return true;}
    });
    /**
     * @class Ext.ux.tree.TreeComboFilter
     * @extends Ext.form.ComboBox
     * Provides a drop-down combobox selector that will apply the filter when
     * any item in the drop-down is selected.
    **/
    Ext.define('Ext.ux.tree.TreeComboFilter',{
     extend: 'Ext.form.ComboBox',
     alias:'widget.treecombofilter',
     tree:null,
     editable:false,
     triggerAction:'all',
     forceSelection:true,
     queryMode:'local',
     remove:true,
     initComponent:function(){
      this.callParent(arguments);
      try{
       if(typeof this.tree === 'string'){ this.tree = Ext.getCmp(this.tree); }
      }catch(e){ console.log('Invalid tree provided to this treecombofilter:'+this.id); }
     },
     listeners:{select:{fn:function(combo,records,eOpts){
      var me = this;
      this.tree.getView().applyFilterFn(this);
     }}},
     /* Override this function to implement custom filtering */
     filterFn:function(node){return true;}
    });
    An example implementation of a toolbar with multiple filters:

    Code:
            var tree = Ext.create('Ext.tree.Panel',{ 
                   .... ,
                   viewType:'treefilteringview'
            };
    
            var filterBar = Ext.create('Ext.toolbar.Toolbar');
            filterBar.add({
                    xtype:'treetextfilter',
                    itemId:'textfilter',
                    emptyText:'Enter search terms',
                    tree:tree,
                    _reselectNode:false,
                    filterFn:function(node){
                            var re = new RegExp(Ext.escapeRe(this.value), 'i');
    
                            return re.test(node.data.someField2Test);
                    },
                    //Example use of before/after functions.  Reselect the node that was selected before filtering.
                    beforeClearFilter:function(){
                            this._reselectNode = this.tree.getSelectionModel().getSelection()[0] || false;
                    },
                    afterClearFilter:function(){
                            this.tree.collapseAll();
                            if(this._reselectNode){ this.tree.expandPath(this._reselectNode.getPath()); this.tree.getSelectionModel().select(this._reselectNode); }
                            else{ this.tree.expandPath(this.tree.getRootNode().getPath()); }
                    }
            },{
                    xtype:'treecombofilter',
                    itemId:'disciplinefilter',
                    emptyText:'<Filter by discipline>',
                    tree:tree,
                    displayField:'name',
                    valueField:'code',
                    store: Ext.create('Ext.data.ArrayStore',{
                            model: Ext.define('filterModel1',{
                                    extend:'Ext.data.Model',
                                    fields:['name','code']
                            }),
                            data:[....]
                    }),
                    filterFn:function(node){
                            var value = this.getValue();
                            var re = new RegExp('^([A-Z0-9]{2,3}-)?'+Ext.escapeRe(value)+'-','i');
    
                            return re.test(node.data.testField);
                    }
            },{
                    xtype:'treecombofilter',
                    itemId:'contractsfilter',
                    emptyText:'<Filter by contract>',
                    store: contractsStore,
                    tree:tree,
                    displayField:'name',
                    valueField:'id',
                    filterFn:function(node){
                            var value = this.getRawValue();
                            var re = new RegExp('^'+Ext.escapeRe(value)+'$','i');
    
                            return re.test(node.data.testField);
                    }
            },{
                    text:'Clear filters',
                    handler:function(){
                            filterBar.getComponent('textfilter').setValue('');
                            filterBar.getComponent('disciplinefilter').reset();
                            filterBar.getComponent('contractsfilter').reset();
                            var node = tree.getSelectionModel().getSelection()[0];
                            tree.getView().clearAllFilters();
                            tree.collapseAll(function(){
                                    if(node){ tree.expandPath(node.getPath()); tree.getSelectionModel().select(node); }
                                    else{ tree.expandPath(defaultPath); }
                            });
                    }
            });
            tree.addDocked(filterBar);
    Last edited by hhangus; 23 Jul 2014 at 2:53 PM. Reason: replaced begin/endBulkUpdate with global Ext.suspend/resumeLayouts

  2. #2
    Sencha - Support Team scottmartin's Avatar
    Join Date
    Jul 2010
    Location
    Houston, Tx
    Posts
    8,916
    Vote Rating
    443
    scottmartin has a brilliant future scottmartin has a brilliant future scottmartin has a brilliant future scottmartin has a brilliant future scottmartin has a brilliant future scottmartin has a brilliant future scottmartin has a brilliant future scottmartin has a brilliant future scottmartin has a brilliant future scottmartin has a brilliant future scottmartin has a brilliant future

      0  

    Default


    Thank you for the contribution.

    Scott.

  3. #3
    Sencha User
    Join Date
    Apr 2012
    Posts
    1
    Vote Rating
    0
    sinvalju is on a distinguished road

      0  

    Default Fail in extjs 4.1.1a

    Fail in extjs 4.1.1a


    Hello,
    Thanks for the work, but see this error


    Uncaught TypeError: Cannot call method 'getView' of null

  4. #4
    Ext JS Premium Member
    Join Date
    Oct 2009
    Posts
    60
    Vote Rating
    1
    hhangus is on a distinguished road

      0  

    Default


    It's been working in production on 4.1.1a for me. Likely you're not folowing the example closely enough. For example, are you making sure to pass the tree to your filter compnents?

    In any case, you're going to have to provide actual code if you want help because there are a mllion reasons getView() might not be defined.

  5. #5
    Sencha User
    Join Date
    Dec 2012
    Posts
    6
    Vote Rating
    0
    Bcg24 is on a distinguished road

      0  

    Default


    How would one utilize this 'view'. Should I just have a config in my tree panel. View:'nameofThis' ?

  6. #6
    Ext JS Premium Member
    Join Date
    Oct 2009
    Posts
    60
    Vote Rating
    1
    hhangus is on a distinguished road

      0  

    Default


    The first line of the example implementation I provided answers your question.

Turkiyenin en sevilen filmlerinin yer aldigi xnxx internet sitemiz olan ve porn sex tarzi bir site olan mobil porno izle sitemiz gercekten dillere destan bir durumda herkesin sevdigi bir site olarak tarihe gececege benziyor. Sitenin en belirgin ozelliklerinden birisi de Turkiyede gercekten kaliteli ve muntazam, duzenli porno izle siteleri olmamasidir. Bu yuzden iste. Ayrica en net goruntu kalitesine sahip adresinde yayinlanmaktadir. Mesela diğer sitelerimizden bahsedecek olursak, en iyi hd porno video arşivine sahip bir siteyiz. "The Best anal porn videos and slut anus, big asses movies set..." hd porno faketaxi