-
Notifications
You must be signed in to change notification settings - Fork 186
/
Copy path_SearchMixin.js
279 lines (250 loc) · 9.03 KB
/
_SearchMixin.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
define([
"dojo/_base/declare", // declare
"dojo/keys", // keys
"dojo/_base/lang", // lang.clone lang.hitch
"dojo/query", // query
"dojo/string", // string.substitute
"dojo/when",
"../registry" // registry.byId
], function(declare, keys, lang, query, 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: {},
// list: [const] String
// Alternate to specifying a store. Id of a dijit/form/DataList widget.
list: "",
_setListAttr: function(list){
// Avoid having list applied to the DOM node, since it has native meaning in modern browsers
this._set("list", list);
},
// 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.
// `${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,
_patternToRegExp: function(pattern){
// summary:
// Helper function to convert a simple pattern to a regular expression for matching.
// description:
// Returns a regular expression object that conforms to the defined conversion rules.
// For example:
//
// - ca* -> /^ca.*$/
// - *ca* -> /^.*ca.*$/
// - *c\*a* -> /^.*c\*a.*$/
// - *c\*a?* -> /^.*c\*a..*$/
//
// and so on.
// pattern: string
// A simple matching pattern to convert that follows basic rules:
//
// - * Means match anything, so ca* means match anything starting with ca
// - ? Means match single character. So, b?b will match to bob and bab, and so on.
// - \ is an escape character. So for example, \* means do not treat * as a match, but literal character *.
//
// To use a \ as a character in the string, it must be escaped. So in the pattern it should be
// represented by \\ to be treated as an ordinary \ character instead of an escape.
return new RegExp("^" + pattern.replace(/(\\.)|(\*)|(\?)|\W/g, function(str, literal, star, question){
return star ? ".*" : question ? "." : literal ? literal : "\\" + str;
}) + "$", this.ignoreCase ? "mi" : "m");
},
_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;
this._prev_key_backspace = false;
if (key === keys.DELETE || key === keys.BACKSPACE) {
this._prev_key_backspace = true;
this._maskValidSubsetError = true;
}
// 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);
},
_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.replace(/([\\\*\?])/g, "\\$1")]),
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 = this._patternToRegExp(qs);
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);
}
});
});