You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
279 lines
9.0 KiB
279 lines
9.0 KiB
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); |
|
} |
|
}); |
|
});
|
|
|