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