if (!dojo._hasResource["dijit.form.ComboBox"]) { // _hasResource checks added
// by build. Do not use
// _hasResource directly in
// your code.
dojo._hasResource["dijit.form.ComboBox"] = true;
dojo.provide("dijit.form.ComboBox");
dojo.require("dojo.data.ItemFileReadStore");
dojo.require("dijit.form.ValidationTextBox");
dojo.requireLocalization("dijit.form", "ComboBox", null,
"ko,zh,ja,zh-tw,ru,it,hu,ROOT,fr,pt,pl,es,de,cs");
dojo.declare("dijit.form.ComboBoxMixin", null, {
// summary:
// Auto-completing text box, and base class for FilteringSelect widget.
//
// The drop down box's values are populated from an class called
// a data provider, which returns a list of values based on the
// characters
// that the user has typed into the input box.
//
// Some of the options to the ComboBox are actually arguments to the
// data
// provider.
//
// You can assume that all the form widgets (and thus anything that
// mixes
// in ComboBoxMixin) will inherit from _FormWidget and thus the "this"
// reference will also "be a" _FormWidget.
// item: Object
// This is the item returned by the dojo.data.store implementation that
// provides the data for this cobobox, 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: Object
// Reference to data provider object used by this ComboBox
store : null,
// 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.
query : {},
// autoComplete: Boolean
// If you type in a partial string, and then tab out of the box,
// automatically copy the first entry displayed in the drop down list to
// the field
autoComplete : true,
// searchDelay: Integer
// Delay in milliseconds between when user types something and we start
// searching based on that value
searchDelay : 100,
// searchAttr: String
// Searches pattern match against this field
searchAttr : "name",
// ignoreCase: Boolean
// Set true if the ComboBox should ignore case when matching possible
// items
ignoreCase : true,
// hasDownArrow: Boolean
// Set this textbox to have a down arrow button.
// Defaults to true.
hasDownArrow : true,
// _hasFocus: Boolean
// Represents focus state of the textbox
// TODO: get rid of this; it's unnecessary (but currently referenced in
// FilteringSelect)
_hasFocus : false,
templateString : "
Χ
▼
\n",
baseClass : "dijitComboBox",
_lastDisplayedValue : "",
getValue : function() {
// don't get the textbox value but rather the previously set hidden
// value
return dijit.form.TextBox.superclass.getValue
.apply(this, arguments);
},
setDisplayedValue : function(/* String */value) {
this._lastDisplayedValue = value;
this.setValue(value, true);
},
_getCaretPos : function(/* DomNode */element) {
// khtml 3.5.2 has selection* methods as does webkit nightlies from
// 2005-06-22
if (typeof(element.selectionStart) == "number") {
// FIXME: this is totally borked on Moz < 1.3. Any recourse?
return element.selectionStart;
} else if (dojo.isIE) {
// in the case of a mouse click in a popup being handled,
// then the document.selection is not the textarea, but the
// popup
// var r = document.selection.createRange();
// hack to get IE 6 to play nice. What a POS browser.
var tr = document.selection.createRange().duplicate();
var ntr = element.createTextRange();
tr.move("character", 0);
ntr.move("character", 0);
try {
// If control doesnt 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);
return String(ntr.text).replace(/\r/g, "").length;
} catch (e) {
return 0; // If focus has shifted, 0 is fine for caret
// pos.
}
}
},
_setCaretPos : function(/* DomNode */element, /* Number */location) {
location = parseInt(location);
this._setSelectedRange(element, location, location);
},
_setSelectedRange : function(/* DomNode */element, /* Number */start, /* Number */
end) {
if (!end) {
end = element.value.length;
} // NOTE: Strange - should be able to put caret at start of text?
// Mozilla
// parts borrowed from
// http://www.faqts.com/knowledge_base/view.phtml/aid/13562/fid/130
if (element.setSelectionRange) {
dijit.focus(element);
element.setSelectionRange(start, end);
} else if (element.createTextRange) { // IE
var range = element.createTextRange();
with (range) {
collapse(true);
moveEnd('character', end);
moveStart('character', start);
select();
}
} else { // otherwise try the event-creation hack (our own
// invention)
// do we need these?
element.value = element.value;
element.blur();
dijit.focus(element);
// figure out how far back to go
var dist = parseInt(element.value.length) - end;
var tchar = String.fromCharCode(37);
var tcc = tchar.charCodeAt(0);
for (var x = 0; x < dist; x++) {
var te = document.createEvent("KeyEvents");
te.initKeyEvent("keypress", true, true, null, false, false,
false, false, tcc, tcc);
element.dispatchEvent(te);
}
}
},
onkeypress : function(/* Event */evt) {
// summary: handles keyboard events
// except for pasting case - ctrl + v(118)
if (evt.altKey || (evt.ctrlKey && evt.charCode != 118)) {
return;
}
var doSearch = false;
this.item = null; // #4872
if (this._isShowingNow) {
this._popupWidget.handleKey(evt);
}
switch (evt.keyCode) {
case dojo.keys.PAGE_DOWN :
case dojo.keys.DOWN_ARROW :
if (!this._isShowingNow || this._prev_key_esc) {
this._arrowPressed();
doSearch = true;
} else {
this._announceOption(this._popupWidget
.getHighlightedOption());
}
dojo.stopEvent(evt);
this._prev_key_backspace = false;
this._prev_key_esc = false;
break;
case dojo.keys.PAGE_UP :
case dojo.keys.UP_ARROW :
if (this._isShowingNow) {
this._announceOption(this._popupWidget
.getHighlightedOption());
}
dojo.stopEvent(evt);
this._prev_key_backspace = false;
this._prev_key_esc = false;
break;
case dojo.keys.ENTER :
// prevent submitting form if user presses enter
// also prevent accepting the value if either Next or
// Previous are selected
var highlighted;
if (this._isShowingNow
&& (highlighted = this._popupWidget
.getHighlightedOption())) {
// only stop event on prev/next
if (highlighted == this._popupWidget.nextButton) {
this._nextSearch(1);
dojo.stopEvent(evt);
break;
} else if (highlighted == this._popupWidget.previousButton) {
this._nextSearch(-1);
dojo.stopEvent(evt);
break;
}
} else {
this.setDisplayedValue(this.getDisplayedValue());
}
// default case:
// prevent submit, but allow event to bubble
evt.preventDefault();
// fall through
case dojo.keys.TAB :
var newvalue = this.getDisplayedValue();
// #4617: if the user had More Choices selected fall into
// the _onBlur handler
if (this._popupWidget
&& (newvalue == this._popupWidget._messages["previousMessage"] || newvalue == this._popupWidget._messages["nextMessage"])) {
break;
}
if (this._isShowingNow) {
this._prev_key_backspace = false;
this._prev_key_esc = false;
if (this._popupWidget.getHighlightedOption()) {
this._popupWidget.setValue({
target : this._popupWidget
.getHighlightedOption()
}, true);
}
this._hideResultList();
}
break;
case dojo.keys.SPACE :
this._prev_key_backspace = false;
this._prev_key_esc = false;
if (this._isShowingNow
&& this._popupWidget.getHighlightedOption()) {
dojo.stopEvent(evt);
this._selectOption();
this._hideResultList();
} else {
doSearch = true;
}
break;
case dojo.keys.ESCAPE :
this._prev_key_backspace = false;
this._prev_key_esc = true;
this._hideResultList();
if (this._lastDisplayedValue != this.getDisplayedValue()) {
this.setDisplayedValue(this._lastDisplayedValue);
dojo.stopEvent(evt);
} else {
this.setValue(this.getValue(), false);
}
break;
case dojo.keys.DELETE :
case dojo.keys.BACKSPACE :
this._prev_key_esc = false;
this._prev_key_backspace = true;
doSearch = true;
break;
case dojo.keys.RIGHT_ARROW : // fall through
case dojo.keys.LEFT_ARROW : // fall through
this._prev_key_backspace = false;
this._prev_key_esc = false;
break;
default :// non char keys (F1-F12 etc..) shouldn't open list
this._prev_key_backspace = false;
this._prev_key_esc = false;
if (dojo.isIE || evt.charCode != 0) {
doSearch = true;
}
}
if (this.searchTimer) {
clearTimeout(this.searchTimer);
}
if (doSearch) {
// need to wait a tad before start search so that the event
// bubbles through DOM and we have value visible
this.searchTimer = setTimeout(dojo.hitch(this,
this._startSearchFromInput), this.searchDelay);
}
},
_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.
// IE7: clear selection so next highlight works all the time
this._setSelectedRange(this.focusNode, this.focusNode.value.length,
this.focusNode.value.length);
// does text autoComplete the value in the textbox?
// #3744: escape regexp so the user's input isn't treated as a
// regular expression.
// Example: If the user typed "(" then the regexp would throw
// "unterminated parenthetical."
// Also see #2558 for the autocompletion bug this regular expression
// fixes.
if (new RegExp("^" + escape(this.focusNode.value), this.ignoreCase
? "i"
: "").test(escape(text))) {
var cpos = this._getCaretPos(this.focusNode);
// only try to extend if we added the last character at the end
// of the input
if ((cpos + 1) > this.focusNode.value.length) {
// only add to input node as we would overwrite
// Capitalisation of chars
// actually, that is ok
this.focusNode.value = text;// .substr(cpos);
// visually highlight the autocompleted characters
this._setSelectedRange(this.focusNode, cpos,
this.focusNode.value.length);
dijit.setWaiState(this.focusNode, "valuenow", text);
}
} else {
// text does not autoComplete; replace the whole value and
// highlight
this.focusNode.value = text;
this._setSelectedRange(this.focusNode, 0,
this.focusNode.value.length);
dijit.setWaiState(this.focusNode, "valuenow", text);
}
},
_openResultList : function(/* Object */results, /* Object */dataObject) {
if (this.disabled
|| dataObject.query[this.searchAttr] != this._lastQuery) {
return;
}
this._popupWidget.clearResultList();
if (!results.length) {
this._hideResultList();
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 zerothvalue = new String(this.store.getValue(results[0],
this.searchAttr));
if (zerothvalue && 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.
(dataObject.query[this.searchAttr] != "*")) {
this._autoCompleteText(zerothvalue);
// announce the autocompleted value
dijit.setWaiState(this.focusNode || this.domNode, "valuenow",
zerothvalue);
}
this._popupWidget.createOptions(results, dataObject, dojo.hitch(
this, 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 (dataObject.direction) {
if (dataObject.direction == 1) {
this._popupWidget.highlightFirstOption();
} else if (dataObject.direction == -1) {
this._popupWidget.highlightLastOption();
}
this._announceOption(this._popupWidget.getHighlightedOption());
}
},
_showResultList : function() {
this._hideResultList();
var items = this._popupWidget.getItems(), visibleCount = Math.min(
items.length, this.maxListLength);
this._arrowPressed();
// hide the tooltip
this._displayMessage("");
// Position the list and if it's too big to fit on the screen then
// size it to the maximum possible height
// Our dear friend IE doesnt take max-height so we need to calculate
// that on our own every time
// TODO: want to redo this, see
// http://trac.dojotoolkit.org/ticket/3272,
// http://trac.dojotoolkit.org/ticket/4108
with (this._popupWidget.domNode.style) {
// natural size of the list has changed, so erase old
// width/height settings,
// which were hardcoded in a previous call to this function (via
// dojo.marginBox() call)
width = "";
height = "";
}
var best = this.open();
// #3212: only set auto scroll bars if necessary
// prevents issues with scroll bars appearing when they shouldn't
// when node is made wider (fractional pixels cause this)
var popupbox = dojo.marginBox(this._popupWidget.domNode);
this._popupWidget.domNode.style.overflow = ((best.h == popupbox.h) && (best.w == popupbox.w))
? "hidden"
: "auto";
// #4134: borrow TextArea scrollbar test so content isn't covered by
// scrollbar and horizontal scrollbar doesn't appear
var newwidth = best.w;
if (best.h < this._popupWidget.domNode.scrollHeight) {
newwidth += 16;
}
dojo.marginBox(this._popupWidget.domNode, {
h : best.h,
w : Math.max(newwidth, this.domNode.offsetWidth)
});
},
_hideResultList : function() {
if (this._isShowingNow) {
dijit.popup.close(this._popupWidget);
this._arrowIdle();
this._isShowingNow = false;
}
},
_onBlur : function() {
// summary: called magically when focus has shifted away from this
// widget and it's dropdown
this._hasFocus = false;
this._hasBeenBlurred = true;
this._hideResultList();
this._arrowIdle();
// 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.getDisplayedValue();
if (this._popupWidget
&& (newvalue == this._popupWidget._messages["previousMessage"] || newvalue == this._popupWidget._messages["nextMessage"])) {
this.setValue(this._lastValueReported, true);
} else {
this.setDisplayedValue(newvalue);
}
},
onfocus : function(/* Event */evt) {
this._hasFocus = true;
// update styling to reflect that we are focused
this._onMouse(evt);
},
_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 == null) {
return;
}
// pull the text value from the item attached to the DOM node
var newValue;
if (node == this._popupWidget.nextButton
|| node == this._popupWidget.previousButton) {
newValue = node.innerHTML;
} else {
newValue = this.store.getValue(node.item, this.searchAttr);
}
// get the text that the user manually entered (cut off
// autocompleted text)
this.focusNode.value = this.focusNode.value.substring(0, this
._getCaretPos(this.focusNode));
// autocomplete the rest of the option to announce change
this._autoCompleteText(newValue);
},
_selectOption : function(/* Event */evt) {
var tgt = null;
if (!evt) {
evt = {
target : this._popupWidget.getHighlightedOption()
};
}
// what if nothing is highlighted yet?
if (!evt.target) {
// handle autocompletion where the the user has hit ENTER or TAB
this.setDisplayedValue(this.getDisplayedValue());
return;
// otherwise the user has accepted the autocompleted value
} else {
tgt = evt.target;
}
if (!evt.noHide) {
this._hideResultList();
this._setCaretPos(this.focusNode, this.store.getValue(tgt.item,
this.searchAttr).length);
}
this._doSelect(tgt);
},
_doSelect : function(tgt) {
this.item = tgt.item;
this.setValue(this.store.getValue(tgt.item, this.searchAttr), true);
},
_onArrowMouseDown : function(evt) {
// summary: callback when arrow is clicked
if (this.disabled) {
return;
}
dojo.stopEvent(evt);
this.focus();
if (this._isShowingNow) {
this._hideResultList();
} else {
// forces full population of results, if they click
// on the arrow it means they want to see more options
this._startSearch("");
}
},
_startSearchFromInput : function() {
this._startSearch(this.focusNode.value);
},
_startSearch : function(/* String */key) {
if (!this._popupWidget) {
this._popupWidget = new dijit.form._ComboBoxMenu({
onChange : dojo.hitch(this, this._selectOption)
});
}
// create a new query to prevent accidentally querying for a hidden
// value from FilteringSelect's keyField
var query = this.query;
this._lastQuery = query[this.searchAttr] = key + "*";
var dataObject = this.store.fetch({
queryOptions : {
ignoreCase : this.ignoreCase,
deep : true
},
query : query,
onComplete : dojo.hitch(this, "_openResultList"),
start : 0,
count : this.pageSize
});
function nextSearch(dataObject, direction) {
dataObject.start += dataObject.count * direction;
// #4091: tell callback the direction of the paging so the
// screen reader knows which menu option to shout
dataObject.direction = direction;
dataObject.store.fetch(dataObject);
}
this._nextSearch = this._popupWidget.onPage = dojo.hitch(this,
nextSearch, dataObject);
},
_getValueField : function() {
return this.searchAttr;
},
// ///////////// Event handlers /////////////////////
_arrowPressed : function() {
if (!this.disabled && this.hasDownArrow) {
dojo.addClass(this.downArrowNode, "dijitArrowButtonActive");
}
},
_arrowIdle : function() {
if (!this.disabled && this.hasDownArrow) {
dojo.removeClass(this.downArrowNode, "dojoArrowButtonPushed");
}
},
compositionend : function(/* Event */evt) {
// summary: When inputting characters using an input method, such as
// Asian
// languages, it will generate this event instead of onKeyDown event
// Note: this event is only triggered in FF (not in IE)
this.onkeypress({
charCode : -1
});
},
// ////////// INITIALIZATION METHODS
// ///////////////////////////////////////
constructor : function() {
this.query = {};
},
postMixInProperties : function() {
if (!this.hasDownArrow) {
this.baseClass = "dijitTextBox";
}
if (!this.store) {
// if user didn't specify store, then assume there are option
// tags
var items = this.srcNodeRef ? dojo.query("> option",
this.srcNodeRef).map(function(node) {
node.style.display = "none";
return {
value : node.getAttribute("value"),
name : String(node.innerHTML)
};
}) : {};
this.store = new dojo.data.ItemFileReadStore({
data : {
identifier : this._getValueField(),
items : items
}
});
// 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
if (items && items.length && !this.value) {
// For