dojo.provide("dijit.form.ComboBox");
dojo.require("dojo.data.ItemFileReadStore");
dojo.require("dijit.form.ValidationTextBox");
dojo.requireLocalization("dijit.form", "ComboBox");
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,
templatePath: dojo.moduleUrl("dijit.form", "templates/ComboBox.html"),
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 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