define("dijit/form/_SearchMixin", [ "dojo/data/util/filter", // patternToRegExp "dojo/_base/declare", // declare "dojo/_base/event", // event.stop "dojo/keys", // keys "dojo/_base/lang", // lang.clone lang.hitch "dojo/query", // query "dojo/sniff", // has("ie") "dojo/string", // string.substitute "dojo/when", "../registry" // registry.byId ], function(filter, declare, event, keys, lang, query, has, string, when, registry){ // module: // dijit/form/_SearchMixin return declare("dijit.form._SearchMixin", null, { // summary: // A mixin that implements the base functionality to search a store based upon user-entered text such as // with `dijit/form/ComboBox` or `dijit/form/FilteringSelect` // tags: // protected // pageSize: Integer // Argument to data provider. // Specifies maximum number of search results to return per query pageSize: Infinity, // store: [const] dojo/store/api/Store // Reference to data provider object used by this ComboBox. // The store must accept an object hash of properties for its query. See `query` and `queryExpr` for details. store: null, // fetchProperties: Object // Mixin to the store's fetch. // For example, to set the sort order of the ComboBox menu, pass: // | { sort: [{attribute:"name",descending: true}] } // To override the default queryOptions so that deep=false, do: // | { queryOptions: {ignoreCase: true, deep: false} } fetchProperties:{}, // query: Object // A query that can be passed to `store` to initially filter the items. // ComboBox overwrites any reference to the `searchAttr` and sets it to the `queryExpr` with the user's input substituted. query: {}, // searchDelay: Integer // Delay in milliseconds between when user types something and we start // searching based on that value searchDelay: 200, // searchAttr: String // Search for items in the data store where this attribute (in the item) // matches what the user typed searchAttr: "name", // queryExpr: String // This specifies what query is sent to the data store, // based on what the user has typed. Changing this expression will modify // whether the results are only exact matches, a "starting with" match, // etc. // dojo.data query expression pattern. // `${0}` will be substituted for the user text. // `*` is used for wildcards. // `${0}*` means "starts with", `*${0}*` means "contains", `${0}` means "is" queryExpr: "${0}*", // ignoreCase: Boolean // Set true if the query should ignore case when matching possible items ignoreCase: true, _abortQuery: function(){ // stop in-progress query if(this.searchTimer){ this.searchTimer = this.searchTimer.remove(); } if(this._queryDeferHandle){ this._queryDeferHandle = this._queryDeferHandle.remove(); } if(this._fetchHandle){ if(this._fetchHandle.abort){ this._cancelingQuery = true; this._fetchHandle.abort(); this._cancelingQuery = false; } if(this._fetchHandle.cancel){ this._cancelingQuery = true; this._fetchHandle.cancel(); this._cancelingQuery = false; } this._fetchHandle = null; } }, _processInput: function(/*Event*/ evt){ // summary: // Handles input (keyboard/paste) events if(this.disabled || this.readOnly){ return; } var key = evt.charOrCode; // except for cutting/pasting case - ctrl + x/v if(evt.altKey || ((evt.ctrlKey || evt.metaKey) && (key != 'x' && key != 'v')) || key == keys.SHIFT){ return; // throw out weird key combinations and spurious events } var doSearch = false; this._prev_key_backspace = false; switch(key){ case keys.DELETE: case keys.BACKSPACE: this._prev_key_backspace = true; this._maskValidSubsetError = true; doSearch = true; break; default: // Non char keys (F1-F12 etc..) shouldn't start a search.. // Ascii characters and IME input (Chinese, Japanese etc.) should. //IME input produces keycode == 229. doSearch = typeof key == 'string' || key == 229; } if(doSearch){ // need to wait a tad before start search so that the event // bubbles through DOM and we have value visible if(!this.store){ this.onSearch(); }else{ this.searchTimer = this.defer("_startSearchFromInput", 1); } } }, onSearch: function(/*===== results, query, options =====*/){ // summary: // Callback when a search completes. // // results: Object // An array of items from the originating _SearchMixin's store. // // query: Object // A copy of the originating _SearchMixin's query property. // // options: Object // The additional parameters sent to the originating _SearchMixin's store, including: start, count, queryOptions. // // tags: // callback }, _startSearchFromInput: function(){ this._startSearch(this.focusNode.value.replace(/([\\\*\?])/g, "\\$1")); }, _startSearch: function(/*String*/ text){ // summary: // Starts a search for elements matching text (text=="" means to return all items), // and calls onSearch(...) when the search completes, to display the results. this._abortQuery(); var _this = this, // Setup parameters to be passed to store.query(). // Create a new query to prevent accidentally querying for a hidden // value from FilteringSelect's keyField query = lang.clone(this.query), // #5970 options = { start: 0, count: this.pageSize, queryOptions: { // remove for 2.0 ignoreCase: this.ignoreCase, deep: true } }, qs = string.substitute(this.queryExpr, [text]), q, startQuery = function(){ var resPromise = _this._fetchHandle = _this.store.query(query, options); if(_this.disabled || _this.readOnly || (q !== _this._lastQuery)){ return; } // avoid getting unwanted notify when(resPromise, function(res){ _this._fetchHandle = null; if(!_this.disabled && !_this.readOnly && (q === _this._lastQuery)){ // avoid getting unwanted notify when(resPromise.total, function(total){ res.total = total; var pageSize = _this.pageSize; if(isNaN(pageSize) || pageSize > res.total){ pageSize = res.total; } // Setup method to fetching the next page of results res.nextPage = function(direction){ // tell callback the direction of the paging so the screen // reader knows which menu option to shout options.direction = direction = direction !== false; options.count = pageSize; if(direction){ options.start += res.length; if(options.start >= res.total){ options.count = 0; } }else{ options.start -= pageSize; if(options.start < 0){ options.count = Math.max(pageSize + options.start, 0); options.start = 0; } } if(options.count <= 0){ res.length = 0; _this.onSearch(res, query, options); }else{ startQuery(); } }; _this.onSearch(res, query, options); }); } }, function(err){ _this._fetchHandle = null; if(!_this._cancelingQuery){ // don't treat canceled query as an error console.error(_this.declaredClass + ' ' + err.toString()); } }); }; lang.mixin(options, this.fetchProperties); // Generate query if(this.store._oldAPI){ // remove this branch for 2.0 q = qs; }else{ // Query on searchAttr is a regex for benefit of dojo/store/Memory, // but with a toString() method to help dojo/store/JsonRest. // Search string like "Co*" converted to regex like /^Co.*$/i. q = filter.patternToRegExp(qs, this.ignoreCase); q.toString = function(){ return qs; }; } // set _lastQuery, *then* start the timeout // otherwise, if the user types and the last query returns before the timeout, // _lastQuery won't be set and their input gets rewritten this._lastQuery = query[this.searchAttr] = q; this._queryDeferHandle = this.defer(startQuery, this.searchDelay); }, //////////// INITIALIZATION METHODS /////////////////////////////////////// constructor: function(){ this.query={}; this.fetchProperties={}; }, postMixInProperties: function(){ if(!this.store){ var list = this.list; if(list){ this.store = registry.byId(list); } } this.inherited(arguments); } }); });