1 | define([ |
---|
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 | }); |
---|