define("dijit/form/FilteringSelect", [ "dojo/data/util/filter", // filter.patternToRegExp "dojo/_base/declare", // declare "dojo/_base/lang", // lang.mixin "dojo/when", "./MappedTextBox", "./ComboBoxMixin" ], function(filter, declare, lang, when, MappedTextBox, ComboBoxMixin){ // module: // dijit/form/FilteringSelect return declare("dijit.form.FilteringSelect", [MappedTextBox, ComboBoxMixin], { // summary: // An enhanced version of the HTML SELECT tag, populated dynamically // // description: // An enhanced version of the HTML SELECT tag, populated dynamically. It works // very nicely with very large data sets because it can load and page data as needed. // It also resembles ComboBox, but does not allow values outside of the provided ones. // If OPTION tags are used as the data provider via markup, then the // OPTION tag's child text node is used as the displayed value when selected // while the OPTION tag's value attribute is used as the widget value on form submit. // To set the default value when using OPTION tags, specify the selected // attribute on 1 of the child OPTION tags. // // Similar features: // // - There is a drop down list of possible values. // - You can only enter a value from the drop down list. (You can't // enter an arbitrary value.) // - The value submitted with the form is the hidden value (ex: CA), // not the displayed value a.k.a. label (ex: California) // // Enhancements over plain HTML version: // // - If you type in some text then it will filter down the list of // possible values in the drop down list. // - List can be specified either as a static list or via a javascript // function (that can get the list from a server) // required: Boolean // True (default) if user is required to enter a value into this field. required: true, _lastDisplayedValue: "", _isValidSubset: function(){ return this._opened; }, isValid: function(){ // Overrides ValidationTextBox.isValid() return !!this.item || (!this.required && this.get('displayedValue') == ""); // #5974 }, _refreshState: function(){ if(!this.searchTimer){ // state will be refreshed after results are returned this.inherited(arguments); } }, _callbackSetLabel: function( /*Array*/ result, /*Object*/ query, /*Object*/ options, /*Boolean?*/ priorityChange){ // summary: // Callback from dojo.store after lookup of user entered value finishes // setValue does a synchronous lookup, // so it calls _callbackSetLabel directly, // and so does not pass dataObject // still need to test against _lastQuery in case it came too late if((query && query[this.searchAttr] !== this._lastQuery) || (!query && result.length && this.store.getIdentity(result[0]) != this._lastQuery)){ return; } if(!result.length){ //#3268: don't modify display value on bad input //#3285: change CSS to indicate error this.set("value", '', priorityChange || (priorityChange === undefined && !this.focused), this.textbox.value, null); }else{ this.set('item', result[0], priorityChange); } }, _openResultList: function(/*Object*/ results, /*Object*/ query, /*Object*/ options){ // Callback when a data store query completes. // Overrides ComboBox._openResultList() // #3285: tap into search callback to see if user's query resembles a match if(query[this.searchAttr] !== this._lastQuery){ return; } this.inherited(arguments); if(this.item === undefined){ // item == undefined for keyboard search // If the search returned no items that means that the user typed // in something invalid (and they can't make it valid by typing more characters), // so flag the FilteringSelect as being in an invalid state this.validate(true); } }, _getValueAttr: function(){ // summary: // Hook for get('value') to work. // don't get the textbox value but rather the previously set hidden value. // Use this.valueNode.value which isn't always set for other MappedTextBox widgets until blur return this.valueNode.value; }, _getValueField: function(){ // Overrides ComboBox._getValueField() return "value"; }, _setValueAttr: function(/*String*/ value, /*Boolean?*/ priorityChange, /*String?*/ displayedValue, /*item?*/ item){ // summary: // Hook so set('value', value) works. // description: // Sets the value of the select. // Also sets the label to the corresponding value by reverse lookup. if(!this._onChangeActive){ priorityChange = null; } if(item === undefined){ if(value === null || value === ''){ value = ''; if(!lang.isString(displayedValue)){ this._setDisplayedValueAttr(displayedValue||'', priorityChange); return; } } var self = this; this._lastQuery = value; when(this.store.get(value), function(item){ self._callbackSetLabel(item? [item] : [], undefined, undefined, priorityChange); }); }else{ this.valueNode.value = value; this.inherited(arguments); } }, _setItemAttr: function(/*item*/ item, /*Boolean?*/ priorityChange, /*String?*/ displayedValue){ // summary: // Set the displayed valued in the input box, and the hidden value // that gets submitted, based on a dojo.data store item. // description: // Users shouldn't call this function; they should be calling // set('item', value) // tags: // private this.inherited(arguments); this._lastDisplayedValue = this.textbox.value; }, _getDisplayQueryString: function(/*String*/ text){ return text.replace(/([\\\*\?])/g, "\\$1"); }, _setDisplayedValueAttr: function(/*String*/ label, /*Boolean?*/ priorityChange){ // summary: // Hook so set('displayedValue', label) works. // description: // Sets textbox to display label. Also performs reverse lookup // to set the hidden value. label should corresponding to item.searchAttr. if(label == null){ label = ''; } // This is called at initialization along with every custom setter. // Usually (or always?) the call can be ignored. If it needs to be // processed then at least make sure that the XHR request doesn't trigger an onChange() // event, even if it returns after creation has finished if(!this._created){ if(!("displayedValue" in this.params)){ return; } priorityChange = false; } // Do a reverse lookup to map the specified displayedValue to the hidden value. // Note that if there's a custom labelFunc() this code if(this.store){ this.closeDropDown(); var query = lang.clone(this.query); // #6196: populate query with user-specifics // Generate query var qs = this._getDisplayQueryString(label), q; 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; }; } this._lastQuery = query[this.searchAttr] = q; // If the label is not valid, the callback will never set it, // so the last valid value will get the warning textbox. Set the // textbox value now so that the impending warning will make // sense to the user this.textbox.value = label; this._lastDisplayedValue = label; this._set("displayedValue", label); // for watch("displayedValue") notification var _this = this; var options = { ignoreCase: this.ignoreCase, deep: true }; lang.mixin(options, this.fetchProperties); this._fetchHandle = this.store.query(query, options); when(this._fetchHandle, function(result){ _this._fetchHandle = null; _this._callbackSetLabel(result || [], query, options, priorityChange); }, function(err){ _this._fetchHandle = null; if(!_this._cancelingQuery){ // don't treat canceled query as an error console.error('dijit.form.FilteringSelect: ' + err.toString()); } }); } }, undo: function(){ this.set('displayedValue', this._lastDisplayedValue); } }); });