265 lines
8.3 KiB
JavaScript
265 lines
8.3 KiB
JavaScript
|
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);
|
||
|
}
|
||
|
});
|
||
|
});
|