Fuzzy Matches on dijit.form.ComboBox / dijit.form.

2019-09-06 16:51发布

问题:

I am trying to extend dijit.form.FilteringSelect with the requirement that all instances of it should match input regardless of where the characters are in the inputted text, and should also ignore whitespace and punctuation (mainly periods and dashes).

For example if an option is "J.P. Morgan" I would want to be able to select that option after typing "JP" or "P Morgan".

Now I know that the part about matching anywhere in the string can be accomplished by passing in queryExpr: "*${0}*" when creating the instance.

What I haven't figured out is how to make it ignore whitespace, periods, and dashes. I have an example of where I'm at here - http://jsfiddle.net/mNYw2/2/. Any help would be appreciated.

回答1:

the thing to master in this case is the store fetch querystrings.. It will call a function in the attached store to pull out any matching items, so if you have a value entered in the autofilling inputfield, it will eventually end up similar to this in the code:

    var query = { this.searchAttr: this.get("value") }; // this is not entirely accurate
    this._fetchHandle = this.store.query(query, options);
    this._fetchHandle.then(  showResultsFunction  );

So, when you define select, override the _setStoreAttr to make changes in the store query api

dojo.declare('CustomFilteringSelect', [FilteringSelect], {
    constructor: function() {
        //???
    },
    _setStoreAttr: function(store) {
          this.inherited(arguments); // allow for comboboxmixin to modify it
          // above line eventually calls this._set("store", store);
          // so now, 'this' has 'store' set allready
          // override here
          this.store.query = function(query, options) {
               // note that some (Memory) stores has no 'fetch' wrapper
          };
    }
});

EDIT: override queryEngine function as opposed to query function

Take a look at the file SimpleQueryEngine.js under dojo/store/util. This is essentially what filters the received Array items on the given String query from the FilteringSelect. Ok, it goes like this:

var MyEngine = function(query, options) {
        // create our matching query function
        switch(typeof query){
                default:
                        throw new Error("Can not query with a " + typeof query);
                case "object": case "undefined":
                       var queryObject = query;
                        query = function(object){
                                for(var key in queryObject){
                                        var required = queryObject[key];
                                        if(required && required.test){
                                                if(!required.test(object[key])){
                                                        return false;
                                                }
                                        }else if(required != object[key]){
                                                return false;
                                        }
                                }
                                return true;
                        };
                        break;
                case "string":
  /// HERE is most likely where you can play with the reqexp matcher.
                        // named query
                        if(!this[query]){
                                throw new Error("No filter function " + query + " was found in store");
                        }
                        query = this[query];
                        // fall through
                case "function":
                        // fall through
        }
        function execute(array){
                // execute the whole query, first we filter
                var results = arrayUtil.filter(array, query);
                // next we sort
                if(options && options.sort){
                        results.sort(function(a, b){
                                for(var sort, i=0; sort = options.sort[i]; i++){
                                        var aValue = a[sort.attribute];
                                        var bValue = b[sort.attribute];
                                        if (aValue != bValue) {
                                                return !!sort.descending == aValue > bValue ? -1 : 1;
                                        }
                                }
                                return 0;
                        });
                }
                // now we paginate
                if(options && (options.start || options.count)){
                        var total = results.length;
                        results = results.slice(options.start || 0, (options.start || 0) + (options.count || Infinity));
                        results.total = total;
                }
                return results;
        }
        execute.matches = query;
        return execute;
};

new Store( { queryEngine: MyEngine });

when execute.matches is set on bottom of this function, what happens is, that the string gets called on each item. Each item has a property - Select.searchAttr - which is tested by RegExp like so: new RegExp(query).test(item[searchAttr]); or maybe a bit simpler to understand; item[searchAttr].matches(query);

I have no testing environment, but locate the inline comment above and start using console.debug..

Example:

Stpre.data = [ 
   { id:'WS', name:  'Will F. Smith' },
   { id:'RD', name:'Robert O. Dinero' },
   { id:'CP', name:'Cle    O. Patra' }
];
Select.searchAttr = "name";
Select.value = "Robert Din"; // keyup->autocomplete->query

Select.query will become Select.queryExp.replace("${0]", Select.value), in your simple queryExp case, 'Robert Din'.. This will get fuzzy and it would be up to you to fill in the regular expression, here's something to start with

 query = query.substr(1,query.length-2); // '*' be gone
 var words = query.split(" ");
 var exp = "";
 dojo.forEach(words, function(word, idx) {
    // check if last word
    var nextWord = words[idx+1] ? words[idx+1] : null;
    // postfix 'match-all-but-first-letter-of-nextWord'
    exp += word + (nextWord ? "[^" + nextWord[0] + "]*" : "");
 });
 // exp should now be "Robert[^D]*Din";
 //  put back '*'
 query = '*' + exp + '*';