source: Dev/trunk/src/client/dijit/form/_SearchMixin.js

Last change on this file was 483, checked in by hendrikvanantwerpen, 11 years ago

Added Dojo 1.9.3 release.

File size: 9.4 KB
Line 
1define([
2        "dojo/_base/declare", // declare
3        "dojo/keys", // keys
4        "dojo/_base/lang", // lang.clone lang.hitch
5        "dojo/query", // query
6        "dojo/string", // string.substitute
7        "dojo/when",
8        "../registry"   // registry.byId
9], function(declare, keys, lang, query, string, when, registry){
10
11        // module:
12        //              dijit/form/_SearchMixin
13
14
15        return declare("dijit.form._SearchMixin", null, {
16                // summary:
17                //              A mixin that implements the base functionality to search a store based upon user-entered text such as
18                //              with `dijit/form/ComboBox` or `dijit/form/FilteringSelect`
19                // tags:
20                //              protected
21
22                // pageSize: Integer
23                //              Argument to data provider.
24                //              Specifies maximum number of search results to return per query
25                pageSize: Infinity,
26
27                // store: [const] dojo/store/api/Store
28                //              Reference to data provider object used by this ComboBox.
29                //              The store must accept an object hash of properties for its query. See `query` and `queryExpr` for details.
30                store: null,
31
32                // fetchProperties: Object
33                //              Mixin to the store's fetch.
34                //              For example, to set the sort order of the ComboBox menu, pass:
35                //      |       { sort: [{attribute:"name",descending: true}] }
36                //              To override the default queryOptions so that deep=false, do:
37                //      |       { queryOptions: {ignoreCase: true, deep: false} }
38                fetchProperties:{},
39
40                // query: Object
41                //              A query that can be passed to `store` to initially filter the items.
42                //              ComboBox overwrites any reference to the `searchAttr` and sets it to the `queryExpr` with the user's input substituted.
43                query: {},
44
45                // searchDelay: Integer
46                //              Delay in milliseconds between when user types something and we start
47                //              searching based on that value
48                searchDelay: 200,
49
50                // searchAttr: String
51                //              Search for items in the data store where this attribute (in the item)
52                //              matches what the user typed
53                searchAttr: "name",
54
55                // queryExpr: String
56                //              This specifies what query is sent to the data store,
57                //              based on what the user has typed.  Changing this expression will modify
58                //              whether the results are only exact matches, a "starting with" match,
59                //              etc.
60                //              `${0}` will be substituted for the user text.
61                //              `*` is used for wildcards.
62                //              `${0}*` means "starts with", `*${0}*` means "contains", `${0}` means "is"
63                queryExpr: "${0}*",
64
65                // ignoreCase: Boolean
66                //              Set true if the query should ignore case when matching possible items
67                ignoreCase: true,
68
69                _patternToRegExp: function(pattern){
70                        // summary:
71                        //              Helper function to convert a simple pattern to a regular expression for matching.
72                        // description:
73                        //              Returns a regular expression object that conforms to the defined conversion rules.
74                        //              For example:
75                        //
76                        //              - ca*   -> /^ca.*$/
77                        //              - *ca*  -> /^.*ca.*$/
78                        //              - *c\*a*  -> /^.*c\*a.*$/
79                        //              - *c\*a?*  -> /^.*c\*a..*$/
80                        //
81                        //              and so on.
82                        // pattern: string
83                        //              A simple matching pattern to convert that follows basic rules:
84                        //
85                        //              - * Means match anything, so ca* means match anything starting with ca
86                        //              - ? Means match single character.  So, b?b will match to bob and bab, and so on.
87                        //              - \ is an escape character.  So for example, \* means do not treat * as a match, but literal character *.
88                        //
89                        //              To use a \ as a character in the string, it must be escaped.  So in the pattern it should be
90                        //              represented by \\ to be treated as an ordinary \ character instead of an escape.
91
92                        return new RegExp("^" + pattern.replace(/(\\.)|(\*)|(\?)|\W/g, function(str, literal, star, question){
93                                return star ? ".*" : question ? "." : literal ? literal : "\\" + str;
94                        }) + "$", this.ignoreCase ? "mi" : "m");
95                },
96
97                _abortQuery: function(){
98                        // stop in-progress query
99                        if(this.searchTimer){
100                                this.searchTimer = this.searchTimer.remove();
101                        }
102                        if(this._queryDeferHandle){
103                                this._queryDeferHandle = this._queryDeferHandle.remove();
104                        }
105                        if(this._fetchHandle){
106                                if(this._fetchHandle.abort){
107                                        this._cancelingQuery = true;
108                                        this._fetchHandle.abort();
109                                        this._cancelingQuery = false;
110                                }
111                                if(this._fetchHandle.cancel){
112                                        this._cancelingQuery = true;
113                                        this._fetchHandle.cancel();
114                                        this._cancelingQuery = false;
115                                }
116                                this._fetchHandle = null;
117                        }
118                },
119
120                _processInput: function(/*Event*/ evt){
121                        // summary:
122                        //              Handles input (keyboard/paste) events
123                        if(this.disabled || this.readOnly){ return; }
124                        var key = evt.charOrCode;
125
126                        // except for cutting/pasting case - ctrl + x/v
127                        if("type" in evt && evt.type.substring(0,3) == "key" && (evt.altKey || ((evt.ctrlKey || evt.metaKey) && (key != 'x' && key != 'v')) || key == keys.SHIFT)){
128                                return; // throw out weird key combinations and spurious events
129                        }
130
131                        var doSearch = false;
132                        this._prev_key_backspace = false;
133
134                        switch(key){
135                                case keys.DELETE:
136                                case keys.BACKSPACE:
137                                        this._prev_key_backspace = true;
138                                        this._maskValidSubsetError = true;
139                                        doSearch = true;
140                                        break;
141
142                                default:
143                                        // Non char keys (F1-F12 etc..) shouldn't start a search..
144                                        // Ascii characters and IME input (Chinese, Japanese etc.) should.
145                                        //IME input produces keycode == 229.
146                                        doSearch = typeof key == 'string' || key == 229;
147                        }
148                        if(doSearch){
149                                // need to wait a tad before start search so that the event
150                                // bubbles through DOM and we have value visible
151                                if(!this.store){
152                                        this.onSearch();
153                                }else{
154                                        this.searchTimer = this.defer("_startSearchFromInput", 1);
155                                }
156                        }
157                },
158
159                onSearch: function(/*===== results, query, options =====*/){
160                        // summary:
161                        //              Callback when a search completes.
162                        //
163                        // results: Object
164                        //              An array of items from the originating _SearchMixin's store.
165                        //
166                        // query: Object
167                        //              A copy of the originating _SearchMixin's query property.
168                        //
169                        // options: Object
170                        //              The additional parameters sent to the originating _SearchMixin's store, including: start, count, queryOptions.
171                        //
172                        // tags:
173                        //              callback
174                },
175
176                _startSearchFromInput: function(){
177                        this._startSearch(this.focusNode.value);
178                },
179
180                _startSearch: function(/*String*/ text){
181                        // summary:
182                        //              Starts a search for elements matching text (text=="" means to return all items),
183                        //              and calls onSearch(...) when the search completes, to display the results.
184
185                        this._abortQuery();
186                        var
187                                _this = this,
188                                // Setup parameters to be passed to store.query().
189                                // Create a new query to prevent accidentally querying for a hidden
190                                // value from FilteringSelect's keyField
191                                query = lang.clone(this.query), // #5970
192                                options = {
193                                        start: 0,
194                                        count: this.pageSize,
195                                        queryOptions: {         // remove for 2.0
196                                                ignoreCase: this.ignoreCase,
197                                                deep: true
198                                        }
199                                },
200                                qs = string.substitute(this.queryExpr, [text.replace(/([\\\*\?])/g, "\\$1")]),
201                                q,
202                                startQuery = function(){
203                                        var resPromise = _this._fetchHandle = _this.store.query(query, options);
204                                        if(_this.disabled || _this.readOnly || (q !== _this._lastQuery)){
205                                                return;
206                                        } // avoid getting unwanted notify
207                                        when(resPromise, function(res){
208                                                _this._fetchHandle = null;
209                                                if(!_this.disabled && !_this.readOnly && (q === _this._lastQuery)){ // avoid getting unwanted notify
210                                                        when(resPromise.total, function(total){
211                                                                res.total = total;
212                                                                var pageSize = _this.pageSize;
213                                                                if(isNaN(pageSize) || pageSize > res.total){ pageSize = res.total; }
214                                                                // Setup method to fetching the next page of results
215                                                                res.nextPage = function(direction){
216                                                                        //      tell callback the direction of the paging so the screen
217                                                                        //      reader knows which menu option to shout
218                                                                        options.direction = direction = direction !== false;
219                                                                        options.count = pageSize;
220                                                                        if(direction){
221                                                                                options.start += res.length;
222                                                                                if(options.start >= res.total){
223                                                                                        options.count = 0;
224                                                                                }
225                                                                        }else{
226                                                                                options.start -= pageSize;
227                                                                                if(options.start < 0){
228                                                                                        options.count = Math.max(pageSize + options.start, 0);
229                                                                                        options.start = 0;
230                                                                                }
231                                                                        }
232                                                                        if(options.count <= 0){
233                                                                                res.length = 0;
234                                                                                _this.onSearch(res, query, options);
235                                                                        }else{
236                                                                                startQuery();
237                                                                        }
238                                                                };
239                                                                _this.onSearch(res, query, options);
240                                                        });
241                                                }
242                                        }, function(err){
243                                                _this._fetchHandle = null;
244                                                if(!_this._cancelingQuery){     // don't treat canceled query as an error
245                                                        console.error(_this.declaredClass + ' ' + err.toString());
246                                                }
247                                        });
248                                };
249
250                        lang.mixin(options, this.fetchProperties);
251
252                        // Generate query
253                        if(this.store._oldAPI){
254                                // remove this branch for 2.0
255                                q = qs;
256                        }else{
257                                // Query on searchAttr is a regex for benefit of dojo/store/Memory,
258                                // but with a toString() method to help dojo/store/JsonRest.
259                                // Search string like "Co*" converted to regex like /^Co.*$/i.
260                                q = this._patternToRegExp(qs);
261                                q.toString = function(){ return qs; };
262                        }
263
264                        // set _lastQuery, *then* start the timeout
265                        // otherwise, if the user types and the last query returns before the timeout,
266                        // _lastQuery won't be set and their input gets rewritten
267                        this._lastQuery = query[this.searchAttr] = q;
268                        this._queryDeferHandle = this.defer(startQuery, this.searchDelay);
269                },
270
271                //////////// INITIALIZATION METHODS ///////////////////////////////////////
272
273                constructor: function(){
274                        this.query={};
275                        this.fetchProperties={};
276                },
277
278                postMixInProperties: function(){
279                        if(!this.store){
280                                var list = this.list;
281                                if(list){
282                                        this.store = registry.byId(list);
283                                }
284                        }
285                        this.inherited(arguments);
286                }
287        });
288});
Note: See TracBrowser for help on using the repository browser.