Slickgrid Treeview Search

时光毁灭记忆、已成空白 提交于 2019-12-06 14:27:02
Binke

UPDATE:

I had to improve the code i wrote over a year ago this week, and with alot of testing, this is what i ended up with. This method is alot faster than the old one, and i mean alot! Tested with a node depth of 5 nodes and 5100 rows this data preparation takes around 1.3s, but if you don't need case insensitive search, removing toLowerCase will half that time to around 600ms. When the searchstrings are prepared, the search is instant.

This is from our setData function where we prepare the data

var self = this,
    searchProperty = "name";

//if it's a tree grid, we need to manipulate the data for us to search it
if (self.options.treeGrid) {

    //createing index prop for faster get
    createParentIndex(items);

    for (var i = 0; i < items.length; i++) {
        items[i]._searchArr = [items[i][searchProperty]];
        var item = items[i];

        if (item.parent != null) {
            var parent = items[item.parentIdx];

            while (parent) {
                parent._searchArr.push.apply(
                    parent._searchArr, uniq_fast(item._searchArr)
                    );

                item = parent;
                parent = items[item.parentIdx];
            }
        }
    }

    //constructing strings to search
    //for case insensitive (.toLowerCase()) this loop is twice as slow (1152ms instead of 560ms for 5100rows) .toLowerCase();
    for (var i = 0; i < items.length; i++) {
        items[i]._search = items[i]._searchArr.join("/").toLowerCase(); 
        items[i]._searchArr = null;
    }

    //now all we need to do in our filter is to check indexOf _search property
}

In the code above, I use some functions. The first one creates two properties, one for its own position in the array, and the second parentIdx for parents index. I'm not so sure if this actually speeds up the performance, but it removes the need for a nested loop in the setData function.

The one that actually makes all the difference here is the uniq_fast, which takes an array and removes all the duplicates in it. The method is one of the many functions from this answer remove-duplicates-from-javascript-array

function createParentIndex(items) {
    for (var i = 0; i < items.length; i++) {
        items[i].idx = i; //own index
        if (items[i].parent != null) {
            for (var j = 0; j < items.length; j++) {
                if (items[i].parent === items[j].id) {
                    items[i].parentIdx = j; //parents index
                    break;
                }
            }
        }
    }
}

function uniq_fast(a) {
    var seen = {};
    var out = [];
    var len = a.length;
    var j = 0;
    for (var i = 0; i < len; i++) {
        var item = a[i];
        if (seen[item] !== 1) {
            seen[item] = 1;
            out[j++] = item;
        }
    }
    return out;
}

Now with all this preparation of the data, our filter function actually becomes pretty small and easy to handle. The filter function is called for each item, and as we now have the _search property on each item, we just check that. If no filter applied we need to make sure that we don't show closed nodes

function treeFilter(item, args) {
    var columnFilters = args.columnFilters;

    var propCount = 0;
    for (var columnId in columnFilters) {
        if (columnId !== undefined && columnFilters[columnId] !== "") {
            propCount++;

            if (item._search === undefined || item._search.indexOf(columnFilters[columnId]) === -1) {
                return false;
            } else {
                item._collapsed = false;
            }
        }
    }

    if (propCount === 0) {
        if (item.parent != null) {
            var dataView = args.grid.getData();
            var parent = dataView.getItemById(item.parent);
            while (parent) {
                if (parent._collapsed) {
                    return false;
                }

                parent = dataView.getItemById(parent.parent);
            }
        }
    }     

    return true;
}

So, the question was asked long ago, but if someone is looking for an answer for this, use the code above. It's fast, but any improvements of the code would be much appritiated!

END OF EDIT

old answer (this is very slow):

As a start, you have to create a filter function that you use with your dataView. The dataView will call your function as soon as you type something. The function will be called for each row in the dataView, passing the row as the item parameter. Returning false indicates that the row should be hidden, and true for visible.

Looking at the Tree example, the filter function looks like this

function myFilter(item, args) {
  if (item["percentComplete"] < percentCompleteThreshold) {
    return false;
  }

  if (searchString != "" && item["title"].indexOf(searchString) == -1) {
    return false;
  }

  if (item.parent != null) {
    var parent = data[item.parent];

    while (parent) {
      if (parent._collapsed || (parent["percentComplete"] < percentCompleteThreshold) || (searchString != "" && parent["title"].indexOf(searchString) == -1)) {
        return false;
      }

      parent = data[parent.parent];
    }
  }

  return true;
}

In my first attempt to do this, I tried to manipulate the parent so that it should not be hidden. The problem is that i have no clue how to unhide it, and the problem is also that you don't know in which order the rows will be filtered (if the parent row is the last to be filtered, parent property is null)

I abandoned that thought and tried to work with the item passed into the method, as this is how it's intended. The way to do it when working with basic parent/child tree structures is to use recursion.

My solution

To start, create a function that holds all the filtering and returns true or false. I've used fixed header row for fast filters as a base and then added my own rules to it. This is a really stripped down version of my realFilter function, so you might need to tweak it a little bit.

function realFilter(item, args) {
    var columnFilters = args.columnFilters;
    var grid = args.grid;
    var returnValue = false;

    for (var columnId in columnFilters) {
        if (columnId !== undefined && columnFilters[columnId] !== "") {
            returnValue = true;
            var c = grid.getColumns()[grid.getColumnIndex(columnId)];

            if (item[c.field].toString().toLowerCase().indexOf(
                columnFilters[columnId].toString().toLowerCase()) == -1) { //if true, don't show this post
                returnValue = false;
            }
        }
    }
    return returnValue;
}

Secondly, it's time for the recursive function. This is the tricky part if you'r not familiar with how they work.

//returns true if a child was found that passed the realFilter
function checkParentForChildren(parent, allItems, args) { 
    var foundChild = false;
    for (var i = 0; i < allItems.length; i++) {
        if (allItems[i].parent == parent.id) {
            if (realFilter(allItems[i], args) == false && foundChild == false) //if the child do not pass realFilter && no child have been found yet for this row 
                foundChild = checkParentForChildren(allItems[i], allItems, args);
            else
                return true;
        }
    }
    return foundChild;
}

At last, we implement the original filter function. This is the function that is called by slickgrid and should be registered to the dataView

//registration of the filter
dataView.setFilter(filter);

//the base filter function
function filter(item, args) {
    var allRows = args.grid.getData().getItems();
    var columnFilters = args.columnFilters;
    var grid = args.grid;
    var checkForChildren = false;

    for (var i = 0; i < allRows.length; i++) {
        if (allRows[i].parent == item.id) {
            checkForChildren = true;
            break;
        }
    }

    for (var columnId in columnFilters) {
        if (columnId !== undefined && columnFilters[columnId] !== "") {
            var c = grid.getColumns()[grid.getColumnIndex(columnId)];
            var searchString = columnFilters[columnId].toLowerCase().trim();

            if (c != undefined) {
                if (item[c.field] == null || item[c.field] == undefined) {
                    return false;
                }
                else { 
                    var returnValue = true;

                    if (checkForChildren) {
                        returnValue = checkParentForChildren(item, allRows, args);
                        if(!returnValue)
                            returnValue = realFilter(item, args);
                    }
                    else
                        returnValue = realFilter(item, args);

                    if (item.parent != null && returnValue == true) {
                        var dataViewData = args.grid.getData().getItems();
                        var parent = dataViewData[item.parent];

                        while (parent) {
                            if (parent._collapsed) {
                                parent._collapsed = false;
                            }
                            parent = dataViewData[parent.parent];
                        }
                    }

                    return returnValue;
                }
            }
        }
    }

    if (item.parent != null) {
        var dataViewData = args.grid.getData().getItems();
        var parent = dataViewData[item.parent];

        while (parent) {
            if (parent._collapsed) {
                return false;
            }

            parent = dataViewData[parent.parent];
        }
    }
    return true;
}

I'm currently working on this so I have not really bothered to improve the code yet. It is working as far as i know, but you may have to tweak some things in filter and realFilter to get it to work as you expect. I wrote this today so it's not tested more than under the development phase.

Note: If you want to use another input for your search you can just use $.keyup() on that field and then pass the data to the header filter. This way you get all the functionality to use column-level filters, even if you don't want to use them in this particular case.

personally I use the grouping example and I also helped on making it multi-column (nested) grouping and with that one it does exactly what you are looking for... So instead of using the one you said, which I think is mainly made for indenting only, you should use this one Interactive grouping and aggregates.

The example does not include the search but it's easy to add it, just like in my project. And yes the parent group never goes away. In the example for multi-grouping, choose 50k rows and then click on "Group by duration then effort-driven then percent", you will see a nice 3 columns grouping :) Copy it, add the search bar and it should work

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!