You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@struts.apache.org by mu...@apache.org on 2008/06/22 17:28:01 UTC

svn commit: r670371 [3/4] - in /struts/struts2/trunk/plugins/dojo/src: main/resources/org/apache/struts2/static/dojo/ main/resources/org/apache/struts2/static/dojo/struts/widget/ profile/

Modified: struts/struts2/trunk/plugins/dojo/src/main/resources/org/apache/struts2/static/dojo/struts_dojo.js.uncompressed.js
URL: http://svn.apache.org/viewvc/struts/struts2/trunk/plugins/dojo/src/main/resources/org/apache/struts2/static/dojo/struts_dojo.js.uncompressed.js?rev=670371&r1=670370&r2=670371&view=diff
==============================================================================
--- struts/struts2/trunk/plugins/dojo/src/main/resources/org/apache/struts2/static/dojo/struts_dojo.js.uncompressed.js (original)
+++ struts/struts2/trunk/plugins/dojo/src/main/resources/org/apache/struts2/static/dojo/struts_dojo.js.uncompressed.js Sun Jun 22 08:28:01 2008
@@ -1,13 +1,3 @@
-/*
-	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
-*/
-
 if(typeof dojo == "undefined"){
 
 // TODOC: HOW TO DOC THE BELOW?
@@ -921,7 +911,7 @@
 
 //These two functions are placed outside of preloadLocalizations
 //So that the xd loading can use/override them.
-dojo.hostenv.localesGenerated /***BUILD:localesGenerated***/; // value will be inserted here at build time, if necessary
+dojo.hostenv.localesGenerated =["ROOT","es-es","es","it-it","pt-br","de","fr-fr","zh-cn","pt","en-us","zh","fr","zh-tw","it","en-gb","xx","de-de","ko-kr","ja-jp","ko","en","ja"]; // value will be inserted here at build time, if necessary
 
 dojo.hostenv.registerNlsPrefix = function(){
 // summary:
@@ -22087,3 +22077,9277 @@
 	}
 );
 
+/*
+ * $Id$
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+dojo.provide("struts.widget.Bind");
+
+
+
+
+
+dojo.widget.defineWidget(
+  "struts.widget.Bind",
+  dojo.widget.HtmlWidget, {
+  widgetType : "Bind",
+  executeScripts : false,
+  scriptSeparation : false,
+  targets : "",
+  targetsArray : null,
+  href : "",
+  handler : "",
+
+  //messages
+  loadingText : "Loading...",
+  errorText : "",
+  showError : true,
+  showLoading : false,
+
+  //pub/sub events
+  listenTopics : "",
+  notifyTopics : "",
+  notifyTopicsArray : null,
+  beforeNotifyTopics : "",
+  beforeNotifyTopicsArray : null,
+  afterNotifyTopics : "",
+  afterNotifyTopicsArray : null,
+  errorNotifyTopics : "",
+  errorNotifyTopicsArray : null,
+
+  formId : "",
+  formFilter : "",
+  formNode : null,
+
+  events : "",
+  indicator : "",
+
+  parseContent : true,
+  
+  highlightColor : "",
+  highlightDuration : 2000,
+  
+  validate : false,
+  ajaxAfterValidation : false,
+  
+  //used for scripts downloading & caching
+  cacheContent : true,
+  //run script on its own scope
+  scriptSeparation : true,
+  //scope for the cript separation
+  scriptScope : null,
+  transport : "",
+   
+  postCreate : function() {
+    var self = this;
+
+    //attach listeners
+    if(!dojo.string.isBlank(this.listenTopics)) {
+      this.log("Listening to " + this.listenTopics + " to refresh");
+      var topics = this.listenTopics.split(",");
+      if(topics) {
+        dojo.lang.forEach(topics, function(topic){
+          dojo.event.topic.subscribe(topic, self, "reloadContents");
+        });
+      }
+    }
+
+    //topics
+    if(!dojo.string.isBlank(this.notifyTopics)) {
+      this.notifyTopicsArray = this.notifyTopics.split(",");
+    }
+    
+    //before topics
+    if(!dojo.string.isBlank(this.beforeNotifyTopics)) {
+      this.beforeNotifyTopicsArray = this.beforeNotifyTopics.split(",");
+    }
+    
+    //after topics
+    if(!dojo.string.isBlank(this.afterNotifyTopics)) {
+      this.afterNotifyTopicsArray = this.afterNotifyTopics.split(",");
+    }
+    
+    //error topics
+    if(!dojo.string.isBlank(this.errorNotifyTopics)) {
+      this.errorNotifyTopicsArray = this.errorNotifyTopics.split(",");
+    }
+
+    if(!dojo.string.isBlank(this.targets)) {
+      //split targets
+      this.targetsArray = this.targets.split(",");
+    }
+
+    if(!dojo.string.isBlank(this.events)) {
+      var eventsArray = this.events.split(",");
+      if(eventsArray && this.domNode) {
+        dojo.lang.forEach(eventsArray, function(event){
+           dojo.event.connect(self.domNode, event, function(evt) {
+             evt.preventDefault();
+             evt.stopPropagation();
+             self.reloadContents();
+           });
+        });
+      }
+    }
+
+    if(dojo.string.isBlank(this.formId)) {
+      //no formId, see if we are inside a form
+      this.formNode = dojo.dom.getFirstAncestorByTag(this.domNode, "form");
+    } else {
+      this.formNode = dojo.byId(this.formId);
+    }
+
+    if(this.formNode && dojo.string.isBlank(this.href)) {
+      this.href = this.formNode.action;
+    }
+  },
+
+  highlight : function() {
+    if(!dojo.string.isBlank(this.highlightColor)) {
+      var nodes = [];
+      //add nodes to array
+      dojo.lang.forEach(this.targetsArray, function(target) {
+        var node = dojo.byId(target);
+        if(node) {
+          nodes.push(node);
+        }
+      });
+      var effect = dojo.lfx.html.highlight(nodes, this.highlightColor, this.highlightDuration);
+      effect.play();    
+    }
+  },
+  
+  log : function(text) {
+    dojo.debug("[" + (this.widgetId ? this.widgetId : "unknown")  + "] " + text);
+  },
+
+  setContent : function(text) {
+    if(this.targetsArray) {
+      var self = this;
+	  var xmlParser = new dojo.xml.Parse();
+      dojo.lang.forEach(this.targetsArray, function(target) {
+        var node = dojo.byId(target);
+        if(node) {
+          node.innerHTML = text;
+  
+          if(self.parseContent && text != self.loadingText){
+            var frag  = xmlParser.parseElement(node, null, true);
+            dojo.widget.getParser().createSubComponents(frag, dojo.widget.byId(target));
+          }
+        } else {
+          self.log("Unable to find target: " + node);
+        }
+      });
+    }
+  },
+  
+  bindHandler : function(type, data, e) {
+    //hide indicator
+    dojo.html.hide(this.indicator);
+    
+    //publish topics
+    this.notify(data, type, e);
+    
+    if(type == "load") {
+      if(this.validate) {
+        StrutsUtils.clearValidationErrors(this.formNode);
+        //validation is active for this action
+        var errors = StrutsUtils.getValidationErrors(data);
+        if(errors && errors.fieldErrors) {
+          //validation failed
+          StrutsUtils.showValidationErrors(this.formNode, errors);
+          return;
+        } else {
+          //validation passed
+          if(!this.ajaxAfterValidation && this.formNode) {
+            //regular submit
+            this.formNode.submit();
+            return;
+          }
+        }
+      } 
+      
+      // no validation or validation passed
+      if(this.executeScripts) {
+        //parse text to extract content and javascript
+        var parsed = this.parse(data);
+         
+        //update targets content
+        this.setContent(parsed.text);
+        
+        //execute scripts
+        this._executeScripts(parsed.scripts);
+      }
+      else {
+        this.setContent(data);
+      }
+      this.highlight();
+    } else {
+      if(this.showError) {
+        var message = dojo.string.isBlank(this.errorText) ? e.message : this.errorText;
+        this.setContent(message);
+      }
+    }
+  },
+
+  notify : function(data, type, e) {
+    var self = this;
+    //general topics
+    if(this.notifyTopicsArray) {
+      dojo.lang.forEach(this.notifyTopicsArray, function(topic) {
+        try {
+          dojo.event.topic.publish(topic, data, type, e, self);
+        } catch(ex){
+		  self.log(ex);
+        }
+      });
+    }
+    
+    //before, after and error topics
+    var topicsArray = null;
+    switch(type) {
+      case "before":
+        this.notifyTo(this.beforeNotifyTopicsArray, null, e);
+        break;
+      case "load":
+        this.notifyTo(this.afterNotifyTopicsArray, data, e);
+        break;
+      case "error":
+        this.notifyTo(this.errorNotifyTopicsArray, data, e);
+        break;
+    }
+  },
+  
+  notifyTo : function(topicsArray, data, e) {
+    var self = this;
+    if(topicsArray) {
+      dojo.lang.forEach(topicsArray, function(topic) {
+      try {
+        if(data != null) {
+          dojo.event.topic.publish(topic, data, e, self);
+        } else {
+          dojo.event.topic.publish(topic, e, self);
+        }
+      } catch(ex){
+        self.log(ex);
+      }
+      });
+    }
+  },
+
+  onDownloadStart : function(event) {
+    if(this.showLoading && !dojo.string.isBlank(this.loadingText)) {
+      event.text = this.loadingText;
+    }
+  },
+
+  reloadContents : function(evt) {
+    if(!dojo.string.isBlank(this.handler)) {
+      //use custom handler
+      this.log("Invoking handler: " + this.handler);
+      window[this.handler](this, this.domNode);
+    }
+    else {
+      try {
+          var self = this;
+          var request = {cancel: false};
+          this.notify(this.widgetId, "before", request);
+          if(request.cancel) {
+            this.log("Request canceled");
+            return;
+          }
+
+          //if the href is null, we still publish the notify topics
+          if(dojo.string.isBlank(this.href)) {
+            return;
+          }
+
+          //if there is a parent form, and it has a "onsubmit"
+          //execute it, validation is usually there, except is validation == true
+          //on which case it is ajax validation, instead of client side
+          if(!this.validate && this.formNode && this.formNode.onsubmit != null) {
+            var makeRequest = this.formNode.onsubmit.call(evt);
+            if(makeRequest != null && !makeRequest) {
+              this.log("Request canceled by 'onsubmit' of the form");
+              return;
+            }
+          }
+
+          //show indicator
+          dojo.html.show(this.indicator);
+		  if(this.showLoading) {
+            this.setContent(this.loadingText);
+          }
+          
+          var tmpHref = this.href;
+          tmpHref = tmpHref + (tmpHref.indexOf("?") > -1 ? "&" : "?") + "struts.enableJSONValidation=true";
+          if(!this.ajaxAfterValidation && this.validate) {
+            tmpHref = tmpHref + (tmpHref.indexOf("?") > -1 ? "&" : "?") + "struts.validateOnly=true";
+          }  
+          
+          if(dojo.dom.isTag(this.domNode, "INPUT", "input") 
+             && this.events == "onclick" 
+             && this.domNode.type == "submit"
+             && !dojo.string.isBlank(this.domNode.name)
+             && !dojo.string.isBlank(this.domNode.value)) {
+             var enc = /utf/i.test("") ? encodeURIComponent : dojo.string.encodeAscii
+             tmpHref = tmpHref + (tmpHref.indexOf("?") > -1 ? "&" : "?") + enc(this.domNode.name) + "=" + enc(this.domNode.value);
+          }
+
+          dojo.io.bind({
+            url: tmpHref,
+            useCache: false,
+            preventCache: true,
+            formNode: self.formNode,
+            formFilter: window[self.formFilter],
+            transport: self.transport,
+            handler: function(type, data, e) {
+              dojo.lang.hitch(self, "bindHandler")(type, data, e);
+            },
+            mimetype: "text/html"
+         });
+      }
+      catch(ex) {
+        if(this.showError) {
+          var message = dojo.string.isBlank(this.errorText) ? ex : this.errorText;
+          this.setContent(message);
+        }  
+      }
+    }
+  },
+
+  //from Dojo's ContentPane
+  parse : function(s) {
+    this.log("Parsing: " + s);
+    var match = [];
+    var tmp = [];
+    var scripts = [];
+    while(match){
+      match = s.match(/<script([^>]*)>([\s\S]*?)<\/script>/i);
+      if(!match){ break; }
+      if(match[1]){
+        attr = match[1].match(/src=(['"]?)([^"']*)\1/i);
+        if(attr){
+          // remove a dojo.js or dojo.js.uncompressed.js from remoteScripts
+          // we declare all files with dojo.js as bad, regardless of folder
+          var tmp2 = attr[2].search(/.*(\bdojo\b(?:\.uncompressed)?\.js)$/);
+          if(tmp2 > -1){
+            this.log("Security note! inhibit:"+attr[2]+" from  beeing loaded again.");
+          }
+        }
+      }
+      if(match[2]){
+        // strip out all djConfig variables from script tags nodeValue
+        // this is ABSOLUTLY needed as reinitialize djConfig after dojo is initialised
+        // makes a dissaster greater than Titanic, update remove writeIncludes() to
+        var sc = match[2].replace(/(?:var )?\bdjConfig\b(?:[\s]*=[\s]*\{[^}]+\}|\.[\w]*[\s]*=[\s]*[^;\n]*)?;?|dojo\.hostenv\.writeIncludes\(\s*\);?/g, "");
+        if(!sc){ continue; }
+
+        // cut out all dojo.require (...) calls, if we have execute
+        // scripts false widgets dont get there require calls
+        // does suck out possible widgetpackage registration as well
+        tmp = [];
+        while(tmp){
+          tmp = sc.match(/dojo\.(?:(?:require(?:After)?(?:If)?)|(?:widget\.(?:manager\.)?registerWidgetPackage)|(?:(?:hostenv\.)?setModulePrefix))\((['"]).*?\1\)\s*;?/);
+          if(!tmp){ break;}
+          sc = sc.replace(tmp[0], "");
+        }
+        scripts.push(sc);
+      }
+      s = s.replace(/<script[^>]*>[\s\S]*?<\/script>/i, "");
+    }
+
+    return {
+      text: s,
+      scripts: scripts
+    };
+  }, 
+  
+  //from Dojo content pane
+  _executeScripts : function (scripts) {
+    var self = this;
+    var tmp = "", code = "";
+    for (var i = 0; i < scripts.length; i++) {
+        if (scripts[i].path) {
+            dojo.io.bind(this._cacheSetting({"url":scripts[i].path, "load":function (type, scriptStr) {
+                dojo.lang.hitch(self, tmp = ";" + scriptStr);
+            }, "error":function (type, error) {
+                error.text = type + " downloading remote script";
+                self._handleDefaults.call(self, error, "onExecError", "debug");
+            }, "mimetype":"text/plain", "sync":true}, this.cacheContent));
+            code += tmp;
+        } else {
+            code += scripts[i];
+        }
+    }
+    try {
+        if (this.scriptSeparation) {
+            delete this.scriptScope;
+            this.scriptScope = new (new Function("_container_", code + "; return this;"))(self);
+        } else {
+            var djg = dojo.global();
+            if (djg.execScript) {
+                djg.execScript(code);
+            } else {
+                var djd = dojo.doc();
+                var sc = djd.createElement("script");
+                sc.appendChild(djd.createTextNode(code));
+                (this.containerNode || this.domNode).appendChild(sc);
+            }
+        }
+    }
+    catch (e) {
+        e.text = "Error running scripts from content:\n" + e.description;
+        this.log(e);
+    }
+ },
+ 
+ _cacheSetting : function (bindObj, useCache) {
+    for (var x in this.bindArgs) {
+        if (dojo.lang.isUndefined(bindObj[x])) {
+            bindObj[x] = this.bindArgs[x];
+        }
+    }
+    if (dojo.lang.isUndefined(bindObj.useCache)) {
+        bindObj.useCache = useCache;
+    }
+    if (dojo.lang.isUndefined(bindObj.preventCache)) {
+        bindObj.preventCache = !useCache;
+    }
+    if (dojo.lang.isUndefined(bindObj.mimetype)) {
+        bindObj.mimetype = "text/html";
+    }
+    return bindObj;
+ }
+ 
+});
+
+
+
+
+dojo.provide("dojo.lang.timing.Timer");
+
+
+dojo.lang.timing.Timer = function(/*int*/ interval){
+	// summary: Timer object executes an "onTick()" method repeatedly at a specified interval. 
+	//			repeatedly at a given interval.
+	// interval: Interval between function calls, in milliseconds.
+	this.timer = null;
+	this.isRunning = false;
+	this.interval = interval;
+
+	this.onStart = null;
+	this.onStop = null;
+};
+
+dojo.extend(dojo.lang.timing.Timer, {
+	onTick : function(){
+		// summary: Method called every time the interval passes.  Override to do something useful.
+	},
+		
+	setInterval : function(interval){
+		// summary: Reset the interval of a timer, whether running or not.
+		// interval: New interval, in milliseconds.
+		if (this.isRunning){
+			dj_global.clearInterval(this.timer);
+		}
+		this.interval = interval;
+		if (this.isRunning){
+			this.timer = dj_global.setInterval(dojo.lang.hitch(this, "onTick"), this.interval);
+		}
+	},
+	
+	start : function(){
+		// summary: Start the timer ticking.
+		// description: Calls the "onStart()" handler, if defined.
+		// 				Note that the onTick() function is not called right away, 
+		//				only after first interval passes.
+		if (typeof this.onStart == "function"){
+			this.onStart();
+		}
+		this.isRunning = true;
+		this.timer = dj_global.setInterval(dojo.lang.hitch(this, "onTick"), this.interval);
+	},
+	
+	stop : function(){
+		// summary: Stop the timer.
+		// description: Calls the "onStop()" handler, if defined.
+		if (typeof this.onStop == "function"){
+			this.onStop();
+		}
+		this.isRunning = false;
+		dj_global.clearInterval(this.timer);
+	}
+});
+
+/*
+ * $Id$
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+dojo.provide("struts.widget.BindDiv");
+
+
+
+
+
+dojo.widget.defineWidget(
+  "struts.widget.BindDiv",
+  dojo.widget.ContentPane, {
+    widgetType : "BindDiv",
+
+    //from ContentPane
+    href : "",
+    extractContent : false,
+    parseContent : false,
+    cacheContent : false,
+    refreshOnShow : false,
+    executeScripts : false,
+    preload : true,
+    
+    //update times
+    updateFreq : 0,
+    delay : 0,
+    autoStart : true,
+    timer : null,
+
+    //messages
+    loadingText : "Loading...",
+    showLoading : false,
+    errorText : "",
+    showError : true,
+
+    //pub/sub events
+    listenTopics : "",
+    notifyTopics : "",
+    notifyTopicsArray : null,
+    stopTimerListenTopics : "",
+    startTimerListenTopics : "",
+    beforeNotifyTopics : "",
+    beforeNotifyTopicsArray : null,
+    afterNotifyTopics : "",
+    afterNotifyTopicsArray : null,
+    errorNotifyTopics : "",
+    errorNotifyTopicsArray : null,
+    
+
+    //callbacks
+    beforeLoading : "",
+    afterLoading : "",
+
+    formId : "",
+    formFilter : "",
+
+    indicator: "",
+
+	//make dojo process the content
+	parseContent : true,
+
+    highlightColor : "",
+    highlightDuration : 2000,
+    
+    //only used when inside a tabbedpanel
+    disabled : false,
+    
+    transport : "",
+    
+    onDownloadStart : function(event) {
+      if(!this.showLoading) {
+        event.returnValue = false;
+        return;
+      }
+      if(this.showLoading && !dojo.string.isBlank(this.loadingText)) {
+        event.text = this.loadingText;
+      }
+    },
+    
+    highlight : function() {
+      if(!dojo.string.isBlank(this.highlightColor)) {
+        var effect = dojo.lfx.html.highlight([this.domNode], this.highlightColor, this.highlightDuration);
+        effect.play();    
+      }        
+    },
+
+    onDownloadError : function(event) {
+      this.onError(event);
+    },
+
+    onContentError : function(event) {
+      this.onError(event);
+    },
+
+    onExecError : function(event) {
+      this.onError(event);
+    },
+
+    onError : function(event) {
+      if(this.showError) {
+        if(!dojo.string.isBlank(this.errorText)) {
+          event.text = this.errorText;
+        }
+      } else {
+        event.text = "";
+      }
+    },
+
+    notify : function(data, type, e) {
+      if(this.notifyTopicsArray) {
+        var self = this;
+        dojo.lang.forEach(this.notifyTopicsArray, function(topic) {
+          try {
+            dojo.event.topic.publish(topic, data, type, e, self);
+          } catch(ex) {
+            self.log(ex);
+          }
+        });
+      }
+      
+      //before, after and error topics
+      var topicsArray = null;
+      switch(type) {
+        case "before":
+          this.notifyTo(this.beforeNotifyTopicsArray, null, e);
+          break;
+        case "load":
+          this.notifyTo(this.afterNotifyTopicsArray, data, e);
+          break;
+        case "error":
+          this.notifyTo(this.errorNotifyTopicsArray, data, e);
+          break;
+      }
+    },
+    
+    notifyTo : function(topicsArray, data, e) {
+      var self = this;
+      if(topicsArray) {
+        dojo.lang.forEach(topicsArray, function(topic) {
+        try {
+          if(data != null) {
+            dojo.event.topic.publish(topic, data, e, self);
+          } else {
+            dojo.event.topic.publish(topic, e, self);
+          }
+        } catch(ex){
+          self.log(ex);
+        }
+        });
+      }
+    },
+
+    postCreate : function(args, frag) {
+      if (this.handler !== "") {
+          this.setHandler(this.handler);
+      }
+
+      var self = this;
+      var hitchedRefresh = function() {
+        dojo.lang.hitch(self, "refresh")();
+      };
+      var hitchedStartTimer = function() {
+        dojo.lang.hitch(self, "startTimer")();
+      };
+
+      if(this.updateFreq > 0) {
+        //there is a timer
+        this.timer = new dojo.lang.timing.Timer(this.updateFreq);
+        this.timer.onTick = hitchedRefresh;
+
+        if(this.autoStart) {
+          //start the timer
+          if(this.delay > 0) {
+            //start time after delay
+            dojo.lang.setTimeout(hitchedStartTimer, this.delay);
+          } else {
+            //start timer now
+            this.startTimer();
+          }
+        }
+      } else {
+        //no timer
+        if(this.delay > 0) {
+          //load after delay
+          dojo.lang.setTimeout(hitchedRefresh, this.delay);
+        }
+      }
+
+      //attach listeners
+      if(!dojo.string.isBlank(this.listenTopics)) {
+        this.log("Listening to " + this.listenTopics + " to refresh");
+        var topics = this.listenTopics.split(",");
+        if(topics) {
+          dojo.lang.forEach(topics, function(topic){
+            dojo.event.topic.subscribe(topic, self, "refresh");
+          });
+        }
+      }
+
+      if(!dojo.string.isBlank(this.stopTimerListenTopics)) {
+        this.log("Listening to " + this.stopTimerListenTopics + " to stop timer");
+        var stopTopics = this.stopTimerListenTopics.split(",");
+        if(stopTopics) {
+          dojo.lang.forEach(stopTopics, function(topic){
+            dojo.event.topic.subscribe(topic, self, "stopTimer");
+          });
+        }
+      }
+
+      if(!dojo.string.isBlank(this.startTimerListenTopics)) {
+        this.log("Listening to " + this.stopTimerListenTopics + " to start timer");
+        var startTopics = this.startTimerListenTopics.split(",");
+        if(startTopics) {
+          dojo.lang.forEach(startTopics, function(topic){
+            dojo.event.topic.subscribe(topic, self, "startTimer");
+          });
+        }
+      }
+     
+      //notify topics
+      if(!dojo.string.isBlank(this.notifyTopics)) {
+        this.notifyTopicsArray = this.notifyTopics.split(",");
+      }
+      
+      //before topics
+      if(!dojo.string.isBlank(this.beforeNotifyTopics)) {
+        this.beforeNotifyTopicsArray = this.beforeNotifyTopics.split(",");
+      }
+      
+      //after topics
+      if(!dojo.string.isBlank(this.afterNotifyTopics)) {
+        this.afterNotifyTopicsArray = this.afterNotifyTopics.split(",");
+      }
+      
+      //error topics
+      if(!dojo.string.isBlank(this.errorNotifyTopics)) {
+        this.errorNotifyTopicsArray = this.errorNotifyTopics.split(",");
+      }
+      
+      if(this.isShowing() && this.preload && this.updateFreq <= 0 && this.delay <= 0) {
+        this.refresh();
+      }
+    },
+
+    _downloadExternalContent: function(url, useCache) {
+      
+      var request = {cancel: false};
+      this.notify(this.widgetId, "before", request);
+      if(request.cancel) {
+        return;
+      }
+
+      //show indicator
+      dojo.html.show(this.indicator);
+
+      this._handleDefaults("Loading...", "onDownloadStart");
+      var self = this;
+      dojo.io.bind({
+        url: url,
+        useCache: useCache,
+        preventCache: !useCache,
+        mimetype: "text/html",
+        formNode: dojo.byId(self.formId),
+        formFilter: window[self.formFilter],
+        transport: self.transport,
+        handler: function(type, data, e) {
+          //hide indicator
+          dojo.html.hide(self.indicator);
+
+          self.notify(data, type, e);
+
+          if(type == "load") {
+            self.onDownloadEnd.call(self, url, data);
+            self.highlight();
+          } else {
+            // works best when from a live server instead of from file system
+            self._handleDefaults.call(self, "Error loading '" + url + "' (" + e.status + " "+  e.statusText + ")", "onDownloadError");
+            self.onLoad();
+          }
+        }
+      });
+     },
+
+    log : function(text) {
+      dojo.debug("[" + this.widgetId + "] " + text);
+    },
+
+    stopTimer : function() {
+      if(this.timer && this.timer.isRunning) {
+        this.log("stopping timer");
+        this.timer.stop();
+      }
+    },
+
+    startTimer : function() {
+      if(this.timer && !this.timer.isRunning) {
+        this.log("starting timer with update interval " + this.updateFreq);
+        this.timer.start();
+      }
+    },
+    
+    //from Dojo's ContentPane
+    //TODO: remove when fixed on Dojo (WW-1869)
+    splitAndFixPaths:function (s, url) {
+      var titles = [], scripts = [], tmp = [];
+      var match = [], requires = [], attr = [], styles = [];
+      var str = "", path = "", fix = "", tagFix = "", tag = "", origPath = "";
+      if (!url) {
+        url = "./";
+      }
+      if (s) {
+        var regex = /<title[^>]*>([\s\S]*?)<\/title>/i;
+        while (match = regex.exec(s)) {
+          titles.push(match[1]);
+          s = s.substring(0, match.index) + s.substr(match.index + match[0].length);
+        }
+        if (this.adjustPaths) {
+          var regexFindTag = /<[a-z][a-z0-9]*[^>]*\s(?:(?:src|href|style)=[^>])+[^>]*>/i;
+          var regexFindAttr = /\s(src|href|style)=(['"]?)([\w()\[\]\/.,\\'"-:;#=&?\s@!]+?)\2/i;
+          var regexProtocols = /^(?:[#]|(?:(?:https?|ftps?|file|javascript|mailto|news):))/;
+          while (tag = regexFindTag.exec(s)) {
+            str += s.substring(0, tag.index);
+            s = s.substring((tag.index + tag[0].length), s.length);
+            tag = tag[0];
+            tagFix = "";
+            while (attr = regexFindAttr.exec(tag)) {
+              path = "";
+              origPath = attr[3];
+              switch (attr[1].toLowerCase()) {
+                case "src":
+                case "href":
+                if (regexProtocols.exec(origPath)) {
+                  path = origPath;
+                } else {
+                  path = (new dojo.uri.Uri(url, origPath).toString());
+                }
+                break;
+                case "style":
+                path = dojo.html.fixPathsInCssText(origPath, url);
+                break;
+                default:
+                path = origPath;
+              }
+              fix = " " + attr[1] + "=" + attr[2] + path + attr[2];
+              tagFix += tag.substring(0, attr.index) + fix;
+              tag = tag.substring((attr.index + attr[0].length), tag.length);
+            }
+            str += tagFix + tag;
+          }
+          s = str + s;
+        }
+        regex = /(?:<(style)[^>]*>([\s\S]*?)<\/style>|<link ([^>]*rel=['"]?stylesheet['"]?[^>]*)>)/i;
+        while (match = regex.exec(s)) {
+          if (match[1] && match[1].toLowerCase() == "style") {
+            styles.push(dojo.html.fixPathsInCssText(match[2], url));
+          } else {
+            if (attr = match[3].match(/href=(['"]?)([^'">]*)\1/i)) {
+              styles.push({path:attr[2]});
+            }
+          }
+          s = s.substring(0, match.index) + s.substr(match.index + match[0].length);
+        }
+        var regex = /<script([^>]*)>([\s\S]*?)<\/script>/i;
+        var regexSrc = /src=(['"]?)([^"']*)\1/i;
+        var regexDojoJs = /.*(\bdojo\b\.js(?:\.uncompressed\.js)?)$/;
+        var regexInvalid = /(?:var )?\bdjConfig\b(?:[\s]*=[\s]*\{[^}]+\}|\.[\w]*[\s]*=[\s]*[^;\n]*)?;?|dojo\.hostenv\.writeIncludes\(\s*\);?/g;
+        var regexRequires = /dojo\.(?:(?:require(?:After)?(?:If)?)|(?:widget\.(?:manager\.)?registerWidgetPackage)|(?:(?:hostenv\.)?setModulePrefix|registerModulePath)|defineNamespace)\((['"]).*?\1\)\s*;?/;
+        while (match = regex.exec(s)) {
+          if (this.executeScripts && match[1]) {
+            if (attr = regexSrc.exec(match[1])) {
+              if (regexDojoJs.exec(attr[2])) {
+                dojo.debug("Security note! inhibit:" + attr[2] + " from  being loaded again.");
+              } else {
+                scripts.push({path:attr[2]});
+              }
+            }
+          }
+          if (match[2]) {
+            var sc = match[2].replace(regexInvalid, "");
+            if (!sc) {
+              continue;
+            }
+            while (tmp = regexRequires.exec(sc)) {
+              requires.push(tmp[0]);
+              sc = sc.substring(0, tmp.index) + sc.substr(tmp.index + tmp[0].length);
+            }
+            if (this.executeScripts) {
+              scripts.push(sc);
+            }
+          }
+          s = s.substr(0, match.index) + s.substr(match.index + match[0].length);
+        }
+        if (this.extractContent) {
+          match = s.match(/<body[^>]*>\s*([\s\S]+)\s*<\/body>/im);
+          if (match) {
+            s = match[1];
+          }
+        }
+        if (this.executeScripts && this.scriptSeparation) {
+          var regex = /(<[a-zA-Z][a-zA-Z0-9]*\s[^>]*?\S=)((['"])[^>]*scriptScope[^>]*>)/;
+          var regexAttr = /([\s'";:\(])scriptScope(.*)/;
+          str = "";
+          while (tag = regex.exec(s)) {
+            tmp = ((tag[3] == "'") ? "\"" : "'");
+            fix = "";
+            str += s.substring(0, tag.index) + tag[1];
+            while (attr = regexAttr.exec(tag[2])) {
+              tag[2] = tag[2].substring(0, attr.index) + attr[1] + "dojo.widget.byId(" + tmp + this.widgetId + tmp + ").scriptScope" + attr[2];
+            }
+            str += tag[2];
+            s = s.substr(tag.index + tag[0].length);
+          }
+          s = str + s;
+        }
+      }
+      return {"xml":s, "styles":styles, "titles":titles, "requires":requires, "scripts":scripts, "url":url};
+    }
+});
+
+/*
+ * $Id$
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+dojo.provide("struts.widget.BindAnchor");
+
+
+
+
+
+dojo.widget.defineWidget(
+  "struts.widget.BindAnchor",
+  struts.widget.Bind, {
+  widgetType : "BindAnchor",
+
+  events: "onclick",
+
+  postCreate : function() {
+     struts.widget.BindAnchor.superclass.postCreate.apply(this);
+     this.domNode.href = "#";
+  }
+});
+
+
+
+
+dojo.provide("dojo.widget.html.stabile");
+
+dojo.widget.html.stabile = {
+	// summary: Maintain state of widgets when user hits back/forward button
+
+	// Characters to quote in single-quoted regexprs
+	_sqQuotables: new RegExp("([\\\\'])", "g"),
+
+	// Current depth.
+	_depth: 0,
+
+	// Set to true when calling v.toString, to sniff for infinite
+	// recursion.
+	_recur: false,
+
+	// Levels of nesting of Array and object displays.
+	// If when >= depth, no display or array or object internals.
+	depthLimit: 2
+};
+
+//// PUBLIC METHODS
+
+dojo.widget.html.stabile.getState = function(id){
+	// summary
+	//	Get the state stored for the widget with the given ID, or undefined
+	//	if none.
+
+	dojo.widget.html.stabile.setup();
+	return dojo.widget.html.stabile.widgetState[id];
+}
+
+dojo.widget.html.stabile.setState = function(id, state, isCommit){
+	// summary
+	//		Set the state stored for the widget with the given ID.  If isCommit
+	//		is true, commits all widget state to more stable storage.
+
+	dojo.widget.html.stabile.setup();
+	dojo.widget.html.stabile.widgetState[id] = state;
+	if(isCommit){
+		dojo.widget.html.stabile.commit(dojo.widget.html.stabile.widgetState);
+	}
+}
+
+dojo.widget.html.stabile.setup = function(){
+	// summary
+	//		Sets up widgetState: a hash keyed by widgetId, maps to an object
+	//		or array writable with "describe".  If there is data in the widget
+	//		storage area, use it, otherwise initialize an empty object.
+
+	if(!dojo.widget.html.stabile.widgetState){
+		var text = dojo.widget.html.stabile._getStorage().value;
+		dojo.widget.html.stabile.widgetState = text ? dj_eval("("+text+")") : {};
+	}
+}
+
+dojo.widget.html.stabile.commit = function(state){
+	// summary
+	//		Commits all widget state to more stable storage, so if the user
+	//		navigates away and returns, it can be restored.
+
+	dojo.widget.html.stabile._getStorage().value = dojo.widget.html.stabile.description(state);
+}
+
+dojo.widget.html.stabile.description = function(v, showAll){
+	// summary
+	//		Return a JSON "description string" for the given value.
+	//		Supports only core JavaScript types with literals, plus Date,
+	//		and cyclic structures are unsupported.
+	//		showAll defaults to false -- if true, this becomes a simple symbolic
+	//		object dumper, but you cannot "eval" the output.
+
+	// Save and later restore dojo.widget.html.stabile._depth;
+	var depth = dojo.widget.html.stabile._depth;
+
+	var describeThis = function() {
+		 return this.description(this, true);
+	} 
+	
+	try {
+
+		if(v===void(0)){
+			return "undefined";
+		}
+		if(v===null){
+			return "null";
+		}
+		if(typeof(v)=="boolean" || typeof(v)=="number"
+		    || v instanceof Boolean || v instanceof Number){
+			return v.toString();
+		}
+
+		if(typeof(v)=="string" || v instanceof String){
+			// Quote strings and their contents as required.
+			// Replacing by $& fails in IE 5.0
+			var v1 = v.replace(dojo.widget.html.stabile._sqQuotables, "\\$1"); 
+			v1 = v1.replace(/\n/g, "\\n");
+			v1 = v1.replace(/\r/g, "\\r");
+			// Any other important special cases?
+			return "'"+v1+"'";
+		}
+
+		if(v instanceof Date){
+			// Create a data constructor.
+			return "new Date("+d.getFullYear+","+d.getMonth()+","+d.getDate()+")";
+		}
+
+		var d;
+		if(v instanceof Array || v.push){
+			// "push" test needed for KHTML/Safari, don't know why -cp
+
+			if(depth>=dojo.widget.html.stabile.depthLimit)
+			  return "[ ... ]";
+
+			d = "[";
+			var first = true;
+			dojo.widget.html.stabile._depth++;
+			for(var i=0; i<v.length; i++){
+				// Skip functions and undefined values
+				// if(v[i]==undef || typeof(v[i])=="function")
+				//   continue;
+				if(first){
+					first = false;
+				}else{
+					d += ",";
+				}
+				d+=arguments.callee(v[i], showAll);
+			}
+			return d+"]";
+		}
+
+		if(v.constructor==Object
+		    || v.toString==describeThis){
+			if(depth>=dojo.widget.html.stabile.depthLimit)
+			  return "{ ... }";
+
+			// Instanceof Hash is good, or if we just use Objects,
+			// we can say v.constructor==Object.
+			// IE (5?) lacks hasOwnProperty, but perhaps objects do not always
+			// have prototypes??
+			if(typeof(v.hasOwnProperty)!="function" && v.prototype){
+				throw new Error("description: "+v+" not supported by script engine");
+			}
+			var first = true;
+			d = "{";
+			dojo.widget.html.stabile._depth++;
+			for(var key in v){
+				// Skip values that are functions or undefined.
+				if(v[key]==void(0) || typeof(v[key])=="function")
+					continue;
+				if(first){
+					first = false;
+				}else{
+					d += ", ";
+				}
+				var kd = key;
+				// If the key is not a legal identifier, use its description.
+				// For strings this will quote the stirng.
+				if(!kd.match(/^[a-zA-Z_][a-zA-Z0-9_]*$/)){
+					kd = arguments.callee(key, showAll);
+				}
+				d += kd+": "+arguments.callee(v[key], showAll);
+			}
+			return d+"}";
+		}
+
+		if(showAll){
+			if(dojo.widget.html.stabile._recur){
+				// Save the original definitions of toString;
+				var objectToString = Object.prototype.toString;
+				return objectToString.apply(v, []);
+			}else{
+				dojo.widget.html.stabile._recur = true;
+				return v.toString();
+			}
+		}else{
+			// log("Description? "+v.toString()+", "+typeof(v));
+			throw new Error("Unknown type: "+v);
+			return "'unknown'";
+		}
+
+	} finally {
+		// Always restore the global current depth.
+		dojo.widget.html.stabile._depth = depth;
+	}
+
+}
+
+
+
+//// PRIVATE TO MODULE
+
+dojo.widget.html.stabile._getStorage = function(){
+	// summary
+	//	Gets an object (form field) with a read/write "value" property.
+
+	if (dojo.widget.html.stabile.dataField) {
+		return dojo.widget.html.stabile.dataField;
+	}
+	var form = document.forms._dojo_form;
+	return dojo.widget.html.stabile.dataField = form ? form.stabile : {value: ""};
+}
+
+
+dojo.provide("dojo.widget.ComboBox");
+
+
+
+
+
+
+
+
+
+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.moduleUri("dojo.widget", "templates/images/combo_box_arrow.png"),
+
+		// dropdownToggle: String
+		//		Animation effect for showing/displaying drop down box
+		dropdownToggle: "fade",
+
+		templateString:"<span _=\"whitespace and CR's between tags adds &nbsp; in FF\"\r\n\tclass=\"dojoComboBoxOuter\"\r\n\t><input style=\"display:none\"  tabindex=\"-1\" name=\"\" value=\"\" \r\n\t\tdojoAttachPoint=\"comboBoxValue\"\r\n\t><input style=\"display:none\"  tabindex=\"-1\" name=\"\" value=\"\" \r\n\t\tdojoAttachPoint=\"comboBoxSelectionValue\"\r\n\t><input type=\"text\" autocomplete=\"off\" class=\"dojoComboBox\"\r\n\t\tdojoAttachEvent=\"key:_handleKeyEvents; keyUp: onKeyUp; compositionEnd; onResize;\"\r\n\t\tdojoAttachPoint=\"textInputNode\"\r\n\t><img hspace=\"0\"\r\n\t\tvspace=\"0\"\r\n\t\tclass=\"dojoComboBox\"\r\n\t\tdojoAttachPoint=\"downArrowNode\"\r\n\t\tdojoAttachEvent=\"onMouseUp: handleArrowClick; onResize;\"\r\n\t\tsrc=\"${this.buttonSrc}\"\r\n></span>\r\n",
+		templateCssString:".dojoComboBoxOuter {\r\n\tborder: 0px !important;\r\n\tmargin: 0px !important;\r\n\tpadding: 0px !important;\r\n\tbackground: transparent !important;\r\n\twhite-space: nowrap !important;\r\n}\r\n\r\n.dojoComboBox {\r\n\tborder: 1px inset #afafaf;\r\n\tmargin: 0px;\r\n\tpadding: 0px;\r\n\tvertical-align: middle !important;\r\n\tfloat: none !important;\r\n\tposition: static !important;\r\n\tdisplay: inline !important;\r\n}\r\n\r\n/* the input box */\r\ninput.dojoComboBox {\r\n\tborder-right-width: 0px !important; \r\n\tmargin-right: 0px !important;\r\n\tpadding-right: 0px !important;\r\n}\r\n\r\n/* the down arrow */\r\nimg.dojoComboBox {\r\n\tborder-left-width: 0px !important;\r\n\tpadding-left: 0px !important;\r\n\tmargin-left: 0px !important;\r\n}\r\n\r\n/* IE vertical-alignment calculations can be off by +-1 but these margins are collapsed away */\r\n.dj_ie img.dojoComboBox {\r\n\tmargin-top: 1px; \r\n\tmargin-bottom: 1px; \r\n}\r\n\r\n/* the drop down 
 */\r\n.dojoComboBoxOptions {\r\n\tfont-family: Verdana, Helvetica, Garamond, sans-serif;\r\n\t/* font-size: 0.7em; */\r\n\tbackground-color: white;\r\n\tborder: 1px solid #afafaf;\r\n\tposition: absolute;\r\n\tz-index: 1000; \r\n\toverflow: auto;\r\n\tcursor: default;\r\n}\r\n\r\n.dojoComboBoxItem {\r\n\tpadding-left: 2px;\r\n\tpadding-top: 2px;\r\n\tmargin: 0px;\r\n}\r\n\r\n.dojoComboBoxItemEven {\r\n\tbackground-color: #f4f4f4;\r\n}\r\n\r\n.dojoComboBoxItemOdd {\r\n\tbackground-color: white;\r\n}\r\n\r\n.dojoComboBoxItemHighlight {\r\n\tbackground-color: #63709A;\r\n\tcolor: white;\r\n}\r\n",templateCssPath: dojo.uri.moduleUri("dojo.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);
+			}
+		}
+	}
+);
+
+/*
+ * $Id$
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+dojo.provide("struts.widget.ComboBox");
+
+
+
+
+struts.widget.ComboBoxDataProvider = function(combobox, node){
+  this.data = [];
+  this.searchLimit = combobox.searchLimit;
+  this.searchType = "STARTSTRING"; // may also be "STARTWORD" or "SUBSTRING"
+  this.caseSensitive = false;
+  // for caching optimizations
+  this._lastSearch = "";
+  this._lastSearchResults = null;
+
+  this.firstRequest = true;
+
+  this.cbox = combobox;
+  this.formId = this.cbox.formId;
+  this.formFilter = this.cbox.formFilter;
+  this.transport = this.cbox.transport;
+
+  this.getData = function(/*String*/ url){
+    //show indicator
+    dojo.html.show(this.cbox.indicator);
+
+    dojo.io.bind({
+      url: url,
+      formNode: dojo.byId(this.formId),
+      formFilter: window[this.formFilter],
+      transport: this.transport,
+      handler: dojo.lang.hitch(this, function(type, data, evt) {
+        //hide indicator
+        dojo.html.hide(this.cbox.indicator);
+
+        //if notifyTopics is published on the first request (onload)
+        //the value of listeners will be reset
+        if(!this.firstRequest || type == "error") {
+          this.cbox.notify.apply(this.cbox, [data, type, evt]);
+        }
+
+        this.firstRequest = false;
+        var arrData = null;
+        var dataByName = data[dojo.string.isBlank(this.cbox.dataFieldName) ? this.cbox.name : this.cbox.dataFieldName];
+        if(!dojo.lang.isArray(data)) {
+           //if there is a dataFieldName, take it
+           if(dataByName) {
+             if(dojo.lang.isArray(dataByName)) {
+                //ok, it is an array
+                arrData = dataByName;
+             } else if(dojo.lang.isObject(dataByName)) {
+                //it is an object, treat it like a map
+                arrData = [];
+                for(var key in dataByName){
+                    arrData.push([key, dataByName[key]]);
+                }
+             }
+           } else {
+             //try to find a match
+             var tmpArrData = [];
+             for(var key in data){
+               //does it start with the field name? take it
+               if(dojo.string.startsWith(key, this.cbox.name)) {
+                 arrData = data[key];
+                 break;
+               } else {
+                 //if nathing else is found, we will use values in this
+                 //object as the data
+                 tmpArrData.push([key, data[key]]);
+               }
+               //grab the first array found, we will use it if nothing else
+               //is found
+               if(!arrData && dojo.lang.isArray(data[key]) && !dojo.lang.isString(data[key])) {
+                 arrData = data[key];
+               }
+             }
+             if(!arrData) {
+               arrData = tmpArrData;
+             }
+           }
+
+           data = arrData;
+        }
+        this.setData(data);
+      }),
+      mimetype: "text/json"
+    });
+  };
+
+  this.startSearch = function (searchStr, callback) {
+    // FIXME: need to add timeout handling here!!
+    this._preformSearch(searchStr, callback);
+  };
+
+  this._preformSearch = function(/*String*/ searchStr, 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.data[x] || !this.data[x][0]) {
+        //needed for IE
+        continue;
+      }
+      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);
+  };
+
+  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;
+    //all ellements must be a key and value pair
+    for(var i = 0; i < this.data.length; i++) {
+      var element = this.data[i];
+      if(!dojo.lang.isArray(element)) {
+        this.data[i] = [element, element];
+      }
+    }
+  };
+
+
+  if(!dojo.string.isBlank(this.cbox.dataUrl) && this.cbox.preload){
+    this.getData(this.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){
+          this.cbox.setAllValues(keyValArr[0], keyValArr[1]);
+        }
+      }
+      this.setData(data);
+    }
+  }
+};
+
+dojo.widget.defineWidget(
+  "struts.widget.ComboBox",
+  dojo.widget.ComboBox, {
+  widgetType : "ComboBox",
+
+  dropdownHeight: 120,
+  dropdownWidth: 0,
+  itemHeight: 0,
+
+  listenTopics : "",
+  notifyTopics : "",
+  notifyTopicsArray : null,
+  beforeNotifyTopics : "",
+  beforeNotifyTopicsArray : null,
+  afterNotifyTopics : "",
+  afterNotifyTopicsArray : null,
+  errorNotifyTopics : "",
+  errorNotifyTopicsArray : null,
+  valueNotifyTopics : "",
+  valueNotifyTopicsArray : null,
+  indicator : "",
+
+  formId : "",
+  formFilter : "",
+  dataProviderClass: "struts.widget.ComboBoxDataProvider",
+
+  loadOnType : false,
+  loadMinimum : 3,
+
+  initialValue : "",
+  initialKey : "",
+
+  visibleDownArrow : true,
+  fadeTime : 100,
+
+  //dojo has "stringstart" which is invalid
+  searchType: "STARTSTRING",
+
+  dataFieldName : "",
+  keyName: "",
+  //embedded the style in the template string in 0.4.2 release, not good
+  templateCssString: null,
+  templateCssString:"/*\r\n * $Id$\r\n *\r\n * Licensed to the Apache Software Foundation (ASF) under one\r\n * or more contributor license agreements.  See the NOTICE file\r\n * distributed with this work for additional information\r\n * regarding copyright ownership.  The ASF licenses this file\r\n * to you under the Apache License, Version 2.0 (the\r\n * \"License\"); you may not use this file except in compliance\r\n * with the License.  You may obtain a copy of the License at\r\n *\r\n *  http://www.apache.org/licenses/LICENSE-2.0\r\n *\r\n * Unless required by applicable law or agreed to in writing,\r\n * software distributed under the License is distributed on an\r\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\r\n * KIND, either express or implied.  See the License for the\r\n * specific language governing permissions and limitations\r\n * under the License.\r\n */\r\n\r\n.dojoComboBoxOuter {\r\n\tborder: 0px !important;\r\n\tmargin: 0px !important;\r\n
 \tpadding: 0px !important;\r\n\tbackground: transparent !important;\r\n\twhite-space: nowrap !important;\r\n}\r\n\r\n.dojoComboBox {\r\n\tborder: 1px inset #afafaf;\r\n\tmargin: 0px;\r\n\tpadding: 0px;\r\n\tvertical-align: middle !important;\r\n\tfloat: none !important;\r\n\tposition: static !important;\r\n\tdisplay: inline;\r\n}\r\n\r\n/* the input box */\r\ninput.dojoComboBox {\r\n\tborder-right-width: 1px !important;\r\n\tmargin-right: 0px !important;\r\n\tpadding-right: 0px !important;\r\n}\r\n\r\n/* the down arrow */\r\nimg.dojoComboBox {\r\n\tborder-left-width: 0px !important;\r\n\tpadding-left: 0px !important;\r\n\tmargin-left: 0px !important;\r\n}\r\n\r\n/* IE vertical-alignment calculations can be off by +-1 but these margins are collapsed away */\r\n.dj_ie img.dojoComboBox {\r\n\tmargin-top: 1px;\r\n\tmargin-bottom: 1px;\r\n}\r\n\r\n/* the drop down */\r\n.dojoComboBoxOptions {\r\n\tfont-family: Verdana, Helvetica, Garamond, sans-serif;\r\n\t/* font-size: 0.7em; */
 \r\n\tbackground-color: white;\r\n\tborder: 1px solid #afafaf;\r\n\tposition: absolute;\r\n\tz-index: 1000;\r\n\toverflow: auto;\r\n\tcursor: default;\r\n}\r\n\r\n.dojoComboBoxItem {\r\n\tpadding-left: 2px;\r\n\tpadding-top: 2px;\r\n\tmargin: 0px;\r\n}\r\n\r\n.dojoComboBoxItemEven {\r\n\tbackground-color: #f4f4f4;\r\n}\r\n\r\n.dojoComboBoxItemOdd {\r\n\tbackground-color: white;\r\n}\r\n\r\n.dojoComboBoxItemHighlight {\r\n\tbackground-color: #63709A;\r\n\tcolor: white;\r\n}\r\n",templateCssPath: dojo.uri.dojoUri("struts/ComboBox.css"),
+
+  //how many results are shown
+  searchLimit : 30,
+
+  transport : "",
+
+  //load options when page loads
+  preload : true,
+
+  tabIndex: "",
+
+  //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";
+      } else {
+        this.optionsListNode.style.height = this.dropdownHeight + "px";
+      }
+
+      this.popupWidget.open(this.domNode, this, this.downArrowNode);
+    } else {
+        this._hideResultList();
+    }
+  },
+
+  _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 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, i + typedText.length);
+                var post = text.substring(i + typedText.length);
+
+                if(!dojo.string.isBlank(pre)) {
+                  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);
+    var self = this;
+    //events
+    if(!dojo.string.isBlank(this.listenTopics)) {
+      var topics = this.listenTopics.split(",");
+      for(var i = 0; i < topics.length; i++) {
+        dojo.event.topic.subscribe(topics[i], function() {
+          var request = {cancel: false};
+	      self.notify(this.widgetId, "before", request);
+	      if(request.cancel) {
+	        return;
+	      }
+          self.clearValues();
+          self.dataProvider.getData(self.dataUrl);
+        });
+      }
+    }
+
+    //notify topics
+    if(!dojo.string.isBlank(this.notifyTopics)) {
+      this.notifyTopicsArray = this.notifyTopics.split(",");
+    }
+
+    //before topics
+    if(!dojo.string.isBlank(this.beforeNotifyTopics)) {
+      this.beforeNotifyTopicsArray = this.beforeNotifyTopics.split(",");
+    }
+
+    //after topics
+    if(!dojo.string.isBlank(this.afterNotifyTopics)) {
+      this.afterNotifyTopicsArray = this.afterNotifyTopics.split(",");
+    }
+
+    //error topics
+    if(!dojo.string.isBlank(this.errorNotifyTopics)) {
+      this.errorNotifyTopicsArray = this.errorNotifyTopics.split(",");
+    }
+
+    //value topics
+    if(!dojo.string.isBlank(this.valueNotifyTopics)) {
+      this.valueNotifyTopicsArray = this.valueNotifyTopics.split(",");
+    }
+
+    //better name
+    this.comboBoxSelectionValue.name = dojo.string.isBlank(this.keyName) ? this.name + "Key" : this.keyName;
+
+    //init values
+    this.comboBoxValue.value = this.initialValue;
+    this.comboBoxSelectionValue.value = this.initialKey;
+    this.textInputNode.value = this.initialValue;
+
+    //tabindex
+    if(!dojo.string.isBlank(this.tabIndex)) {
+      this.textInputNode.tabIndex = this.tabIndex;
+    }
+
+    //hide arrow?
+    if(!this.visibleDownArrow) {

[... 6702 lines stripped ...]