766 lines
26 KiB
JavaScript
766 lines
26 KiB
JavaScript
|
//>>built
|
||
|
define("dijit/form/_AutoCompleterMixin", [
|
||
|
"dojo/_base/connect", // keys keys.SHIFT
|
||
|
"dojo/data/util/filter", // patternToRegExp
|
||
|
"dojo/_base/declare", // declare
|
||
|
"dojo/_base/Deferred", // Deferred.when
|
||
|
"dojo/dom-attr", // domAttr.get
|
||
|
"dojo/_base/event", // event.stop
|
||
|
"dojo/keys",
|
||
|
"dojo/_base/lang", // lang.clone lang.hitch
|
||
|
"dojo/query", // query
|
||
|
"dojo/regexp", // regexp.escapeString
|
||
|
"dojo/_base/sniff", // has("ie")
|
||
|
"dojo/string", // string.substitute
|
||
|
"dojo/_base/window", // win.doc.selection.createRange
|
||
|
"./DataList",
|
||
|
"../registry", // registry.byId
|
||
|
"./_TextBoxMixin" // defines _TextBoxMixin.selectInputText
|
||
|
], function(connect, filter, declare, Deferred, domAttr, event, keys, lang, query, regexp, has, string, win,
|
||
|
DataList, registry, _TextBoxMixin){
|
||
|
|
||
|
// module:
|
||
|
// dijit/form/_AutoCompleterMixin
|
||
|
// summary:
|
||
|
// A mixin that implements the base functionality for `dijit.form.ComboBox`/`dijit.form.FilteringSelect`
|
||
|
|
||
|
|
||
|
return declare("dijit.form._AutoCompleterMixin", null, {
|
||
|
// summary:
|
||
|
// A mixin that implements the base functionality for `dijit.form.ComboBox`/`dijit.form.FilteringSelect`
|
||
|
// description:
|
||
|
// All widgets that mix in dijit.form._AutoCompleterMixin must extend `dijit.form._FormValueWidget`.
|
||
|
// tags:
|
||
|
// protected
|
||
|
|
||
|
// item: Object
|
||
|
// This is the item returned by the dojo.data.store implementation that
|
||
|
// provides the data for this ComboBox, it's the currently selected item.
|
||
|
item: null,
|
||
|
|
||
|
// pageSize: Integer
|
||
|
// Argument to data provider.
|
||
|
// Specifies number of search results per page (before hitting "next" button)
|
||
|
pageSize: Infinity,
|
||
|
|
||
|
// store: [const] dojo.store.api.Store
|
||
|
// Reference to data provider object used by this ComboBox
|
||
|
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,
|
||
|
// before doing further filtering based on `searchAttr` and the key.
|
||
|
// Any reference to the `searchAttr` is ignored.
|
||
|
query: {},
|
||
|
|
||
|
// autoComplete: Boolean
|
||
|
// If user types in a partial string, and then tab out of the `<input>` box,
|
||
|
// automatically copy the first entry displayed in the drop down list to
|
||
|
// the `<input>` field
|
||
|
autoComplete: true,
|
||
|
|
||
|
// highlightMatch: String
|
||
|
// One of: "first", "all" or "none".
|
||
|
//
|
||
|
// If the ComboBox/FilteringSelect opens with the search results and the searched
|
||
|
// string can be found, it will be highlighted. If set to "all"
|
||
|
// then will probably want to change `queryExpr` parameter to '*${0}*'
|
||
|
//
|
||
|
// Highlighting is only performed when `labelType` is "text", so as to not
|
||
|
// interfere with any HTML markup an HTML label might contain.
|
||
|
highlightMatch: "first",
|
||
|
|
||
|
// searchDelay: Integer
|
||
|
// Delay in milliseconds between when user types something and we start
|
||
|
// searching based on that value
|
||
|
searchDelay: 100,
|
||
|
|
||
|
// searchAttr: String
|
||
|
// Search for items in the data store where this attribute (in the item)
|
||
|
// matches what the user typed
|
||
|
searchAttr: "name",
|
||
|
|
||
|
// labelAttr: String?
|
||
|
// The entries in the drop down list come from this attribute in the
|
||
|
// dojo.data items.
|
||
|
// If not specified, the searchAttr attribute is used instead.
|
||
|
labelAttr: "",
|
||
|
|
||
|
// labelType: String
|
||
|
// Specifies how to interpret the labelAttr in the data store items.
|
||
|
// Can be "html" or "text".
|
||
|
labelType: "text",
|
||
|
|
||
|
// queryExpr: String
|
||
|
// This specifies what query ComboBox/FilteringSelect sends to the data store,
|
||
|
// based on what the user has typed. Changing this expression will modify
|
||
|
// whether the drop down shows only exact matches, a "starting with" match,
|
||
|
// etc. Use it in conjunction with highlightMatch.
|
||
|
// 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 ComboBox/FilteringSelect should ignore case when matching possible items
|
||
|
ignoreCase: true,
|
||
|
|
||
|
// Flags to _HasDropDown to limit height of drop down to make it fit in viewport
|
||
|
maxHeight: -1,
|
||
|
|
||
|
// For backwards compatibility let onClick events propagate, even clicks on the down arrow button
|
||
|
_stopClickEvents: false,
|
||
|
|
||
|
_getCaretPos: function(/*DomNode*/ element){
|
||
|
// khtml 3.5.2 has selection* methods as does webkit nightlies from 2005-06-22
|
||
|
var pos = 0;
|
||
|
if(typeof(element.selectionStart) == "number"){
|
||
|
// FIXME: this is totally borked on Moz < 1.3. Any recourse?
|
||
|
pos = element.selectionStart;
|
||
|
}else if(has("ie")){
|
||
|
// in the case of a mouse click in a popup being handled,
|
||
|
// then the win.doc.selection is not the textarea, but the popup
|
||
|
// var r = win.doc.selection.createRange();
|
||
|
// hack to get IE 6 to play nice. What a POS browser.
|
||
|
var tr = win.doc.selection.createRange().duplicate();
|
||
|
var ntr = element.createTextRange();
|
||
|
tr.move("character",0);
|
||
|
ntr.move("character",0);
|
||
|
try{
|
||
|
// If control doesn't have focus, you get an exception.
|
||
|
// Seems to happen on reverse-tab, but can also happen on tab (seems to be a race condition - only happens sometimes).
|
||
|
// There appears to be no workaround for this - googled for quite a while.
|
||
|
ntr.setEndPoint("EndToEnd", tr);
|
||
|
pos = String(ntr.text).replace(/\r/g,"").length;
|
||
|
}catch(e){
|
||
|
// If focus has shifted, 0 is fine for caret pos.
|
||
|
}
|
||
|
}
|
||
|
return pos;
|
||
|
},
|
||
|
|
||
|
_setCaretPos: function(/*DomNode*/ element, /*Number*/ location){
|
||
|
location = parseInt(location);
|
||
|
_TextBoxMixin.selectInputText(element, location, location);
|
||
|
},
|
||
|
|
||
|
_setDisabledAttr: function(/*Boolean*/ value){
|
||
|
// Additional code to set disabled state of ComboBox node.
|
||
|
// Overrides _FormValueWidget._setDisabledAttr() or ValidationTextBox._setDisabledAttr().
|
||
|
this.inherited(arguments);
|
||
|
this.domNode.setAttribute("aria-disabled", value);
|
||
|
},
|
||
|
|
||
|
_abortQuery: function(){
|
||
|
// stop in-progress query
|
||
|
if(this.searchTimer){
|
||
|
clearTimeout(this.searchTimer);
|
||
|
this.searchTimer = null;
|
||
|
}
|
||
|
if(this._fetchHandle){
|
||
|
if(this._fetchHandle.cancel){
|
||
|
this._cancelingQuery = true;
|
||
|
this._fetchHandle.cancel();
|
||
|
this._cancelingQuery = false;
|
||
|
}
|
||
|
this._fetchHandle = null;
|
||
|
}
|
||
|
},
|
||
|
|
||
|
_onInput: function(/*Event*/ evt){
|
||
|
// summary:
|
||
|
// Handles paste events
|
||
|
this.inherited(arguments);
|
||
|
if(evt.charOrCode == 229){ // IME or cut/paste event
|
||
|
this._onKey(evt);
|
||
|
}
|
||
|
},
|
||
|
|
||
|
_onKey: function(/*Event*/ evt){
|
||
|
// summary:
|
||
|
// Handles keyboard events
|
||
|
|
||
|
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;
|
||
|
var pw = this.dropDown;
|
||
|
var highlighted = null;
|
||
|
this._prev_key_backspace = false;
|
||
|
this._abortQuery();
|
||
|
|
||
|
// _HasDropDown will do some of the work:
|
||
|
// 1. when drop down is not yet shown:
|
||
|
// - if user presses the down arrow key, call loadDropDown()
|
||
|
// 2. when drop down is already displayed:
|
||
|
// - on ESC key, call closeDropDown()
|
||
|
// - otherwise, call dropDown.handleKey() to process the keystroke
|
||
|
this.inherited(arguments);
|
||
|
|
||
|
if(this._opened){
|
||
|
highlighted = pw.getHighlightedOption();
|
||
|
}
|
||
|
switch(key){
|
||
|
case keys.PAGE_DOWN:
|
||
|
case keys.DOWN_ARROW:
|
||
|
case keys.PAGE_UP:
|
||
|
case keys.UP_ARROW:
|
||
|
// Keystroke caused ComboBox_menu to move to a different item.
|
||
|
// Copy new item to <input> box.
|
||
|
if(this._opened){
|
||
|
this._announceOption(highlighted);
|
||
|
}
|
||
|
event.stop(evt);
|
||
|
break;
|
||
|
|
||
|
case keys.ENTER:
|
||
|
// prevent submitting form if user presses enter. Also
|
||
|
// prevent accepting the value if either Next or Previous
|
||
|
// are selected
|
||
|
if(highlighted){
|
||
|
// only stop event on prev/next
|
||
|
if(highlighted == pw.nextButton){
|
||
|
this._nextSearch(1);
|
||
|
event.stop(evt);
|
||
|
break;
|
||
|
}else if(highlighted == pw.previousButton){
|
||
|
this._nextSearch(-1);
|
||
|
event.stop(evt);
|
||
|
break;
|
||
|
}
|
||
|
}else{
|
||
|
// Update 'value' (ex: KY) according to currently displayed text
|
||
|
this._setBlurValue(); // set value if needed
|
||
|
this._setCaretPos(this.focusNode, this.focusNode.value.length); // move cursor to end and cancel highlighting
|
||
|
}
|
||
|
// default case:
|
||
|
// if enter pressed while drop down is open, or for FilteringSelect,
|
||
|
// if we are in the middle of a query to convert a directly typed in value to an item,
|
||
|
// prevent submit
|
||
|
if(this._opened || this._fetchHandle){
|
||
|
event.stop(evt);
|
||
|
}
|
||
|
// fall through
|
||
|
|
||
|
case keys.TAB:
|
||
|
var newvalue = this.get('displayedValue');
|
||
|
// if the user had More Choices selected fall into the
|
||
|
// _onBlur handler
|
||
|
if(pw && (
|
||
|
newvalue == pw._messages["previousMessage"] ||
|
||
|
newvalue == pw._messages["nextMessage"])
|
||
|
){
|
||
|
break;
|
||
|
}
|
||
|
if(highlighted){
|
||
|
this._selectOption(highlighted);
|
||
|
}
|
||
|
// fall through
|
||
|
|
||
|
case keys.ESCAPE:
|
||
|
if(this._opened){
|
||
|
this._lastQuery = null; // in case results come back later
|
||
|
this.closeDropDown();
|
||
|
}
|
||
|
break;
|
||
|
|
||
|
case ' ':
|
||
|
if(highlighted){
|
||
|
// user is effectively clicking a choice in the drop down menu
|
||
|
event.stop(evt);
|
||
|
this._selectOption(highlighted);
|
||
|
this.closeDropDown();
|
||
|
}else{
|
||
|
// user typed a space into the input box, treat as normal character
|
||
|
doSearch = true;
|
||
|
}
|
||
|
break;
|
||
|
|
||
|
case keys.DELETE:
|
||
|
case keys.BACKSPACE:
|
||
|
this._prev_key_backspace = true;
|
||
|
doSearch = true;
|
||
|
break;
|
||
|
|
||
|
default:
|
||
|
// Non char keys (F1-F12 etc..) shouldn't open list.
|
||
|
// 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
|
||
|
this.item = undefined; // undefined means item needs to be set
|
||
|
this.searchTimer = setTimeout(lang.hitch(this, "_startSearchFromInput"),1);
|
||
|
}
|
||
|
},
|
||
|
|
||
|
_autoCompleteText: function(/*String*/ text){
|
||
|
// summary:
|
||
|
// Fill in the textbox with the first item from the drop down
|
||
|
// list, and highlight the characters that were
|
||
|
// auto-completed. For example, if user typed "CA" and the
|
||
|
// drop down list appeared, the textbox would be changed to
|
||
|
// "California" and "ifornia" would be highlighted.
|
||
|
|
||
|
var fn = this.focusNode;
|
||
|
|
||
|
// IE7: clear selection so next highlight works all the time
|
||
|
_TextBoxMixin.selectInputText(fn, fn.value.length);
|
||
|
// does text autoComplete the value in the textbox?
|
||
|
var caseFilter = this.ignoreCase? 'toLowerCase' : 'substr';
|
||
|
if(text[caseFilter](0).indexOf(this.focusNode.value[caseFilter](0)) == 0){
|
||
|
var cpos = this.autoComplete ? this._getCaretPos(fn) : fn.value.length;
|
||
|
// only try to extend if we added the last character at the end of the input
|
||
|
if((cpos+1) > fn.value.length){
|
||
|
// only add to input node as we would overwrite Capitalisation of chars
|
||
|
// actually, that is ok
|
||
|
fn.value = text;//.substr(cpos);
|
||
|
// visually highlight the autocompleted characters
|
||
|
_TextBoxMixin.selectInputText(fn, cpos);
|
||
|
}
|
||
|
}else{
|
||
|
// text does not autoComplete; replace the whole value and highlight
|
||
|
fn.value = text;
|
||
|
_TextBoxMixin.selectInputText(fn);
|
||
|
}
|
||
|
},
|
||
|
|
||
|
_openResultList: function(/*Object*/ results, /*Object*/ query, /*Object*/ options){
|
||
|
// summary:
|
||
|
// Callback when a search completes.
|
||
|
// description:
|
||
|
// 1. generates drop-down list and calls _showResultList() to display it
|
||
|
// 2. if this result list is from user pressing "more choices"/"previous choices"
|
||
|
// then tell screen reader to announce new option
|
||
|
this._fetchHandle = null;
|
||
|
if( this.disabled ||
|
||
|
this.readOnly ||
|
||
|
(query[this.searchAttr] !== this._lastQuery) // TODO: better way to avoid getting unwanted notify
|
||
|
){
|
||
|
return;
|
||
|
}
|
||
|
var wasSelected = this.dropDown.getHighlightedOption();
|
||
|
this.dropDown.clearResultList();
|
||
|
if(!results.length && options.start == 0){ // if no results and not just the previous choices button
|
||
|
this.closeDropDown();
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// Fill in the textbox with the first item from the drop down list,
|
||
|
// and highlight the characters that were auto-completed. For
|
||
|
// example, if user typed "CA" and the drop down list appeared, the
|
||
|
// textbox would be changed to "California" and "ifornia" would be
|
||
|
// highlighted.
|
||
|
|
||
|
var nodes = this.dropDown.createOptions(
|
||
|
results,
|
||
|
options,
|
||
|
lang.hitch(this, "_getMenuLabelFromItem")
|
||
|
);
|
||
|
|
||
|
// show our list (only if we have content, else nothing)
|
||
|
this._showResultList();
|
||
|
|
||
|
// #4091:
|
||
|
// tell the screen reader that the paging callback finished by
|
||
|
// shouting the next choice
|
||
|
if(options.direction){
|
||
|
if(1 == options.direction){
|
||
|
this.dropDown.highlightFirstOption();
|
||
|
}else if(-1 == options.direction){
|
||
|
this.dropDown.highlightLastOption();
|
||
|
}
|
||
|
if(wasSelected){
|
||
|
this._announceOption(this.dropDown.getHighlightedOption());
|
||
|
}
|
||
|
}else if(this.autoComplete && !this._prev_key_backspace
|
||
|
// when the user clicks the arrow button to show the full list,
|
||
|
// startSearch looks for "*".
|
||
|
// it does not make sense to autocomplete
|
||
|
// if they are just previewing the options available.
|
||
|
&& !/^[*]+$/.test(query[this.searchAttr].toString())){
|
||
|
this._announceOption(nodes[1]); // 1st real item
|
||
|
}
|
||
|
},
|
||
|
|
||
|
_showResultList: function(){
|
||
|
// summary:
|
||
|
// Display the drop down if not already displayed, or if it is displayed, then
|
||
|
// reposition it if necessary (reposition may be necessary if drop down's height changed).
|
||
|
this.closeDropDown(true);
|
||
|
this.openDropDown();
|
||
|
this.domNode.setAttribute("aria-expanded", "true");
|
||
|
},
|
||
|
|
||
|
loadDropDown: function(/*Function*/ /*===== callback =====*/){
|
||
|
// Overrides _HasDropDown.loadDropDown().
|
||
|
// This is called when user has pressed button icon or pressed the down arrow key
|
||
|
// to open the drop down.
|
||
|
|
||
|
this._startSearchAll();
|
||
|
},
|
||
|
|
||
|
isLoaded: function(){
|
||
|
// signal to _HasDropDown that it needs to call loadDropDown() to load the
|
||
|
// drop down asynchronously before displaying it
|
||
|
return false;
|
||
|
},
|
||
|
|
||
|
closeDropDown: function(){
|
||
|
// Overrides _HasDropDown.closeDropDown(). Closes the drop down (assuming that it's open).
|
||
|
// This method is the callback when the user types ESC or clicking
|
||
|
// the button icon while the drop down is open. It's also called by other code.
|
||
|
this._abortQuery();
|
||
|
if(this._opened){
|
||
|
this.inherited(arguments);
|
||
|
this.domNode.setAttribute("aria-expanded", "false");
|
||
|
this.focusNode.removeAttribute("aria-activedescendant");
|
||
|
}
|
||
|
},
|
||
|
|
||
|
_setBlurValue: function(){
|
||
|
// if the user clicks away from the textbox OR tabs away, set the
|
||
|
// value to the textbox value
|
||
|
// #4617:
|
||
|
// if value is now more choices or previous choices, revert
|
||
|
// the value
|
||
|
var newvalue = this.get('displayedValue');
|
||
|
var pw = this.dropDown;
|
||
|
if(pw && (
|
||
|
newvalue == pw._messages["previousMessage"] ||
|
||
|
newvalue == pw._messages["nextMessage"]
|
||
|
)
|
||
|
){
|
||
|
this._setValueAttr(this._lastValueReported, true);
|
||
|
}else if(typeof this.item == "undefined"){
|
||
|
// Update 'value' (ex: KY) according to currently displayed text
|
||
|
this.item = null;
|
||
|
this.set('displayedValue', newvalue);
|
||
|
}else{
|
||
|
if(this.value != this._lastValueReported){
|
||
|
this._handleOnChange(this.value, true);
|
||
|
}
|
||
|
this._refreshState();
|
||
|
}
|
||
|
},
|
||
|
|
||
|
_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
|
||
|
var value = '';
|
||
|
if(item){
|
||
|
if(!displayedValue){
|
||
|
displayedValue = this.store._oldAPI ? // remove getValue() for 2.0 (old dojo.data API)
|
||
|
this.store.getValue(item, this.searchAttr) : item[this.searchAttr];
|
||
|
}
|
||
|
value = this._getValueField() != this.searchAttr ? this.store.getIdentity(item) : displayedValue;
|
||
|
}
|
||
|
this.set('value', value, priorityChange, displayedValue, item);
|
||
|
},
|
||
|
|
||
|
_announceOption: function(/*Node*/ node){
|
||
|
// summary:
|
||
|
// a11y code that puts the highlighted option in the textbox.
|
||
|
// This way screen readers will know what is happening in the
|
||
|
// menu.
|
||
|
|
||
|
if(!node){
|
||
|
return;
|
||
|
}
|
||
|
// pull the text value from the item attached to the DOM node
|
||
|
var newValue;
|
||
|
if(node == this.dropDown.nextButton ||
|
||
|
node == this.dropDown.previousButton){
|
||
|
newValue = node.innerHTML;
|
||
|
this.item = undefined;
|
||
|
this.value = '';
|
||
|
}else{
|
||
|
newValue = (this.store._oldAPI ? // remove getValue() for 2.0 (old dojo.data API)
|
||
|
this.store.getValue(node.item, this.searchAttr) : node.item[this.searchAttr]).toString();
|
||
|
this.set('item', node.item, false, newValue);
|
||
|
}
|
||
|
// get the text that the user manually entered (cut off autocompleted text)
|
||
|
this.focusNode.value = this.focusNode.value.substring(0, this._lastInput.length);
|
||
|
// set up ARIA activedescendant
|
||
|
this.focusNode.setAttribute("aria-activedescendant", domAttr.get(node, "id"));
|
||
|
// autocomplete the rest of the option to announce change
|
||
|
this._autoCompleteText(newValue);
|
||
|
},
|
||
|
|
||
|
_selectOption: function(/*DomNode*/ target){
|
||
|
// summary:
|
||
|
// Menu callback function, called when an item in the menu is selected.
|
||
|
this.closeDropDown();
|
||
|
if(target){
|
||
|
this._announceOption(target);
|
||
|
}
|
||
|
this._setCaretPos(this.focusNode, this.focusNode.value.length);
|
||
|
this._handleOnChange(this.value, true);
|
||
|
},
|
||
|
|
||
|
_startSearchAll: function(){
|
||
|
this._startSearch('');
|
||
|
},
|
||
|
|
||
|
_startSearchFromInput: function(){
|
||
|
this._startSearch(this.focusNode.value.replace(/([\\\*\?])/g, "\\$1"));
|
||
|
},
|
||
|
|
||
|
_getQueryString: function(/*String*/ text){
|
||
|
return string.substitute(this.queryExpr, [text]);
|
||
|
},
|
||
|
|
||
|
_startSearch: function(/*String*/ key){
|
||
|
// summary:
|
||
|
// Starts a search for elements matching key (key=="" means to return all items),
|
||
|
// and calls _openResultList() when the search completes, to display the results.
|
||
|
if(!this.dropDown){
|
||
|
var popupId = this.id + "_popup",
|
||
|
dropDownConstructor = lang.isString(this.dropDownClass) ?
|
||
|
lang.getObject(this.dropDownClass, false) : this.dropDownClass;
|
||
|
this.dropDown = new dropDownConstructor({
|
||
|
onChange: lang.hitch(this, this._selectOption),
|
||
|
id: popupId,
|
||
|
dir: this.dir,
|
||
|
textDir: this.textDir
|
||
|
});
|
||
|
this.focusNode.removeAttribute("aria-activedescendant");
|
||
|
this.textbox.setAttribute("aria-owns",popupId); // associate popup with textbox
|
||
|
}
|
||
|
this._lastInput = key; // Store exactly what was entered by the user.
|
||
|
|
||
|
// Setup parameters to be passed to store.query().
|
||
|
// Create a new query to prevent accidentally querying for a hidden
|
||
|
// value from FilteringSelect's keyField
|
||
|
var query = lang.clone(this.query); // #5970
|
||
|
var options = {
|
||
|
start: 0,
|
||
|
count: this.pageSize,
|
||
|
queryOptions: { // remove for 2.0
|
||
|
ignoreCase: this.ignoreCase,
|
||
|
deep: true
|
||
|
}
|
||
|
};
|
||
|
lang.mixin(options, this.fetchProperties);
|
||
|
|
||
|
// Generate query
|
||
|
var qs = this._getQueryString(key), 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;
|
||
|
|
||
|
// Function to run the query, wait for the results, and then call _openResultList()
|
||
|
var _this = this,
|
||
|
startQuery = function(){
|
||
|
var resPromise = _this._fetchHandle = _this.store.query(query, options);
|
||
|
Deferred.when(resPromise, function(res){
|
||
|
_this._fetchHandle = null;
|
||
|
res.total = resPromise.total;
|
||
|
_this._openResultList(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());
|
||
|
_this.closeDropDown();
|
||
|
}
|
||
|
});
|
||
|
};
|
||
|
|
||
|
// #5970: 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.searchTimer = setTimeout(lang.hitch(this, function(query, _this){
|
||
|
this.searchTimer = null;
|
||
|
|
||
|
startQuery();
|
||
|
|
||
|
// Setup method to handle clicking next/previous buttons to page through results
|
||
|
this._nextSearch = this.dropDown.onPage = function(direction){
|
||
|
options.start += options.count * direction;
|
||
|
// tell callback the direction of the paging so the screen
|
||
|
// reader knows which menu option to shout
|
||
|
options.direction = direction;
|
||
|
startQuery();
|
||
|
_this.focus();
|
||
|
};
|
||
|
}, query, this), this.searchDelay);
|
||
|
},
|
||
|
|
||
|
_getValueField: function(){
|
||
|
// summary:
|
||
|
// Helper for postMixInProperties() to set this.value based on data inlined into the markup.
|
||
|
// Returns the attribute name in the item (in dijit.form._ComboBoxDataStore) to use as the value.
|
||
|
return this.searchAttr;
|
||
|
},
|
||
|
|
||
|
//////////// INITIALIZATION METHODS ///////////////////////////////////////
|
||
|
|
||
|
constructor: function(){
|
||
|
this.query={};
|
||
|
this.fetchProperties={};
|
||
|
},
|
||
|
|
||
|
postMixInProperties: function(){
|
||
|
if(!this.store){
|
||
|
var srcNodeRef = this.srcNodeRef;
|
||
|
var list = this.list;
|
||
|
if(list){
|
||
|
this.store = registry.byId(list);
|
||
|
}else{
|
||
|
// if user didn't specify store, then assume there are option tags
|
||
|
this.store = new DataList({}, srcNodeRef);
|
||
|
}
|
||
|
|
||
|
// if there is no value set and there is an option list, set
|
||
|
// the value to the first value to be consistent with native Select
|
||
|
// Firefox and Safari set value
|
||
|
// IE6 and Opera set selectedIndex, which is automatically set
|
||
|
// by the selected attribute of an option tag
|
||
|
// IE6 does not set value, Opera sets value = selectedIndex
|
||
|
if(!("value" in this.params)){
|
||
|
var item = (this.item = this.store.fetchSelectedItem());
|
||
|
if(item){
|
||
|
var valueField = this._getValueField();
|
||
|
// remove getValue() for 2.0 (old dojo.data API)
|
||
|
this.value = this.store._oldAPI ? this.store.getValue(item, valueField) : item[valueField];
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
this.inherited(arguments);
|
||
|
},
|
||
|
|
||
|
postCreate: function(){
|
||
|
// summary:
|
||
|
// Subclasses must call this method from their postCreate() methods
|
||
|
// tags:
|
||
|
// protected
|
||
|
|
||
|
// find any associated label element and add to ComboBox node.
|
||
|
var label=query('label[for="'+this.id+'"]');
|
||
|
if(label.length){
|
||
|
label[0].id = (this.id+"_label");
|
||
|
this.domNode.setAttribute("aria-labelledby", label[0].id);
|
||
|
|
||
|
}
|
||
|
this.inherited(arguments);
|
||
|
},
|
||
|
|
||
|
_getMenuLabelFromItem: function(/*Item*/ item){
|
||
|
var label = this.labelFunc(item, this.store),
|
||
|
labelType = this.labelType;
|
||
|
// If labelType is not "text" we don't want to screw any markup ot whatever.
|
||
|
if(this.highlightMatch != "none" && this.labelType == "text" && this._lastInput){
|
||
|
label = this.doHighlight(label, this._escapeHtml(this._lastInput));
|
||
|
labelType = "html";
|
||
|
}
|
||
|
return {html: labelType == "html", label: label};
|
||
|
},
|
||
|
|
||
|
doHighlight: function(/*String*/ label, /*String*/ find){
|
||
|
// summary:
|
||
|
// Highlights the string entered by the user in the menu. By default this
|
||
|
// highlights the first occurrence found. Override this method
|
||
|
// to implement your custom highlighting.
|
||
|
// tags:
|
||
|
// protected
|
||
|
|
||
|
var
|
||
|
// Add (g)lobal modifier when this.highlightMatch == "all" and (i)gnorecase when this.ignoreCase == true
|
||
|
modifiers = (this.ignoreCase ? "i" : "") + (this.highlightMatch == "all" ? "g" : ""),
|
||
|
i = this.queryExpr.indexOf("${0}");
|
||
|
find = regexp.escapeString(find); // escape regexp special chars
|
||
|
return this._escapeHtml(label).replace(
|
||
|
// prepend ^ when this.queryExpr == "${0}*" and append $ when this.queryExpr == "*${0}"
|
||
|
new RegExp((i == 0 ? "^" : "") + "("+ find +")" + (i == (this.queryExpr.length - 4) ? "$" : ""), modifiers),
|
||
|
'<span class="dijitComboBoxHighlightMatch">$1</span>'
|
||
|
); // returns String, (almost) valid HTML (entities encoded)
|
||
|
},
|
||
|
|
||
|
_escapeHtml: function(/*String*/ str){
|
||
|
// TODO Should become dojo.html.entities(), when exists use instead
|
||
|
// summary:
|
||
|
// Adds escape sequences for special characters in XML: &<>"'
|
||
|
str = String(str).replace(/&/gm, "&").replace(/</gm, "<")
|
||
|
.replace(/>/gm, ">").replace(/"/gm, """); //balance"
|
||
|
return str; // string
|
||
|
},
|
||
|
|
||
|
reset: function(){
|
||
|
// Overrides the _FormWidget.reset().
|
||
|
// Additionally reset the .item (to clean up).
|
||
|
this.item = null;
|
||
|
this.inherited(arguments);
|
||
|
},
|
||
|
|
||
|
labelFunc: function(/*item*/ item, /*dojo.store.api.Store*/ store){
|
||
|
// summary:
|
||
|
// Computes the label to display based on the dojo.data store item.
|
||
|
// returns:
|
||
|
// The label that the ComboBox should display
|
||
|
// tags:
|
||
|
// private
|
||
|
|
||
|
// Use toString() because XMLStore returns an XMLItem whereas this
|
||
|
// method is expected to return a String (#9354).
|
||
|
// Remove getValue() for 2.0 (old dojo.data API)
|
||
|
return (store._oldAPI ? store.getValue(item, this.labelAttr || this.searchAttr) :
|
||
|
item[this.labelAttr || this.searchAttr]).toString(); // String
|
||
|
},
|
||
|
|
||
|
_setValueAttr: function(/*String*/ value, /*Boolean?*/ priorityChange, /*String?*/ displayedValue, /*item?*/ item){
|
||
|
// summary:
|
||
|
// Hook so set('value', value) works.
|
||
|
// description:
|
||
|
// Sets the value of the select.
|
||
|
this._set("item", item||null); // value not looked up in store
|
||
|
if(!value){ value = ''; } // null translates to blank
|
||
|
this.inherited(arguments);
|
||
|
},
|
||
|
_setTextDirAttr: function(/*String*/ textDir){
|
||
|
// summary:
|
||
|
// Setter for textDir, needed for the dropDown's textDir update.
|
||
|
// description:
|
||
|
// Users shouldn't call this function; they should be calling
|
||
|
// set('textDir', value)
|
||
|
// tags:
|
||
|
// private
|
||
|
this.inherited(arguments);
|
||
|
// update the drop down also (_ComboBoxMenuMixin)
|
||
|
if(this.dropDown){
|
||
|
this.dropDown._set("textDir", textDir);
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
});
|