mirror of
https://github.com/Alfresco/alfresco-community-repo.git
synced 2025-08-14 17:58:59 +00:00
git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/alfresco/HEAD/root@5282 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261
951 lines
30 KiB
JavaScript
951 lines
30 KiB
JavaScript
/*
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
);
|