<?xml version="1.0"?>

<!-- ***** BEGIN LICENSE BLOCK *****
   - Version: MPL 1.1/GPL 2.0/LGPL 2.1
   -
   - The contents of this file are subject to the Mozilla Public License Version
   - 1.1 (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.mozilla.org/MPL/
   -
   - Software distributed under the License is distributed on an "AS IS" basis,
   - WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
   - for the specific language governing rights and limitations under the
   - License.
   -
   - The Original Code is Mozilla XForms support.
   -
   - The Initial Developer of the Original Code is
   - Olli Pettay
   - Portions created by the Initial Developer are Copyright (C) 2005
   - the Initial Developer. All Rights Reserved.
   -
   - Contributor(s):
   -  Olli Pettay <Olli.Pettay@helsinki.fi>
   -
   - Alternatively, the contents of this file may be used under the terms of
   - either the GNU General Public License Version 2 or later (the "GPL"), or
   - the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
   - in which case the provisions of the GPL or the LGPL are applicable instead
   - of those above. If you wish to allow use of your version of this file only
   - under the terms of either the GPL or the LGPL, and not to allow others to
   - use your version of this file under the terms of the MPL, indicate your
   - decision by deleting the provisions above and replace them with the notice
   - and other provisions required by the GPL or the LGPL. If you do not delete
   - the provisions above, a recipient may use your version of this file under
   - the terms of any one of the MPL, the GPL or the LGPL.
   -
   - ***** END LICENSE BLOCK ***** -->

<bindings id="select1Bindings"
   xmlns="http://www.mozilla.org/xbl"
   xmlns:html="http://www.w3.org/1999/xhtml"
   xmlns:xbl="http://www.mozilla.org/xbl"
   xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
   xmlns:xforms="http://www.w3.org/2002/xforms"
   xmlns:mozType="http://www.mozilla.org/projects/xforms/2005/type">

  <!-- select1 -->
  <binding id="xformswidget-select1"
           extends="chrome://xforms/content/xforms.xml#xformswidget-base">

    <!-- The strange indentation is because of the whitespace nodes.-->
    <content>
      <children includes="label|hint"/>
      <html:span class="-moz-select1-container"
                 anonid="container"><html:input
                 class="-moz-xforms-select1-input xf-value"
                 anonid="control"
                 xbl:inherits="tabindex"
                 onfocus="this.parentNode.parentNode.dispatchDOMUIEvent('DOMFocusIn')"
                 onblur="this.parentNode.parentNode.handleBlur(); this.parentNode.parentNode.dispatchDOMUIEvent('DOMFocusOut');"
                 onclick="this.parentNode.parentNode.handleControlClick();"
                 onkeypress="this.parentNode.parentNode.handleKeyPress(event);"
                 oninput="this.parentNode.parentNode.handleInput();"
      /><html:input mozType:dropmarker="true"
                    type="button"
                    anonid="dropmarker"
                    onmousedown="this.parentNode.parentNode.shouldHandleBlur = false;"
                    onmouseup="this.parentNode.parentNode.shouldHandleBlur = true;"
                    onclick="this.parentNode.parentNode.togglePopup();
                             this.previousSibling.focus();"
        /></html:span>
      <html:div mozType:comboboxpopup="true" anonid="popup"
                onmouseover="this.parentNode.shouldHandleBlur = false;
                             this.parentNode.mouseOver(event);"
                onmouseup="this.parentNode.mouseUp(event);"
                onmouseout="this.parentNode.shouldHandleBlur = true;">
        <children/>
      </html:div>
    </content>

    <implementation implements="nsIXFormsNSSelect1Element, nsIXFormsNSEditableElement, nsIXFormsComboboxUIWidget, nsIAccessibleProvider">

      <!-- nsIXFormsNSEditableElement -->
      <property name="editor" readonly="true"
                onget="return this.selectionOpen ? this.inputField.editor : null;"/>

    <!-- nsIXFormsNSSelect1Element -->
      <property name="selectedItem">
        <getter>
          return this._selected;
        </getter>
        <setter>
          if (this._selected == val)
            return;

          if (this._selected)
            this._selected.setActive(false);

          this._selected =
            val.QueryInterface(Components.interfaces.nsIXFormsItemElement);
          if (this._selected) {
            this._selected.setActive(true);
            this.updateInputField();
            this._handleSelection(true);
          }
        </setter>
      </property>

      <!-- nsIXFormsComboboxUIWidget -->
      <property name="open">
        <getter>
          return this.popupOpen;
        </getter>
        <setter>
          if (val)
            this.showPopup();
          else
            this.hidePopup();
        </setter>
      </property>

      <!-- nsIAccessibleProvider -->
      <property name="accessibleType" readonly="true">
        <getter>
          return Components.interfaces.nsIAccessibleProvider.XFormsSelectCombobox;
        </getter>
      </property>

      <field name="_inputField">null</field>
      <field name="_dropMarker">null</field>
      <field name="_popup">null</field>
      <field name="_container">null</field>
      <field name="_width">-1</field>
      
      <!-- This is either an nsIXFormsItemElement or null. -->
      <field name="_selected">null</field>
      <field name="_tmpSelected">null</field>
      <field name="_lastSelectedItem">null</field>
      <field name="popupOpen">false</field>
      <field name="shouldHandleBlur">true</field>
      <field name="_outOfRange">false</field>

      <property name="selectionOpen" readonly="true">
        <getter>
          return this.getAttribute("selection") == "open";
        </getter>
      </property>

      <property name="inputField" readonly="true">
        <getter>
          if (!this._inputField) {
            this._inputField =
              document.getAnonymousElementByAttribute(this, "anonid", "control");
          }
          return this._inputField;
        </getter>
      </property>

      <property name="dropMarker" readonly="true">
        <getter>
          if (!this._dropMarker) {
            this._dropMarker =
              document.getAnonymousElementByAttribute(this, "anonid", "dropmarker");
          }
          return this._dropMarker;
        </getter>
      </property>

      <property name="popup" readonly="true">
        <getter>
          if (!this._popup) {
            this._popup =
              document.getAnonymousElementByAttribute(this, "anonid", "popup");
          }
          return this._popup;
        </getter>
      </property>

      <property name="container" readonly="true">
        <getter>
          if (!this._container) {
            this._container =
              document.getAnonymousElementByAttribute(this, "anonid", "container");
          }
          return this._container;
        </getter>
      </property>

      <property name="incremental" readonly="true">
        <getter>
        <![CDATA[
          // default is true
          var incremental = true;

          if (this.getAttribute("incremental") == "false") {
            incremental = false;
          }

          return incremental;
        ]]>
        </getter>
      </property>

      <method name="updateInputField">
        <body>
          <![CDATA[
          if (this._selected) {
            // Remove extra white space characters from the beginning of the label.
            this.inputField.value = this._selected.labelText.replace(/^[\s\n]+/, "");

            // Fire 'ValueChange' event. This event is used by accessible
            // module.
            var event = this.ownerDocument.createEvent("Events");
            event.initEvent("ValueChange", false, false);
            this.dispatchEvent(event);
          }
          ]]>
        </body>
      </method>

      <method name="handleControlClick">
        <parameter name="aEvent"/>
        <body>
          <![CDATA[
          if (this.selectionOpen) {
            if (!this._selected && this.incremental) {
              this._handleSelection(false);
              return;
            }
            if (this.popupOpen) {
              this.hidePopup();
            }
            return;
          }
          this.togglePopup();
          ]]>
        </body>
      </method>

      <method name="handleKeyPress">
        <parameter name="aEvent"/>
        <body>
          <![CDATA[
            var key = aEvent.keyCode;
            if (key == aEvent.DOM_VK_RETURN || key == aEvent.DOM_VK_ENTER ||
                key == aEvent.DOM_VK_F4 || aEvent.altKey &&
                (key == aEvent.DOM_VK_UP || key == aEvent.DOM_VK_DOWN)) {
              var open = this.popupOpen;
              this.togglePopup();
              if (open && this._selected) {
                this.updateInputField();
                if (this.incremental) {
                  this._handleSelection(true);
                } else {
                  this.dispatchSelectEvents();
                }
              }
              if (aEvent.altKey && (key == aEvent.DOM_VK_UP ||
                  key == aEvent.DOM_VK_DOWN)) {
                aEvent.preventDefault();
              }
            } else if (key == aEvent.DOM_VK_UP || key == aEvent.DOM_VK_DOWN) {
              if (this.selectionOpen && !this.popupOpen) {
                this.togglePopup();
              }
              this.internalScroll(aEvent.keyCode == aEvent.DOM_VK_DOWN);
              if (this._selected && this.popupOpen) {
                var el = this._selected.QueryInterface(Components.interfaces.nsIDOMElement);
                if ("scrollIntoView" in el) {
                  el.scrollIntoView(false);
                }
              }

              if (!this.popupOpen && this.incremental) {
                if (this._selected) {
                  this.updateInputField();
                }
                this._handleSelection(true);
              }
              aEvent.preventDefault();
            } else if (key == aEvent.DOM_VK_TAB) {
              // Hiding popup when user uses keyboard to focus out
              // from <select1>. No need to update the value of the control
              // in this case. 
              if (this._selected && this.popupOpen) {
                this.hidePopup();
                this._selected.setActive(false);
                this._selected = this._tmpSelected;
                this._tmpSelected = null;
                if (this._selected) {
                  this._selected.setActive(true);
                }
              }
            } else if (aEvent.charCode && !this.selectionOpen) {
              // Cache sequence of last pressed keys and search it in value of
              // xforms:label inside xforms:item elements.
              var currtime = new Date().valueOf();
              var char = String.fromCharCode(aEvent.charCode);
              if (currtime - this.searchKeypressTime <= 1000)
                this.searchValue += char;
              else
                this.searchValue = char;

              this.selectItemByLabel(this.searchValue);
              this.searchKeypressTime = currtime;
            }
            return true;
          ]]>
        </body>
      </method>

      <field name="searchValue">""</field>
      <field name="searchKeypressTime">0</field>

      <method name="handleInput">
        <body>
          <![CDATA[
            if (this._selected) {
              this._selected.setActive(false);
              this._selected = null;
            }
            if (this.incremental) {
              this._handleSelection(false);
            }

            return true;
          ]]>
        </body>
      </method>

      <!-- This is used by internalScroll to find the next
           selectable <item>. Returns a DOMElement or null. -->
      <method name="findNextSelectable">
        <parameter name="aNode"/>
        <parameter name="aDown"/>
        <body>
          <![CDATA[
            if (!aNode) {
              return null;
            }

            var node = aNode;
            var next = null;
            while (node) {
              if (aDown) {
                next = node.nextSibling;
              } else {
                next = node.previousSibling;
              }

              if (next && next.namespaceURI == this.XFORMS_NS) {
                if (next.localName == "item") {
                  return next;
                } else if (next.localName == "choices") {
                  var child = null;
                  if (aDown) {
                    child = next.firstChild;
                  } else {
                    child = next.lastChild;
                  }
                  if (child.namespaceURI == this.XFORMS_NS &&
                      child.localName == "item") {
                    return child;
                  }
                  child = this.findNextSelectable(child, aDown);
                  if (child) {
                    return child;
                  }
                } else if (next.localName == "itemset" &&
                           next.anonymousItemSetContent.childNodes.length > 0) {
                  return aDown ? next.anonymousItemSetContent.firstChild
                               : next.anonymousItemSetContent.lastChild;
                }
              }
              node = next;
            }

            // if we are in a choices or itemset element
            var parent = aNode.parentNode;
            if (parent.namespaceURI == this.XFORMS_NS &&
                parent.localName == "choices") {
                var sibling = aDown ? parent.nextSibling : parent.previousSibling;

                if (sibling.namespaceURI == this.XFORMS_NS &&
                    sibling.localName == "item") {
                  return sibling;
                }
                return this.findNextSelectable(sibling, aDown);
              } 
              
            if (parent.parentNode.namespaceURI == this.XFORMS_NS &&
                parent.parentNode.localName == "itemset") {
              var sibling2 = aDown
                ? parent.parentNode.nextSibling
                : parent.parentNode.previousSibling;

              if (sibling2.namespaceURI == this.XFORMS_NS &&
                  sibling2.localName == "item") {
                return sibling2;
              }
              return this.findNextSelectable(sibling2, aDown);
            }

            return null;
          ]]>
        </body>
      </method>

      <method name="internalScroll">
        <parameter name="aDown"/>
        <body>
          <![CDATA[
            var label = null;
            var next = null, nextItem = null;
            var node = this.firstChild;
            while (node) {
              if (node.namespaceURI == this.XFORMS_NS &&
                  node.localName == "label") {
                label = node;
                break;
              }
              node = node.nextSibling;
            }

            if (this._selected) {
              node = this._selected.QueryInterface(Components.interfaces.nsIDOMNode);
            } else if (label) {
              node = label.nextSibling;
            } else {
              node = this.firstChild;
            }

            next = this.findNextSelectable(node, aDown);
            if (next) {
              if (this._selected) {
                this._lastSelected = this._selected;
                this._selected.setActive(false);
                this._selected = null;
              }

              nextItem = next.QueryInterface(Components.interfaces.nsIXFormsItemElement);
              if (nextItem) {
                this._selected = nextItem;
                this._selected.setActive(true);
                if (!this.selectionOpen)
                  this.updateInputField();
              }
              return true;
            }
          ]]>
        </body>
      </method>

      <method name="mouseUp">
        <parameter name="aEvent"/>
        <body>
          <![CDATA[
            // Use original target, because <item> is possible (anonymously)
            // inside <itemset>
            var target = aEvent.originalTarget;
            while (target && target != this) {
              if (target.namespaceURI == this.XFORMS_NS &&
                  (target.localName == "item" || target.localName == "choices")) {
                break;
              }
              target = target.parentNode;
            }

            if (target == this) {
              return true;
            }

            this.hidePopup();

            if (this._selected) {
              this.updateInputField();
              if (this.incremental) {
                this._handleSelection(true);
              } else {
                this.dispatchSelectEvents();
              }
            }
            this.inputField.focus();
          ]]>
        </body>
      </method>

      <method name="mouseOver">
        <parameter name="aEvent"/>
        <body>
          <![CDATA[
          // Use original target, because <item> is possible (anonymously)
          // inside <itemset>
          var target = aEvent.originalTarget;
          while (target && target != this) {
            if (target.namespaceURI == this.XFORMS_NS && target.localName == "item") {
              if (this._selected) {
                this._selected.setActive(false);
                this._selected = null;
              }

              var item =
                target.QueryInterface(Components.interfaces.nsIXFormsItemElement);
              if (item) {
                item.setActive(true);
                this._selected = item;
              }
              break;
            }
            target = target.parentNode;
          }
          ]]>
        </body>
      </method>

      <method name="refreshWidth">
        <body>
          <![CDATA[
          if (!this.popupOpen) {
            this.inputField.removeAttribute("style");
            this.popup.style.width = "auto";
            this.popup.style.height = "auto";
            this.popup.style.maxHeight = "none";
            this._width = -1;
            var popupBox = document.getBoxObjectFor(this.popup);
            var w = popupBox.width;
            if (w > 0) {
              w = w + 12; // Adding some 'padding' for possible scrollbar
              this.inputField.setAttribute("style", "width:" + w + "px;");
              this._width = w + document.getBoxObjectFor(this.dropMarker).width;
            }
            this.popup.style.maxHeight = "10px";
            this.popup.style.left = "0px";
            this.popup.style.top = "0px";
          }
          ]]>
        </body>
      </method>

      <method name="hidePopup">
        <body>
          this.popup.style.visibility = "hidden";
          this.popupOpen = false;
        </body>
      </method>

      <method name="showPopup">
        <body>
        <![CDATA[
          if (this.popupOpen || this.accessors.isReadonly())
            return;

          // Calculating the size and position of the popup.
          var style = "";
          var containerBox = document.getBoxObjectFor(this.container);
          var x;
          var y;
          var adjust = 0;
          var absolute = false;
          var absoluteOffsetY = 0;
          var p = this.container.offsetParent;
          var compStyle =
            document.defaultView.getComputedStyle(p, null);
          if (compStyle.getPropertyValue("position") != "absolute" &&
              compStyle.getPropertyValue("position") != "relative") {
            adjust = document.documentElement.offsetTop;
            x = containerBox.x;
            y = containerBox.y;
          } else {
            absolute = true;
            x = this.container.offsetLeft;
            y = this.container.offsetTop;
            absoluteOffsetY = y;
            while (p) {
              absoluteOffsetY += p.offsetTop;
              p = p.offsetParent;
            }
          }

          var h = containerBox.height;
          var w = containerBox.width;
          var targetY = y + h;
          this.popup.style.maxHeight = "none";
          var popupBox = document.getBoxObjectFor(this.popup);
          var popupHeight = popupBox.height;
          var pY = window.pageYOffset;
          var iH = window.innerHeight;
          var belowSelect = absolute
            ? (iH - (absoluteOffsetY + h) - h)
            : (iH - (y - pY + h - adjust) - h);

          if (belowSelect < popupHeight) {
            if ((y - pY) > popupHeight) {
              targetY = y - popupHeight;
            } else if (belowSelect < (y - pY)) {
              style = style + "max-height:" + (y - pY - adjust) + "px;";
              targetY = pY + adjust;
            } else {
              style = style + "max-height:" + belowSelect + "px;";
            }
          }
          style = style + "left:" + x + "px;";
          style = style + "top:" + targetY + "px;";

          style = style + "width:";
          if (this.selectionOpen) {
            style = style + w + "px;";
          } else if (this._width < 0) {
            style = style + "auto;"
          } else {
            style = style + this._width + "px;";
          }

          style = style + "visibility:visible;";
          this.popup.setAttribute("style", style);
          this.popupOpen = true;
          this._tmpSelected = this._selected;

          if (this._selected) {
            var el = this._selected.QueryInterface(Components.interfaces.nsIDOMElement);
            if ("scrollIntoView" in el) {
              el.scrollIntoView(false);
            }
          }
        ]]>
        </body>
      </method>

      <method name="togglePopup">
        <body>
          if (!this.popupOpen)
            this.showPopup();
          else
            this.hidePopup();
        </body>
      </method>

      <method name="refresh">
        <body>
          <![CDATA[
          try {
            var nodeValue = null, newValue = null;
            var boundNode = this.accessors.getBoundNode();
            var outOfRange = false;
            if (boundNode && boundNode.hasChildNodes()) {
              // Since this is a select1, there should normally be just one
              // child node here.  But no guarantee that a select1 generated
              // the value coming in.  So we'll look for text node with 
              // non-whitespace characters to compare with an item's xf:value.
              // Any other node that we encounter we look to match with an
              // item's xf:copy.  If more than one of either of these exists
              // in the instance data, we need to generate a xforms-out-of-range
              // event and style the select1 as out-of-range since by
              // definition a select1 can not select more than one item.
              var child = boundNode.firstChild;
              while (child) {
                var type = child.nodeType;
                if (type == Node.TEXT_NODE) {
                  // if child is a text node completely filled with
                  // whitespace let's ignore it and get the next node
                  var string = child.nodeValue;
                  var nonWhitespace = false;
                  if (string) {
                    // this regexp tests whether only whitespace is contained
                    // between the beginning and ending of the string.
                    nonWhitespace = !(/^\s*$/.test(string));
                  }
                  if (nonWhitespace) {
                    if (newValue || nodeValue) {
                      // oh oh!  We've already found a selectable node in the
                      // instance data and now we have another.  That shouldn't
                      // happen.
                      outOfRange = true;
                    }
                    newValue = string;
                  }
                } else {
                  // if it's not a text node, we'll assume that we are looking at
                  // a node worth comparing.  As such, look for an
                  // item with a copy element that might match this node.
                  if (newValue || nodeValue) {
                    // oh oh!  We've already found a selectable node in the
                    // instance data and now we have another.  That shouldn't
                    // happen.
                    outOfRange = true;
                  }
                  nodeValue = child;
                }

                if (child == boundNode.lastChild) {
                  break;
                }
                child = child.nextSibling;
              }
            }

            if (outOfRange) {
              // a control can't be out of range (or in range for that matter)
              // if there is no selected value, so make sure that there is a
              // bound node first.
              if ((outOfRange != this._outOfRange) && boundNode) {
                this._outOfRange = outOfRange;
                this.accessors.setInRange(false);
              }

              // can't possibly work, no sense continuing.
              this.inputField.value = "";
              this._selected.setActive(false);
              this._selected = null;
              this.refreshWidth();
              return false;
            }

            if (!this.selectionOpen || this.accessors.isReadonly()) {
              this.inputField.setAttribute("readonly", "readonly");
            } else {
              this.inputField.removeAttribute("readonly");
            }

            if (this._selected && !this._selected.isCopyItem) {
              var envelope = this._getSelectedValue();
              if (envelope) {
                var textNode = null;
                if (envelope.nodeType == Node.ELEMENT_NODE) {
                  textNode = envelope.firstChild;
                  if (newValue == textNode.nodeValue) {
                    // Value in instance data already selected. Need to only
                    // refresh the width.
                    this.refreshWidth();
                    return true;
                  }
                } else {
                  if (newValue == envelope.nodeValue) {
                    // Value in instance data already selected. Need to only
                    // refresh the width.
                    this.refreshWidth();
                    return true;
                  }
                }
              }
              this._selected.setActive(false);
              this._selected = null;
              this._lastSelectedItem = null;
            }

            if (newValue) {
              this.selectItemByValue(newValue);
            } else if (nodeValue) {
              this.selectItemByNode(nodeValue);
            }

            outOfRange = false;
            if (this._selected) {
              this.updateInputField();
              this._lastSelectedItem = this._selected;
            } else if (this.selectionOpen) {
              this.inputField.value = newValue;
            } else {
              this.inputField.value = "";
              outOfRange = true;
            }

            // a control can't be out of range (or in range for that matter)
            // if there is no selected value, so make sure that there is a
            // bound node first.
            if ((outOfRange != this._outOfRange) && boundNode) {
              this._outOfRange = outOfRange;
              this.accessors.setInRange(!outOfRange);
            }
            this.refreshWidth();
          } catch (ex) {}
          return true;
          ]]>
        </body>
      </method>

      <method name="focus">
        <body>
          this.inputField.focus();
          return true;
        </body>
      </method>

      <method name="getCurrentValue">
        <body>
          throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
        </body>
      </method>

      <method name="selectItemByLabel">
        <parameter name="aValue"/>
        <parameter name="aContextNode"/>
        <body>
        <![CDATA[
          var item =
            this.searchItemByLabel(new RegExp("^" + aValue, "i"), this);

          if (this._selected) {
            this._selected.setActive(false);
            this._selected = null;
          }
          this._selected = item;
          if (this._selected) {
            this._selected.setActive(true);
            this.updateInputField();
          }
        ]]>
        </body>
      </method>

      <method name="searchItemByLabel">
        <parameter name="aExp"/>
        <parameter name="aContextNode"/>
        <body>
        <![CDATA[
          for (var child = aContextNode.firstChild; child;
               child = child.nextSibling) {
            if (child.namespaceURI != this.XFORMS_NS)
              continue;

            switch (child.localName) {
              case "item":
                child = child.
                  QueryInterface(Components.interfaces.nsIXFormsItemElement);
                if (child.labelText.search(aExp) != -1) {
                  return child;
                }
                break;
              case "choices":
                var item = this.searchItemByLabel(aExp, child);
                if (item)
                  return item;
                break;
              case "itemset":
                var item =
                  this.searchItemByLabel(aExp, child.anonymousItemSetContent);
                if (item)
                  return item;
                break;
            }
          }
        ]]>
        </body>
      </method>

      <method name="selectItemByValue">
        <parameter name="aValue"/>
        <body>
          <![CDATA[
          var node = this.firstChild;
          var item;
          while (node) {
            item = null;
            try {
              if (node.nodeType == document.ELEMENT_NODE &&
                  node.namespaceURI == this.XFORMS_NS &&
                  node.localName != "label") {

                item = node.QueryInterface(Components.interfaces.nsIXFormsSelectChild);
                if (item) {
                  item = item.selectItemByValue(aValue);
                  if (item) {
                    if (this._selected) {
                      this._selected.setActive(false);
                      this._selected = null;
                    }
                    this._selected = item.QueryInterface(Components.interfaces.nsIXFormsItemElement);
                    if (this._selected) {
                      this._selected.setActive(true);
                      return;
                    }
                  }
                }
              }
            } catch (ex) {}
            node = node.nextSibling;
          }
          ]]>
        </body>
      </method>

      <method name="selectItemByNode">
        <parameter name="aNode"/>
        <body>
          <![CDATA[

          // select the copyItem in this select1 whose copyNode equals aNode

          var node = this.firstChild;
          var item;
          while (node) {
            item = null;
            try {
              if (node.nodeType == document.ELEMENT_NODE &&
                  node.namespaceURI == this.XFORMS_NS &&
                  node.localName != "label") {

                item = node.QueryInterface(Components.interfaces.nsIXFormsSelectChild);
                if (item) {
                  item = item.selectItemByNode(aNode);
                  if (item) {
                    if (this._selected) {
                      this._selected.setActive(false);
                      this._selected = null;
                    }
                    this._selected = item.QueryInterface(Components.interfaces.nsIXFormsItemElement);
                    if (this._selected) {
                      this._selected.setActive(true);
                      return;
                    }
                  }
                }
              }
            } catch (ex) {}
            node = node.nextSibling;
          }
          ]]>
        </body>
      </method>

      <method name="handleBlur">
        <body>
          <![CDATA[
          if (this.shouldHandleBlur) {
            var open = this.popupOpen;
            this.hidePopup();

            if (open) {
              if (this._selected) {
                this._selected.setActive(false);
                this._selected = this._tmpSelected;
                this._tmpSelected = null;
                if (this._selected) {
                  this._selected.setActive(true);
                }
              }
              return;
            }

            if (this._selected) {
              this.updateInputField();
            }

            this._handleSelection(false, true);
          }
          ]]>
        </body>
      </method>

      <!-- _handleSelection updates the bound node with the value from the
            currently selected item's value element or copy element. -->
      <method name="_handleSelection">
        <parameter name="aDispatchSelectEvents"/>
        <parameter name="aInBlur"/>
        <body>
          <![CDATA[
          // if aDispatchSelectEvents is true, then we need to make sure to
          // dispatch the xforms-deselect and xforms-select events before we
          // change the value of the bound node otherwise we'll get the
          // event ordering wrong.  Similar with setting out of/in range (must
          // happen after dispatch select/deselect).  aDispatchSelectEvents is
          // a REQUIRED parameter, whether it be true or false.

          // aInBlur is not a required parameter.  It is true if handleSelection
          // was called from the blur handler.

          var boundNode = this.accessors.getBoundNode();
          if (!boundNode) {
            return;
          }

          if (this.selectionOpen && !this._selected) {
            if (aDispatchSelectEvents == true) {
              this.dispatchSelectEvents();
            }
            this.accessors.setValue(this.inputField.value);

            // open selection can't be out of range
            this._outOfRange = false;
            this.accessors.setInRange(true);
            return;
          }

          if (!this._selected) {
            // no reason to continue
            return;
          }

          if (aInBlur && aInBlur == true) {
            // if _handleSelection is called due to a blur, we only really care
            // about making sure the bound node is in sync if @incremental is
            // false.  Otherwise the bound node is already up to date so might
            // as well return.
            if (this.incremental) {
              return;
            }
          }

          if (boundNode.nodeType != boundNode.ELEMENT_NODE) {
            // if the boundNode type isn't an ELEMENT_NODE, then contentEnvelope
            // isn't an ELEMENT_NODE (since it is a clone of the bound node).
            // So if contentEnvelope has a value, it will be the nodeValue.
            var envelope = this._getSelectedValue();
            if (envelope) {
              if (aDispatchSelectEvents == true) {
                this.dispatchSelectEvents();
              }
              this.accessors.setValue(envelope.nodeValue);
              this._outOfRange = false;
              this.accessors.setInRange(true);
              return;
            }

            // not allowed to copy a node under a non ELEMENT node, so
            // generating a binding exception per spec.
            var ev = document.createEvent("Events");
            ev.initEvent("xforms-binding-exception", true, false);
            this.dispatchEvent(ev);

            // well, whatever we had selected isn't going to cut it.  But the
            // user did choose to deselect the previous item in favor of this
            // this item, so we really shouldn't go back to what
            // was there before.  So we'll go to nothing.  Make sure bound
            // node reflects this.  Seems to be consistent with what Novell
            // and formsPlayer does, too.
            this._selected.setActive(false);
            this._selected = null;
            this.inputField.value = "";

            if (aDispatchSelectEvents == true) {
              this.dispatchSelectEvents();
            }

            this.accessors.setValue("");
            this._outOfRange = true;
            this.accessors.setInRange(false);
            return;
          }

          var contentEnvelope = this._getSelectedValue();
          var copyInvolved = this._selected.isCopyItem;
          if (!copyInvolved && this._lastSelectedItem) {
            copyInvolved = this._lastSelectedItem.isCopyItem;
          }
          if (!copyInvolved) {
            // Since we aren't selecting a copyItem nor causing a copyItem to
            // be deselected, no sense using setContent.  Too expensive.
            if (aDispatchSelectEvents == true) {
              this.dispatchSelectEvents();
            }
            this.accessors.setValue(contentEnvelope.textContent);
            this._outOfRange = false;
            this.accessors.setInRange(true);
          } else {
            if (aDispatchSelectEvents == true) {
              this.dispatchSelectEvents();
            }
            this.accessors.setContent(contentEnvelope, true);
            this._outOfRange = false;
            this.accessors.setInRange(true);
          }
          ]]>
        </body>
      </method>

      <method name="dispatchSelectEvents">
        <body>
          <![CDATA[
          if (this._lastSelectedItem != this._selected) {
            if (this._lastSelectedItem) {
              this.dispatchSelectEvent(this._lastSelectedItem, "xforms-deselect");
            }
  
            if (this._selected) {
              this.dispatchSelectEvent(this._selected, "xforms-select");
            }

            this._lastSelectedItem = this._selected;
          }
          ]]>
        </body>
      </method>

      <method name="dispatchSelectEvent">
        <parameter name="aElement"/>
        <parameter name="aName"/>
        <body>
        <![CDATA[
          var ev = document.createEvent("Events");
          ev.initEvent(aName, true, false);

          var elm = aElement;

          // per errata for XForms 1.0 second edition, we send the event to the
          // item, even if it is contained in an itemset

          elm.dispatchEvent(ev);
          return true;
        ]]>
        </body>
      </method>

      <method name="_getSelectedValue">
        <body>
        <![CDATA[
          // The purpose of this function is to return the select1's currently
          // selected item's value in a contentEnvelope.  It achieves this by
          // cloning the bound node to create the contentEnvelope which will be
          // returned.  If the contentEnvelope is an element node, the contents
          // of the selected item's value will be inserted as a child of the
          // contentEnvelope.  If it is a textnode, the selected item value will
          // be put in the contentEnvelope.nodeValue.

          if (!this._selected) {
            // this will probably only happen if there was an exception
            // somewhere else first.  But no sense continuing below and adding
            // more exceptions to the console.
            return null;
          }

          var boundNode = this.accessors.getBoundNode();
          if (!boundNode)  {
            return null;
          }

          var contentEnvelope = boundNode.cloneNode(false);
          if (!contentEnvelope) {
            return null;
          }
          var contentDocument = contentEnvelope.ownerDocument;

          if (contentEnvelope.nodeType == Node.ELEMENT_NODE) { 
            var contentNode = null;
            if (this._selected.isCopyItem) {
              var copyNode = this._selected.copyNode;
              if (copyNode) {
                contentNode = contentDocument.importNode(copyNode, true);
              }
            } else {
              contentNode =
                contentDocument.createTextNode(this._selected.value);
            }

            contentEnvelope.appendChild(contentNode);
          } else {
            // if the selected item is not a copyItem, then we'll just put the
            // item's value in the nodeValue for the contentEnvelope.  Otherwise
            // we are stuck trying to stick an element node under a non-element
            // node and that ainna gonna work.
            if (!this._selected.isCopyItem) {
              contentEnvelope.nodeValue = this._selected.value;
            } else {
              contentEnvelope = null;
            }
          }

          return contentEnvelope;
        ]]>
        </body>
      </method>
    </implementation>

    <handlers>
      <handler event="keypress" keycode="VK_ESCAPE">
        if (this.popupOpen) {
          this.hidePopup();
          if (this._selected)
            this._selected.setActive(false);
            this._selected = this._tmpSelected;
            this._tmpSelected = null;
            if (this._selected)
              this._selected.setActive(true);
          }
        }
      </handler>
    </handlers>
  </binding>


  <!-- The binding for <item> is needed only because of the
       scrollIntoView method. -->
  <binding id="xformswidget-select1-item">
    <content>
      <html:div anonid="content">
        <children/>
      </html:div>
    </content>

    <implementation implements="nsIAccessibleProvider">
      <property name="accessibleType" readonly="true">
        <getter>
          return Components.interfaces.nsIAccessibleProvider.XFormsItemCombobox;
        </getter>
      </property>

      <field name="_content">null</field>
      <property name="content" readonly="true">
        <getter>
          if (!this._content) {
            this._content =
              document.getAnonymousElementByAttribute(this, "anonid", "content");
          }
          return this._content;
        </getter>
      </property>

      <!-- Only (X)HTML elements have method 'scrollIntoView', 
           so we need to forward this method call to the anonymous content
           of this element. -->
      <method name="scrollIntoView">
        <parameter name="aTop"/>
        <body>
          // this.content is null if anonymous content hasn't been created yet.
          var content = this.content;
          if (content) {
            content.scrollIntoView(aTop);
          }
        </body>
      </method>

      <property name="parentControl" readonly="true">
        <getter>
        <![CDATA[
          if (!this._parentControl) {
            var parent = this.parentNode;
            for (; parent; parent = parent.parentNode) {
              if (parent.namespaceURI == this.XFORMS_NS &&
                  (parent.localName == "select" ||
                   parent.localName == "select1"))
                break;
            }
            this._parentControl = parent;
          }
          return this._parentControl;
        ]]>
        </getter>
      </property>

      <method name="refreshParentControl">
        <parameter name="aTarget"/>
        <parameter name="aInsert"/>
        <body>
        <![CDATA[
          var parentControl = this.parentControl;
          if (!parentControl)
            return;

          // if the currently selected item is going away (i.e. due to an
          // itemset rebuilding) then the select1 needs to forget about it
          if (!aInsert) {
            if (aTarget.namespaceURI == this.XFORMS_NS &&
                aTarget.localName == "item") {
              if (aTarget == parentControl._selected) {
                parentControl._selected = null;
              }
            }
          }

          var parent = aTarget.parentNode;
          for (; parent; parent = parent.parentNode) {
            if (parent.namespaceURI == this.XFORMS_NS &&
                parent.localName == "label") {
              parentControl.refreshWidth();
              return;
            }
          }
        ]]>
        </body>
      </method>

      <property name="XFORMS_NS" readonly="true"
                onget="return 'http://www.w3.org/2002/xforms';"/>
    </implementation>

    <handlers>
      <handler event="DOMNodeInserted"
               action="refreshParentControl(event.originalTarget, true);"/>
      <handler event="DOMNodeRemoved"
               action="refreshParentControl(event.originalTarget, false);"/>
    </handlers>
  </binding>
</bindings>
