blob: 4cb7bc64bde680fa862adbed43e6737c4dd02f8c [file] [log] [blame]
/*
Copyright (c) 2004-2006, The Dojo Foundation
All Rights Reserved.
Licensed under the Academic Free License version 2.1 or above OR the
modified BSD license. For more information on Dojo licensing, see:
http://dojotoolkit.org/community/licensing.shtml
*/
dojo.provide("dojo.widget.ComboBox");
dojo.require("dojo.widget.*");
dojo.require("dojo.event.*");
dojo.require("dojo.io.*");
dojo.require("dojo.html.*");
dojo.require("dojo.string");
dojo.require("dojo.widget.html.stabile");
dojo.require("dojo.widget.PopupContainer");
dojo.declare(
"dojo.widget.incrementalComboBoxDataProvider",
null,
function(options){
// summary:
// Reference implementation / interface for Combobox incremental data provider.
// This class takes a search string and returns values that match
// that search string. The filtering of values (to find values matching given
// search string) is done on the server.
//
// options:
// Structure containing {dataUrl: "foo.js?search={searchString}"} or similar data.
// dataUrl is a URL that is passed the search string a returns a JSON structure
// showing the matching values, like [ ["Alabama","AL"], ["Alaska","AK"], ["American Samoa","AS"] ]
this.searchUrl = options.dataUrl;
// TODO: cache doesn't work
this._cache = {};
this._inFlight = false;
this._lastRequest = null;
// allowCache: Boolean
// Setting to use/not use cache for previously seen values
// TODO: caching doesn't work.
// TODO: read the setting for this value from the widget parameters
this.allowCache = false;
},
{
_addToCache: function(/*String*/ keyword, /*Array*/ data){
if(this.allowCache){
this._cache[keyword] = data;
}
},
startSearch: function(/*String*/ searchStr, /*Function*/ callback){
// summary:
// Start the search for patterns that match searchStr, and call
// specified callback functions with the results
// searchStr:
// The characters the user has typed into the <input>.
// callback:
// This function will be called with the result, as an
// array of label/value pairs (the value is used for the Select widget). Example:
// [ ["Alabama","AL"], ["Alaska","AK"], ["American Samoa","AS"] ]
if(this._inFlight){
// FIXME: implement backoff!
}
var tss = encodeURIComponent(searchStr);
var realUrl = dojo.string.substituteParams(this.searchUrl, {"searchString": tss});
var _this = this;
var request = this._lastRequest = dojo.io.bind({
url: realUrl,
method: "get",
mimetype: "text/json",
load: function(type, data, evt){
_this._inFlight = false;
if(!dojo.lang.isArray(data)){
var arrData = [];
for(var key in data){
arrData.push([data[key], key]);
}
data = arrData;
}
_this._addToCache(searchStr, data);
if (request == _this._lastRequest){
callback(data);
}
}
});
this._inFlight = true;
}
}
);
dojo.declare(
"dojo.widget.basicComboBoxDataProvider",
null,
function(/*Object*/ options, /*DomNode*/ node){
// summary:
// Reference implementation / interface for Combobox data provider.
// This class takes a search string and returns values that match
// that search string. All possible values for the combobox are downloaded
// on initialization, and then startSearch() runs locally,
// merely filting that downloaded list, to find values matching search string
//
// NOTE: this data provider is designed as a naive reference
// implementation, and as such it is written more for readability than
// speed. A deployable data provider would implement lookups, search
// caching (and invalidation), and a significantly less naive data
// structure for storage of items.
//
// options: Object
// Options object. Example:
// {
// dataUrl: String (URL to query to get list of possible drop down values),
// setAllValues: Function (callback for setting initially selected value)
// }
// The return format for dataURL is (for example)
// [ ["Alabama","AL"], ["Alaska","AK"], ["American Samoa","AS"] ... ]
//
// node:
// Pointer to the domNode in the original markup.
// This is needed in the case when the list of values is embedded
// in the html like <select> <option>Alabama</option> <option>Arkansas</option> ...
// rather than specified as a URL.
// _data: Array
// List of every possible value for the drop down list
// startSearch() simply searches this array and returns matching values.
this._data = [];
// searchLimit: Integer
// Maximum number of results to return.
// TODO: need to read this value from the widget parameters
this.searchLimit = 30;
// searchType: String
// Defines what values match the search string; see searchType parameter
// of ComboBox for details
// TODO: need to read this value from the widget parameters; the setting in ComboBox is being ignored.
this.searchType = "STARTSTRING";
// caseSensitive: Boolean
// Should search be case sensitive?
// TODO: this should be a parameter to combobox?
this.caseSensitive = false;
if(!dj_undef("dataUrl", options) && !dojo.string.isBlank(options.dataUrl)){
this._getData(options.dataUrl);
}else{
// check to see if we can populate the list from <option> elements
if((node)&&(node.nodeName.toLowerCase() == "select")){
// NOTE: we're not handling <optgroup> here yet
var opts = node.getElementsByTagName("option");
var ol = opts.length;
var data = [];
for(var x=0; x<ol; x++){
var text = opts[x].textContent || opts[x].innerText || opts[x].innerHTML;
var keyValArr = [String(text), String(opts[x].value)];
data.push(keyValArr);
if(opts[x].selected){
options.setAllValues(keyValArr[0], keyValArr[1]);
}
}
this.setData(data);
}
}
},
{
_getData: function(/*String*/ url){
dojo.io.bind({
url: url,
load: dojo.lang.hitch(this, function(type, data, evt){
if(!dojo.lang.isArray(data)){
var arrData = [];
for(var key in data){
arrData.push([data[key], key]);
}
data = arrData;
}
this.setData(data);
}),
mimetype: "text/json"
});
},
startSearch: function(/*String*/ searchStr, /*Function*/ callback){
// summary:
// Start the search for patterns that match searchStr.
// searchStr:
// The characters the user has typed into the <input>.
// callback:
// This function will be called with the result, as an
// array of label/value pairs (the value is used for the Select widget). Example:
// [ ["Alabama","AL"], ["Alaska","AK"], ["American Samoa","AS"] ]
// FIXME: need to add timeout handling here!!
this._performSearch(searchStr, callback);
},
_performSearch: function(/*String*/ searchStr, /*Function*/ callback){
//
// NOTE: this search is LINEAR, which means that it exhibits perhaps
// the worst possible speed characteristics of any search type. It's
// written this way to outline the responsibilities and interfaces for
// a search.
//
var st = this.searchType;
// FIXME: this is just an example search, which means that we implement
// only a linear search without any of the attendant (useful!) optimizations
var ret = [];
if(!this.caseSensitive){
searchStr = searchStr.toLowerCase();
}
for(var x=0; x<this._data.length; x++){
if((this.searchLimit > 0)&&(ret.length >= this.searchLimit)){
break;
}
// FIXME: we should avoid copies if possible!
var dataLabel = new String((!this.caseSensitive) ? this._data[x][0].toLowerCase() : this._data[x][0]);
if(dataLabel.length < searchStr.length){
// this won't ever be a good search, will it? What if we start
// to support regex search?
continue;
}
if(st == "STARTSTRING"){
if(searchStr == dataLabel.substr(0, searchStr.length)){
ret.push(this._data[x]);
}
}else if(st == "SUBSTRING"){
// this one is a gimmie
if(dataLabel.indexOf(searchStr) >= 0){
ret.push(this._data[x]);
}
}else if(st == "STARTWORD"){
// do a substring search and then attempt to determine if the
// preceeding char was the beginning of the string or a
// whitespace char.
var idx = dataLabel.indexOf(searchStr);
if(idx == 0){
// implicit match
ret.push(this._data[x]);
}
if(idx <= 0){
// if we didn't match or implicily matched, march onward
continue;
}
// otherwise, we have to go figure out if the match was at the
// start of a word...
// this code is taken almost directy from nWidgets
var matches = false;
while(idx!=-1){
// make sure the match either starts whole string, or
// follows a space, or follows some punctuation
if(" ,/(".indexOf(dataLabel.charAt(idx-1)) != -1){
// FIXME: what about tab chars?
matches = true; break;
}
idx = dataLabel.indexOf(searchStr, idx+1);
}
if(!matches){
continue;
}else{
ret.push(this._data[x]);
}
}
}
callback(ret);
},
setData: function(/*Array*/ pdata){
// summary: set (or reset) the data and initialize lookup structures
this._data = pdata;
}
}
);
dojo.widget.defineWidget(
"dojo.widget.ComboBox",
dojo.widget.HtmlWidget,
{
// summary:
// Auto-completing text box, and base class for Select 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.
// forceValidOption: Boolean
// If true, only allow selection of strings in drop down list.
// If false, user can select a value from the drop down, or just type in
// any random value.
forceValidOption: false,
// searchType: String
// Argument to data provider.
// Specifies rule for matching typed in string w/list of available auto-completions.
// startString - look for auto-completions that start w/the specified string.
// subString - look for auto-completions containing the typed in string.
// startWord - look for auto-completions where any word starts w/the typed in string.
searchType: "stringstart",
// dataProvider: Object
// (Read only) reference to data provider object created for this combobox
// according to "dataProviderClass" argument.
dataProvider: null,
// autoComplete: Boolean
// If you type 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,
// searchDelay: Integer
// Delay in milliseconds between when user types something and we start
// searching based on that value
searchDelay: 100,
// dataUrl: String
// URL argument passed to data provider object (class name specified in "dataProviderClass")
// An example of the URL format for the default data provider is
// "remoteComboBoxData.js?search=%{searchString}"
dataUrl: "",
// fadeTime: Integer
// Milliseconds duration of fadeout for drop down box
fadeTime: 200,
// maxListLength: Integer
// Limits list to X visible rows, scroll on rest
maxListLength: 8,
// mode: String
// Mode must be specified unless dataProviderClass is specified.
// "local" to inline search string, "remote" for JSON-returning live search
// or "html" for dumber live search.
mode: "local",
// selectedResult: Array
// (Read only) array specifying the value/label that the user selected
selectedResult: null,
// dataProviderClass: String
// Name of data provider class (code that maps a search string to a list of values)
// The class must match the interface demonstrated by dojo.widget.incrementalComboBoxDataProvider
dataProviderClass: "",
// buttonSrc: URI
// URI for the down arrow icon to the right of the input box.
buttonSrc: dojo.uri.dojoUri("src/widget/templates/images/combo_box_arrow.png"),
// dropdownToggle: String
// Animation effect for showing/displaying drop down box
dropdownToggle: "fade",
templatePath: dojo.uri.dojoUri("src/widget/templates/ComboBox.html"),
templateCssPath: dojo.uri.dojoUri("src/widget/templates/ComboBox.css"),
setValue: function(/*String*/ value){
// summary: Sets the value of the combobox
this.comboBoxValue.value = value;
if (this.textInputNode.value != value){ // prevent mucking up of selection
this.textInputNode.value = value;
// only change state and value if a new value is set
dojo.widget.html.stabile.setState(this.widgetId, this.getState(), true);
this.onValueChanged(value);
}
},
onValueChanged: function(/*String*/ value){
// summary: callback when value changes, for user to attach to
},
getValue: function(){
// summary: Rerturns combo box value
return this.comboBoxValue.value;
},
getState: function(){
// summary:
// Used for saving state of ComboBox when navigates to a new
// page, in case they then hit the browser's "Back" button.
return {value: this.getValue()};
},
setState: function(/*Object*/ state){
// summary:
// Used for restoring state of ComboBox when has navigated to a new
// page but then hits browser's "Back" button.
this.setValue(state.value);
},
enable:function(){
this.disabled=false;
this.textInputNode.removeAttribute("disabled");
},
disable: function(){
this.disabled = true;
this.textInputNode.setAttribute("disabled",true);
},
_getCaretPos: function(/*DomNode*/ element){
// khtml 3.5.2 has selection* methods as does webkit nightlies from 2005-06-22
if(dojo.lang.isNumber(element.selectionStart)){
// FIXME: this is totally borked on Moz < 1.3. Any recourse?
return element.selectionStart;
}else if(dojo.render.html.ie){
// 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){
element.focus();
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();
element.focus();
// 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);
}
}
},
_handleKeyEvents: function(/*Event*/ evt){
// summary: handles keyboard events
if(evt.ctrlKey || evt.altKey || !evt.key){ return; }
// reset these
this._prev_key_backspace = false;
this._prev_key_esc = false;
var k = dojo.event.browser.keys;
var doSearch = true;
switch(evt.key){
case k.KEY_DOWN_ARROW:
if(!this.popupWidget.isShowingNow){
this._startSearchFromInput();
}
this._highlightNextOption();
dojo.event.browser.stopEvent(evt);
return;
case k.KEY_UP_ARROW:
this._highlightPrevOption();
dojo.event.browser.stopEvent(evt);
return;
case k.KEY_TAB:
// using linux alike tab for autocomplete
if(!this.autoComplete && this.popupWidget.isShowingNow && this._highlighted_option){
dojo.event.browser.stopEvent(evt);
this._selectOption({ 'target': this._highlighted_option, 'noHide': false});
// put caret last
this._setSelectedRange(this.textInputNode, this.textInputNode.value.length, null);
}else{
this._selectOption();
return;
}
break;
case k.KEY_ENTER:
// prevent submitting form if we press enter with list open
if(this.popupWidget.isShowingNow){
dojo.event.browser.stopEvent(evt);
}
if(this.autoComplete){
this._selectOption();
return;
}
// fallthrough
case " ":
if(this.popupWidget.isShowingNow && this._highlighted_option){
dojo.event.browser.stopEvent(evt);
this._selectOption();
this._hideResultList();
return;
}
break;
case k.KEY_ESCAPE:
this._hideResultList();
this._prev_key_esc = true;
return;
case k.KEY_BACKSPACE:
this._prev_key_backspace = true;
if(!this.textInputNode.value.length){
this.setAllValues("", "");
this._hideResultList();
doSearch = false;
}
break;
case k.KEY_RIGHT_ARROW: // fall through
case k.KEY_LEFT_ARROW: // fall through
doSearch = false;
break;
default:// non char keys (F1-F12 etc..) shouldn't open list
if(evt.charCode==0){
doSearch = false;
}
}
if(this.searchTimer){
clearTimeout(this.searchTimer);
}
if(doSearch){
// if we have gotten this far we dont want to keep our highlight
this._blurOptionNode();
// need to wait a tad before start search so that the event bubbles through DOM and we have value visible
this.searchTimer = setTimeout(dojo.lang.hitch(this, this._startSearchFromInput), this.searchDelay);
}
},
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
evt.key = evt.keyCode;
this._handleKeyEvents(evt);
},
onKeyUp: function(/*Event*/ evt){
// summary: callback on key up event
this.setValue(this.textInputNode.value);
},
setSelectedValue: function(/*String*/ value){
// summary:
// This sets a hidden value associated w/the displayed value.
// The hidden value (and this function) shouldn't be used; if
// you need a hidden value then use Select widget instead of ComboBox.
// TODO: remove?
// FIXME, not sure what to do here!
this.comboBoxSelectionValue.value = value;
},
setAllValues: function(/*String*/ value1, /*String*/ value2){
// summary:
// This sets the displayed value and hidden value.
// The hidden value (and this function) shouldn't be used; if
// you need a hidden value then use Select widget instead of ComboBox.
this.setSelectedValue(value2);
this.setValue(value1);
},
_focusOptionNode: function(/*DomNode*/ node){
// summary: does the actual highlight
if(this._highlighted_option != node){
this._blurOptionNode();
this._highlighted_option = node;
dojo.html.addClass(this._highlighted_option, "dojoComboBoxItemHighlight");
}
},
_blurOptionNode: function(){
// sumary: removes highlight on highlighted
if(this._highlighted_option){
dojo.html.removeClass(this._highlighted_option, "dojoComboBoxItemHighlight");
this._highlighted_option = null;
}
},
_highlightNextOption: function(){
if((!this._highlighted_option) || !this._highlighted_option.parentNode){
this._focusOptionNode(this.optionsListNode.firstChild);
}else if(this._highlighted_option.nextSibling){
this._focusOptionNode(this._highlighted_option.nextSibling);
}
dojo.html.scrollIntoView(this._highlighted_option);
},
_highlightPrevOption: function(){
if(this._highlighted_option && this._highlighted_option.previousSibling){
this._focusOptionNode(this._highlighted_option.previousSibling);
}else{
this._highlighted_option = null;
this._hideResultList();
return;
}
dojo.html.scrollIntoView(this._highlighted_option);
},
_itemMouseOver: function(/*Event*/ evt){
if (evt.target === this.optionsListNode){ return; }
this._focusOptionNode(evt.target);
dojo.html.addClass(this._highlighted_option, "dojoComboBoxItemHighlight");
},
_itemMouseOut: function(/*Event*/ evt){
if (evt.target === this.optionsListNode){ return; }
this._blurOptionNode();
},
onResize: function(){
// summary: this function is called when the input area has changed size
var inputSize = dojo.html.getContentBox(this.textInputNode);
if( inputSize.height <= 0 ){
// need more time to calculate size
dojo.lang.setTimeout(this, "onResize", 100);
return;
}
var buttonSize = { width: inputSize.height, height: inputSize.height};
dojo.html.setContentBox(this.downArrowNode, buttonSize);
},
fillInTemplate: function(/*Object*/ args, /*Object*/ frag){
// there's some browser specific CSS in ComboBox.css
dojo.html.applyBrowserClass(this.domNode);
var source = this.getFragNodeRef(frag);
if (! this.name && source.name){ this.name = source.name; }
this.comboBoxValue.name = this.name;
this.comboBoxSelectionValue.name = this.name+"_selected";
/* different nodes get different parts of the style */
dojo.html.copyStyle(this.domNode, source);
dojo.html.copyStyle(this.textInputNode, source);
dojo.html.copyStyle(this.downArrowNode, source);
with (this.downArrowNode.style){ // calculate these later
width = "0px";
height = "0px";
}
// Use specified data provider class; if no class is specified
// then use comboboxDataProvider or incrmentalComboBoxDataProvider
// depending on setting of mode
var dpClass;
if(this.dataProviderClass){
if(typeof this.dataProviderClass == "string"){
dpClass = dojo.evalObjPath(this.dataProviderClass)
}else{
dpClass = this.dataProviderClass;
}
}else{
if(this.mode == "remote"){
dpClass = dojo.widget.incrementalComboBoxDataProvider;
}else{
dpClass = dojo.widget.basicComboBoxDataProvider;
}
}
this.dataProvider = new dpClass(this, this.getFragNodeRef(frag));
this.popupWidget = new dojo.widget.createWidget("PopupContainer",
{toggle: this.dropdownToggle, toggleDuration: this.toggleDuration});
dojo.event.connect(this, 'destroy', this.popupWidget, 'destroy');
this.optionsListNode = this.popupWidget.domNode;
this.domNode.appendChild(this.optionsListNode);
dojo.html.addClass(this.optionsListNode, 'dojoComboBoxOptions');
dojo.event.connect(this.optionsListNode, 'onclick', this, '_selectOption');
dojo.event.connect(this.optionsListNode, 'onmouseover', this, '_onMouseOver');
dojo.event.connect(this.optionsListNode, 'onmouseout', this, '_onMouseOut');
// TODO: why does onmouseover and onmouseout connect to two separate handlers???
dojo.event.connect(this.optionsListNode, "onmouseover", this, "_itemMouseOver");
dojo.event.connect(this.optionsListNode, "onmouseout", this, "_itemMouseOut");
},
_openResultList: function(/*Array*/ results){
if (this.disabled){
return;
}
this._clearResultList();
if(!results.length){
this._hideResultList();
}
if( (this.autoComplete)&&
(results.length)&&
(!this._prev_key_backspace)&&
(this.textInputNode.value.length > 0)){
var cpos = this._getCaretPos(this.textInputNode);
// only try to extend if we added the last character at the end of the input
if((cpos+1) > this.textInputNode.value.length){
// only add to input node as we would overwrite Capitalisation of chars
this.textInputNode.value += results[0][0].substr(cpos);
// build a new range that has the distance from the earlier
// caret position to the end of the first string selected
this._setSelectedRange(this.textInputNode, cpos, this.textInputNode.value.length);
}
}
var even = true;
while(results.length){
var tr = results.shift();
if(tr){
var td = document.createElement("div");
td.appendChild(document.createTextNode(tr[0]));
td.setAttribute("resultName", tr[0]);
td.setAttribute("resultValue", tr[1]);
td.className = "dojoComboBoxItem "+((even) ? "dojoComboBoxItemEven" : "dojoComboBoxItemOdd");
even = (!even);
this.optionsListNode.appendChild(td);
}
}
// show our list (only if we have content, else nothing)
this._showResultList();
},
_onFocusInput: function(){
this._hasFocus = true;
},
_onBlurInput: function(){
this._hasFocus = false;
this._handleBlurTimer(true, 500);
},
_handleBlurTimer: function(/*Boolean*/clear, /*Number*/ millisec){
// summary: collect all blur timers issues here
if(this.blurTimer && (clear || millisec)){
clearTimeout(this.blurTimer);
}
if(millisec){ // we ignore that zero is false and never sets as that never happens in this widget
this.blurTimer = dojo.lang.setTimeout(this, "_checkBlurred", millisec);
}
},
_onMouseOver: function(/*Event*/ evt){
// summary: needed in IE and Safari as inputTextNode loses focus when scrolling optionslist
if(!this._mouseover_list){
this._handleBlurTimer(true, 0);
this._mouseover_list = true;
}
},
_onMouseOut:function(/*Event*/ evt){
// summary: needed in IE and Safari as inputTextNode loses focus when scrolling optionslist
var relTarget = evt.relatedTarget;
try { // fixes #1807
if(!relTarget || relTarget.parentNode != this.optionsListNode){
this._mouseover_list = false;
this._handleBlurTimer(true, 100);
this._tryFocus();
}
}catch(e){}
},
_isInputEqualToResult: function(/*String*/ result){
var input = this.textInputNode.value;
if(!this.dataProvider.caseSensitive){
input = input.toLowerCase();
result = result.toLowerCase();
}
return (input == result);
},
_isValidOption: function(){
var tgt = dojo.html.firstElement(this.optionsListNode);
var isValidOption = false;
while(!isValidOption && tgt){
if(this._isInputEqualToResult(tgt.getAttribute("resultName"))){
isValidOption = true;
}else{
tgt = dojo.html.nextElement(tgt);
}
}
return isValidOption;
},
_checkBlurred: function(){
if(!this._hasFocus && !this._mouseover_list){
this._hideResultList();
// clear the list if the user empties field and moves away.
if(!this.textInputNode.value.length){
this.setAllValues("", "");
return;
}
var isValidOption = this._isValidOption();
// enforce selection from option list
if(this.forceValidOption && !isValidOption){
this.setAllValues("", "");
return;
}
if(!isValidOption){// clear
this.setSelectedValue("");
}
}
},
_selectOption: function(/*Event*/ evt){
var tgt = null;
if(!evt){
evt = { target: this._highlighted_option };
}
if(!dojo.html.isDescendantOf(evt.target, this.optionsListNode)){
// handle autocompletion where the the user has hit ENTER or TAB
// if the input is empty do nothing
if(!this.textInputNode.value.length){
return;
}
tgt = dojo.html.firstElement(this.optionsListNode);
// user has input value not in option list
if(!tgt || !this._isInputEqualToResult(tgt.getAttribute("resultName"))){
return;
}
// otherwise the user has accepted the autocompleted value
}else{
tgt = evt.target;
}
while((tgt.nodeType!=1)||(!tgt.getAttribute("resultName"))){
tgt = tgt.parentNode;
if(tgt === dojo.body()){
return false;
}
}
this.selectedResult = [tgt.getAttribute("resultName"), tgt.getAttribute("resultValue")];
this.setAllValues(tgt.getAttribute("resultName"), tgt.getAttribute("resultValue"));
if(!evt.noHide){
this._hideResultList();
this._setSelectedRange(this.textInputNode, 0, null);
}
this._tryFocus();
},
_clearResultList: function(){
if(this.optionsListNode.innerHTML){
this.optionsListNode.innerHTML = ""; // browser natively knows how to collect this memory
}
},
_hideResultList: function(){
this.popupWidget.close();
},
_showResultList: function(){
// Our dear friend IE doesnt take max-height so we need to calculate that on our own every time
var childs = this.optionsListNode.childNodes;
if(childs.length){
var visibleCount = Math.min(childs.length,this.maxListLength);
with(this.optionsListNode.style)
{
display = "";
if(visibleCount == childs.length){
//no scrollbar is required, so unset height to let browser calcuate it,
//as in css, overflow is already set to auto
height = "";
}else{
//show it first to get the correct dojo.style.getOuterHeight(childs[0])
//FIXME: shall we cache the height of the item?
height = visibleCount * dojo.html.getMarginBox(childs[0]).height +"px";
}
width = (dojo.html.getMarginBox(this.domNode).width-2)+"px";
}
this.popupWidget.open(this.domNode, this, this.downArrowNode);
}else{
this._hideResultList();
}
},
handleArrowClick: function(){
// summary: callback when arrow is clicked
this._handleBlurTimer(true, 0);
this._tryFocus();
if(this.popupWidget.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("");
}
},
_tryFocus: function(){
try {
this.textInputNode.focus();
} catch (e){
// element isn't focusable if disabled, or not visible etc - not easy to test for.
};
},
_startSearchFromInput: function(){
this._startSearch(this.textInputNode.value);
},
_startSearch: function(/*String*/ key){
this.dataProvider.startSearch(key, dojo.lang.hitch(this, "_openResultList"));
},
postCreate: function(){
this.onResize();
// TODO: add these attach events to template
dojo.event.connect(this.textInputNode, "onblur", this, "_onBlurInput");
dojo.event.connect(this.textInputNode, "onfocus", this, "_onFocusInput");
if (this.disabled){
this.disable();
}
var s = dojo.widget.html.stabile.getState(this.widgetId);
if (s){
this.setState(s);
}
}
}
);