Author: hermanns Date: Fri Dec 1 08:47:25 2006 New Revision: 481288 URL: http://svn.apache.org/viewvc?view=rev&rev=481288 Log: Autocompleter problems o Added missing files
Issue Number: WW-1529 Submitted by: Musachy Barroso Added: struts/struts2/trunk/apps/showcase/src/main/java/org/apache/struts2/showcase/ajax/AutocompleterExampleAction.java struts/struts2/trunk/apps/showcase/src/main/webapp/ajax/options.ftl struts/struts2/trunk/core/src/main/resources/org/apache/struts2/static/dojo/struts/widget/ComboBox.js Added: struts/struts2/trunk/apps/showcase/src/main/java/org/apache/struts2/showcase/ajax/AutocompleterExampleAction.java URL: http://svn.apache.org/viewvc/struts/struts2/trunk/apps/showcase/src/main/java/org/apache/struts2/showcase/ajax/AutocompleterExampleAction.java?view=auto&rev=481288 ============================================================================== --- struts/struts2/trunk/apps/showcase/src/main/java/org/apache/struts2/showcase/ajax/AutocompleterExampleAction.java (added) +++ struts/struts2/trunk/apps/showcase/src/main/java/org/apache/struts2/showcase/ajax/AutocompleterExampleAction.java Fri Dec 1 08:47:25 2006 @@ -0,0 +1,39 @@ +package org.apache.struts2.showcase.ajax; + +import java.util.ArrayList; +import java.util.List; + +import com.opensymphony.xwork2.ActionSupport; + +public class AutocompleterExampleAction extends ActionSupport { + private String select; + private List<String> options = new ArrayList<String>(); + + private static final long serialVersionUID = -8481638176160014396L; + + public String execute() throws Exception { + if ("fruits".equals(select)) { + options.add("apple"); + options.add("banana"); + options.add("grape"); + options.add("pear"); + } else if ("colors".equals(select)) { + options.add("red"); + options.add("green"); + options.add("blue"); + } + return SUCCESS; + } + + public String getSelect() { + return select; + } + + public void setSelect(String select) { + this.select = select; + } + + public List<String> getOptions() { + return options; + } +} Added: struts/struts2/trunk/apps/showcase/src/main/webapp/ajax/options.ftl URL: http://svn.apache.org/viewvc/struts/struts2/trunk/apps/showcase/src/main/webapp/ajax/options.ftl?view=auto&rev=481288 ============================================================================== --- struts/struts2/trunk/apps/showcase/src/main/webapp/ajax/options.ftl (added) +++ struts/struts2/trunk/apps/showcase/src/main/webapp/ajax/options.ftl Fri Dec 1 08:47:25 2006 @@ -0,0 +1,5 @@ +[ +<#list options as option> + ["${option}"], +</#list> +] \ No newline at end of file Added: struts/struts2/trunk/core/src/main/resources/org/apache/struts2/static/dojo/struts/widget/ComboBox.js URL: http://svn.apache.org/viewvc/struts/struts2/trunk/core/src/main/resources/org/apache/struts2/static/dojo/struts/widget/ComboBox.js?view=auto&rev=481288 ============================================================================== --- struts/struts2/trunk/core/src/main/resources/org/apache/struts2/static/dojo/struts/widget/ComboBox.js (added) +++ struts/struts2/trunk/core/src/main/resources/org/apache/struts2/static/dojo/struts/widget/ComboBox.js Fri Dec 1 08:47:25 2006 @@ -0,0 +1,586 @@ +dojo.provide("struts.widget.ComboBox"); + +dojo.require("dojo.html.*"); +dojo.require("dojo.widget.ComboBox"); + +struts.widget.ComboBoxDataProvider = function(/*Array*/ dataPairs, /*Number*/ limit, /*Number*/ timeout){ + // 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. + + this.data = []; + this.searchTimeout = timeout || 500; + this.searchLimit = limit || 30; + this.searchType = "STARTSTRING"; // may also be "STARTWORD" or "SUBSTRING" + this.caseSensitive = false; + // for caching optimizations + this._lastSearch = ""; + this._lastSearchResults = null; + + this.beforeLoading = ""; + this.afterLoading = ""; + + this.formId = ""; + this.formFilter = ""; + + this.init = function(/*Widget*/ cbox, /*DomNode*/ node){ + this.beforeLoading = cbox.beforeLoading; + this.afterLoading = cbox.afterLoading; + + this.formId = cbox.formId; + this.formFilter = cbox.formFilter; + + if(!dojo.string.isBlank(cbox.dataUrl)){ + this.getData(cbox.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){ + cbox.setAllValues(keyValArr[0], keyValArr[1]); + } + } + this.setData(data); + } + } + }; + + this.getData = function(/*String*/ url){ + if(!dojo.string.isBlank(this.beforeLoading)) { + eval(this.beforeLoading); + } + + dojo.io.bind({ + url: url, + formNode: dojo.byId(this.formId), + formFilter: window[this.formFilter], + load: dojo.lang.hitch(this, function(type, data, evt){ + if(!dojo.string.isBlank(this.afterLoading)) { + eval(this.afterLoading); + } + 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" + }); + }; + + this.startSearch = function(/*String*/ searchStr, /*String*/ type, /*Boolean*/ ignoreLimit){ + // FIXME: need to add timeout handling here!! + this._preformSearch(searchStr, type, ignoreLimit); + }; + + this._preformSearch = function(/*String*/ searchStr, /*String*/ type, /*Boolean*/ ignoreLimit){ + // + // 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 = type||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((!ignoreLimit)&&(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]); + } + } + } + this.provideSearchResults(ret); + }; + + this.provideSearchResults = function(/*Array*/ resultsDataPairs){ + }; + + this.addData = function(/*Array*/ pairs){ + // FIXME: incredibly naive and slow! + this.data = this.data.concat(pairs); + }; + + this.setData = function(/*Array*/ pdata){ + // populate this.data and initialize lookup structures + this.data = pdata; + }; + + if(dataPairs){ + this.setData(dataPairs); + } +}; + +dojo.widget.defineWidget( + "struts.widget.ComboBox", + dojo.widget.ComboBox, { + widgetType : "ComboBox", + + dropdownHeight: 120, + dropdownWidth: 0, + itemHeight: 0, + + refreshListenTopic : "", + onValueChangedPublishTopic : "", + + //callbacks + beforeLoading : "", + afterLoading : "", + + formId : "", + formFilter : "", + dataProviderClass: "struts.widget.ComboBoxDataProvider", + //from Dojo's ComboBox + 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){ + + this.optionsListNode.style.width = this.dropdownWidth === 0 ? (dojo.html.getMarginBox(this.domNode).width-2)+"px" : this.dropdownWidth + "px"; + + if(this.itemHeight === 0 || dojo.string.isBlank(this.textInputNode.value)) { + this.optionsListNode.style.height = this.dropdownHeight + "px"; + this.optionsListNode.style.display = ""; + this.itemHeight = dojo.html.getMarginBox(childs[0]).height; + } + + //if there is extra space, adjust height + var totalHeight = this.itemHeight * childs.length; + if(totalHeight < this.dropdownHeight) { + this.optionsListNode.style.height = totalHeight + 2 + "px"; + } + + this.popupWidget.open(this.domNode, this, this.downArrowNode); + } else { + this.hideResultList(); + } + }, + + openResultList: function(/*Array*/ results){ + if (!this.isEnabled){ + 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 typedText = this.textInputNode.value; + var even = true; + while(results.length){ + var tr = results.shift(); + if(tr){ + var td = document.createElement("div"); + var text = tr[0]; + var i = text.toLowerCase().indexOf(typedText.toLowerCase()); + if(i >= 0) { + var pre = text.substring(0, i); + var matched = text.substring(i, typedText.length); + var post = text.substring(i + typedText.length); + + td.appendChild(document.createTextNode(pre)); + var boldNode = document.createElement("b"); + td.appendChild(boldNode); + boldNode.appendChild(document.createTextNode(matched)); + td.appendChild(document.createTextNode(post)); + } else { + 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(); + }, + + postCreate : function() { + struts.widget.ComboBox.superclass.postCreate.apply(this); + + //events + if(!dojo.string.isBlank(this.refreshListenTopic)) { + var self = this; + dojo.event.topic.subscribe(this.refreshListenTopic, function() { + self.dataProvider.getData(self.dataUrl); + }); + } + if(!dojo.string.isBlank(this.onValueChangedPublishTopic)) { + dojo.event.topic.registerPublisher(this.onValueChangedPublishTopic, this, "onValueChanged"); + } + } +}); +dojo.provide("struts.widget.ComboBox"); + +dojo.require("dojo.html.*"); +dojo.require("dojo.widget.ComboBox"); + +struts.widget.ComboBoxDataProvider = function(/*Array*/ dataPairs, /*Number*/ limit, /*Number*/ timeout){ + // 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. + + this.data = []; + this.searchTimeout = timeout || 500; + this.searchLimit = limit || 30; + this.searchType = "STARTSTRING"; // may also be "STARTWORD" or "SUBSTRING" + this.caseSensitive = false; + // for caching optimizations + this._lastSearch = ""; + this._lastSearchResults = null; + + this.beforeLoading = ""; + this.afterLoading = ""; + + this.formId = ""; + this.formFilter = ""; + + this.init = function(/*Widget*/ cbox, /*DomNode*/ node){ + this.beforeLoading = cbox.beforeLoading; + this.afterLoading = cbox.afterLoading; + + this.formId = cbox.formId; + this.formFilter = cbox.formFilter; + + if(!dojo.string.isBlank(cbox.dataUrl)){ + this.getData(cbox.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){ + cbox.setAllValues(keyValArr[0], keyValArr[1]); + } + } + this.setData(data); + } + } + }; + + this.getData = function(/*String*/ url){ + if(!dojo.string.isBlank(this.beforeLoading)) { + eval(this.beforeLoading); + } + + dojo.io.bind({ + url: url, + formNode: dojo.byId(this.formId), + formFilter: window[this.formFilter], + load: dojo.lang.hitch(this, function(type, data, evt){ + if(!dojo.string.isBlank(this.afterLoading)) { + eval(this.afterLoading); + } + 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" + }); + }; + + this.startSearch = function(/*String*/ searchStr, /*String*/ type, /*Boolean*/ ignoreLimit){ + // FIXME: need to add timeout handling here!! + this._preformSearch(searchStr, type, ignoreLimit); + }; + + this._preformSearch = function(/*String*/ searchStr, /*String*/ type, /*Boolean*/ ignoreLimit){ + // + // 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 = type||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((!ignoreLimit)&&(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]); + } + } + } + this.provideSearchResults(ret); + }; + + this.provideSearchResults = function(/*Array*/ resultsDataPairs){ + }; + + this.addData = function(/*Array*/ pairs){ + // FIXME: incredibly naive and slow! + this.data = this.data.concat(pairs); + }; + + this.setData = function(/*Array*/ pdata){ + // populate this.data and initialize lookup structures + this.data = pdata; + }; + + if(dataPairs){ + this.setData(dataPairs); + } +}; + +dojo.widget.defineWidget( + "struts.widget.ComboBox", + dojo.widget.ComboBox, { + widgetType : "ComboBox", + + dropdownHeight: 120, + dropdownWidth: 0, + itemHeight: 0, + + refreshListenTopic : "", + onValueChangedPublishTopic : "", + + //callbacks + beforeLoading : "", + afterLoading : "", + + formId : "", + formFilter : "", + dataProviderClass: "struts.widget.ComboBoxDataProvider", + //from Dojo's ComboBox + 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){ + + this.optionsListNode.style.width = this.dropdownWidth === 0 ? (dojo.html.getMarginBox(this.domNode).width-2)+"px" : this.dropdownWidth + "px"; + + if(this.itemHeight === 0 || dojo.string.isBlank(this.textInputNode.value)) { + this.optionsListNode.style.height = this.dropdownHeight + "px"; + this.optionsListNode.style.display = ""; + this.itemHeight = dojo.html.getMarginBox(childs[0]).height; + } + + //if there is extra space, adjust height + var totalHeight = this.itemHeight * childs.length; + if(totalHeight < this.dropdownHeight) { + this.optionsListNode.style.height = totalHeight + 2 + "px"; + } + + this.popupWidget.open(this.domNode, this, this.downArrowNode); + } else { + this.hideResultList(); + } + }, + + openResultList: function(/*Array*/ results){ + if (!this.isEnabled){ + 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 typedText = this.textInputNode.value; + var even = true; + while(results.length){ + var tr = results.shift(); + if(tr){ + var td = document.createElement("div"); + var text = tr[0]; + var i = text.toLowerCase().indexOf(typedText.toLowerCase()); + if(i >= 0) { + var pre = text.substring(0, i); + var matched = text.substring(i, typedText.length); + var post = text.substring(i + typedText.length); + + td.appendChild(document.createTextNode(pre)); + var boldNode = document.createElement("b"); + td.appendChild(boldNode); + boldNode.appendChild(document.createTextNode(matched)); + td.appendChild(document.createTextNode(post)); + } else { + 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(); + }, + + postCreate : function() { + struts.widget.ComboBox.superclass.postCreate.apply(this); + + //events + if(!dojo.string.isBlank(this.refreshListenTopic)) { + var self = this; + dojo.event.topic.subscribe(this.refreshListenTopic, function() { + self.dataProvider.getData(self.dataUrl); + }); + } + if(!dojo.string.isBlank(this.onValueChangedPublishTopic)) { + dojo.event.topic.registerPublisher(this.onValueChangedPublishTopic, this, "onValueChanged"); + } + } +});