Bug 1476611 - Part 2 - Flatten the "richlistbox" bindings. r=bgrins draft
authorPaolo Amadini <paolo.mozmail@amadzone.org>
Mon, 23 Jul 2018 14:19:40 +0100
changeset 821486 201108d347c8ae1ca73dcdf96fc3b1c9868c9859
parent 821468 bd876c62bd0639c717612d5033b14b82e609d778
push id117109
push userpaolo.mozmail@amadzone.org
push dateMon, 23 Jul 2018 13:20:32 +0000
reviewersbgrins
bugs1476611
milestone63.0a1
Bug 1476611 - Part 2 - Flatten the "richlistbox" bindings. r=bgrins MozReview-Commit-ID: FrXKW1T7wYd
toolkit/content/jar.mn
toolkit/content/widgets/listbox.xml
toolkit/content/widgets/richlistbox.xml
--- a/toolkit/content/jar.mn
+++ b/toolkit/content/jar.mn
@@ -74,17 +74,16 @@ toolkit.jar:
    content/global/bindings/datetimepopup.xml   (widgets/datetimepopup.xml)
    content/global/bindings/datetimebox.xml     (widgets/datetimebox.xml)
    content/global/bindings/datetimebox.css     (widgets/datetimebox.css)
 *  content/global/bindings/dialog.xml          (widgets/dialog.xml)
    content/global/bindings/editor.xml          (widgets/editor.xml)
 *  content/global/bindings/findbar.xml         (widgets/findbar.xml)
    content/global/bindings/general.xml         (widgets/general.xml)
    content/global/bindings/groupbox.xml        (widgets/groupbox.xml)
-   content/global/bindings/listbox.xml         (widgets/listbox.xml)
    content/global/bindings/menu.xml            (widgets/menu.xml)
    content/global/bindings/menulist.xml        (widgets/menulist.xml)
    content/global/bindings/notification.xml    (widgets/notification.xml)
    content/global/bindings/numberbox.xml       (widgets/numberbox.xml)
    content/global/bindings/popup.xml           (widgets/popup.xml)
    content/global/bindings/progressmeter.xml   (widgets/progressmeter.xml)
    content/global/bindings/radio.xml           (widgets/radio.xml)
    content/global/bindings/remote-browser.xml  (widgets/remote-browser.xml)
deleted file mode 100644
--- a/toolkit/content/widgets/listbox.xml
+++ /dev/null
@@ -1,663 +0,0 @@
-<?xml version="1.0"?>
-
-<!-- This Source Code Form is subject to the terms of the Mozilla Public
-   - License, v. 2.0. If a copy of the MPL was not distributed with this
-   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
-
-<!-- This files relies on these specific Chrome/XBL globals -->
-<!-- globals ChromeNodeList -->
-
-<bindings id="listboxBindings"
-          xmlns="http://www.mozilla.org/xbl"
-          xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
-          xmlns:xbl="http://www.mozilla.org/xbl">
-
-  <!--
-    Interface binding that is base for bindings of xul:listbox and
-    xul:richlistbox elements. This binding assumes that successors bindings
-    will implement the following properties and methods:
-
-    /** Return the number of items */
-    readonly itemCount
-
-    /** Return index of given item
-    * @param aItem - given item element */
-    getIndexOfItem(aItem)
-
-    /** Return item at given index
-    * @param aIndex - index of item element */
-    getItemAtIndex(aIndex)
-
-    /** Return count of item elements */
-    getRowCount()
-
-    /** Return true if item of given index is visible
-     * @param aIndex - index of item element
-     *
-     * @note XXX: this method should be removed after bug 364612 is fixed
-     */
-    ensureIndexIsVisible(aIndex)
-
-    /** Return true if item element is visible
-     * @param aElement - given item element */
-    ensureElementIsVisible(aElement)
-
-    /** Scroll list control to make visible item of given index
-     * @param aIndex - index of item element
-     *
-     * @note XXX: this method should be removed after bug 364612 is fixed
-     */
-    scrollToIndex(aIndex)
-
-    /** Create item element and append it to the end of listbox
-     * @param aLabel - label of new item element
-     * @param aValue - value of new item element */
-    appendItem(aLabel, aValue)
-
-    /** Scroll up/down one page
-     * @param aDirection - specifies scrolling direction, should be either -1 or 1
-     * @return the number of elements the selection scrolled
-     */
-    scrollOnePage(aDirection)
-
-    /** Fire "select" event */
-    _fireOnSelect()
-   -->
-   <binding id="listbox-base"
-            extends="chrome://global/content/bindings/general.xml#basecontrol">
-
-    <implementation implements="nsIDOMXULMultiSelectControlElement">
-      <field name="_lastKeyTime">0</field>
-      <field name="_incrementalString">""</field>
-
-    <!-- nsIDOMXULSelectControlElement -->
-      <property name="selectedItem"
-                onset="this.selectItem(val);">
-        <getter>
-        <![CDATA[
-          return this.selectedItems.length > 0 ? this.selectedItems[0] : null;
-        ]]>
-        </getter>
-      </property>
-
-      <property name="selectedIndex">
-        <getter>
-        <![CDATA[
-          if (this.selectedItems.length > 0)
-            return this.getIndexOfItem(this.selectedItems[0]);
-          return -1;
-        ]]>
-        </getter>
-        <setter>
-        <![CDATA[
-          if (val >= 0) {
-            // This is a micro-optimization so that a call to getIndexOfItem or
-            // getItemAtIndex caused by _fireOnSelect (especially for derived
-            // widgets) won't loop the children.
-            this._selecting = {
-              item: this.getItemAtIndex(val),
-              index: val
-            };
-            this.selectItem(this._selecting.item);
-            delete this._selecting;
-          } else {
-            this.clearSelection();
-            this.currentItem = null;
-          }
-        ]]>
-        </setter>
-      </property>
-
-      <property name="value">
-        <getter>
-        <![CDATA[
-          if (this.selectedItems.length > 0)
-            return this.selectedItem.value;
-          return null;
-        ]]>
-        </getter>
-        <setter>
-        <![CDATA[
-          var kids = this.getElementsByAttribute("value", val);
-          if (kids && kids.item(0))
-            this.selectItem(kids[0]);
-          return val;
-        ]]>
-        </setter>
-      </property>
-
-    <!-- nsIDOMXULMultiSelectControlElement -->
-      <property name="selType"
-                onget="return this.getAttribute('seltype');"
-                onset="this.setAttribute('seltype', val); return val;"/>
-
-      <property name="currentItem" onget="return this._currentItem;">
-        <setter>
-          if (this._currentItem == val)
-            return val;
-
-          if (this._currentItem)
-            this._currentItem.current = false;
-          this._currentItem = val;
-
-          if (val)
-            val.current = true;
-
-          return val;
-        </setter>
-      </property>
-
-      <property name="currentIndex">
-        <getter>
-          return this.currentItem ? this.getIndexOfItem(this.currentItem) : -1;
-        </getter>
-        <setter>
-        <![CDATA[
-          if (val >= 0)
-            this.currentItem = this.getItemAtIndex(val);
-          else
-            this.currentItem = null;
-        ]]>
-        </setter>
-      </property>
-
-      <field name="selectedItems">new ChromeNodeList()</field>
-
-      <method name="addItemToSelection">
-        <parameter name="aItem"/>
-        <body>
-        <![CDATA[
-          if (this.selType != "multiple" && this.selectedCount)
-            return;
-
-          if (aItem.selected)
-            return;
-
-          this.selectedItems.append(aItem);
-          aItem.selected = true;
-
-          this._fireOnSelect();
-        ]]>
-        </body>
-      </method>
-
-      <method name="removeItemFromSelection">
-        <parameter name="aItem"/>
-        <body>
-        <![CDATA[
-          if (!aItem.selected)
-            return;
-
-          this.selectedItems.remove(aItem);
-          aItem.selected = false;
-          this._fireOnSelect();
-        ]]>
-        </body>
-      </method>
-
-      <method name="toggleItemSelection">
-        <parameter name="aItem"/>
-        <body>
-        <![CDATA[
-          if (aItem.selected)
-            this.removeItemFromSelection(aItem);
-          else
-            this.addItemToSelection(aItem);
-        ]]>
-        </body>
-      </method>
-
-      <method name="selectItem">
-        <parameter name="aItem"/>
-        <body>
-        <![CDATA[
-          if (!aItem)
-            return;
-
-          if (this.selectedItems.length == 1 && this.selectedItems[0] == aItem)
-            return;
-
-          this._selectionStart = null;
-
-          var suppress = this._suppressOnSelect;
-          this._suppressOnSelect = true;
-
-          this.clearSelection();
-          this.addItemToSelection(aItem);
-          this.currentItem = aItem;
-
-          this._suppressOnSelect = suppress;
-          this._fireOnSelect();
-        ]]>
-        </body>
-      </method>
-
-      <method name="selectItemRange">
-        <parameter name="aStartItem"/>
-        <parameter name="aEndItem"/>
-        <body>
-        <![CDATA[
-          if (this.selType != "multiple")
-            return;
-
-          if (!aStartItem)
-            aStartItem = this._selectionStart ?
-              this._selectionStart : this.currentItem;
-
-          if (!aStartItem)
-            aStartItem = aEndItem;
-
-          var suppressSelect = this._suppressOnSelect;
-          this._suppressOnSelect = true;
-
-          this._selectionStart = aStartItem;
-
-          var currentItem;
-          var startIndex = this.getIndexOfItem(aStartItem);
-          var endIndex = this.getIndexOfItem(aEndItem);
-          if (endIndex < startIndex) {
-            currentItem = aEndItem;
-            aEndItem = aStartItem;
-            aStartItem = currentItem;
-          } else {
-            currentItem = aStartItem;
-          }
-
-          while (currentItem) {
-            this.addItemToSelection(currentItem);
-            if (currentItem == aEndItem) {
-              currentItem = this.getNextItem(currentItem, 1);
-              break;
-            }
-            currentItem = this.getNextItem(currentItem, 1);
-          }
-
-          // Clear around new selection
-          // Don't use clearSelection() because it causes a lot of noise
-          // with respect to selection removed notifications used by the
-          // accessibility API support.
-          var userSelecting = this._userSelecting;
-          this._userSelecting = false; // that's US automatically unselecting
-          for (; currentItem; currentItem = this.getNextItem(currentItem, 1))
-            this.removeItemFromSelection(currentItem);
-
-          for (currentItem = this.getItemAtIndex(0); currentItem != aStartItem;
-               currentItem = this.getNextItem(currentItem, 1))
-            this.removeItemFromSelection(currentItem);
-          this._userSelecting = userSelecting;
-
-          this._suppressOnSelect = suppressSelect;
-
-          this._fireOnSelect();
-        ]]>
-        </body>
-      </method>
-
-      <method name="selectAll">
-        <body>
-          this._selectionStart = null;
-
-          var suppress = this._suppressOnSelect;
-          this._suppressOnSelect = true;
-
-          var item = this.getItemAtIndex(0);
-          while (item) {
-            this.addItemToSelection(item);
-            item = this.getNextItem(item, 1);
-          }
-
-          this._suppressOnSelect = suppress;
-          this._fireOnSelect();
-        </body>
-      </method>
-
-      <method name="invertSelection">
-        <body>
-          this._selectionStart = null;
-
-          var suppress = this._suppressOnSelect;
-          this._suppressOnSelect = true;
-
-          var item = this.getItemAtIndex(0);
-          while (item) {
-            if (item.selected)
-              this.removeItemFromSelection(item);
-            else
-              this.addItemToSelection(item);
-            item = this.getNextItem(item, 1);
-          }
-
-          this._suppressOnSelect = suppress;
-          this._fireOnSelect();
-        </body>
-      </method>
-
-      <method name="clearSelection">
-        <body>
-        <![CDATA[
-          if (this.selectedItems) {
-            while (this.selectedItems.length > 0) {
-              let item = this.selectedItems[0];
-              item.selected = false;
-              this.selectedItems.remove(item);
-            }
-          }
-
-          this._selectionStart = null;
-          this._fireOnSelect();
-        ]]>
-        </body>
-      </method>
-
-      <property name="selectedCount" readonly="true"
-                onget="return this.selectedItems.length;"/>
-
-      <method name="getSelectedItem">
-        <parameter name="aIndex"/>
-        <body>
-        <![CDATA[
-          return aIndex < this.selectedItems.length ?
-            this.selectedItems[aIndex] : null;
-        ]]>
-        </body>
-      </method>
-
-    <!-- Other public members -->
-      <property name="disableKeyNavigation"
-                onget="return this.hasAttribute('disableKeyNavigation');">
-        <setter>
-          if (val)
-            this.setAttribute("disableKeyNavigation", "true");
-          else
-            this.removeAttribute("disableKeyNavigation");
-          return val;
-        </setter>
-      </property>
-
-      <property name="suppressOnSelect"
-                onget="return this.getAttribute('suppressonselect') == 'true';"
-                onset="this.setAttribute('suppressonselect', val);"/>
-
-      <property name="_selectDelay"
-                onset="this.setAttribute('_selectDelay', val);"
-                onget="return this.getAttribute('_selectDelay') || 50;"/>
-
-      <method name="timedSelect">
-        <parameter name="aItem"/>
-        <parameter name="aTimeout"/>
-        <body>
-        <![CDATA[
-          var suppress = this._suppressOnSelect;
-          if (aTimeout != -1)
-            this._suppressOnSelect = true;
-
-          this.selectItem(aItem);
-
-          this._suppressOnSelect = suppress;
-
-          if (aTimeout != -1) {
-            if (this._selectTimeout)
-              window.clearTimeout(this._selectTimeout);
-            this._selectTimeout =
-              window.setTimeout(this._selectTimeoutHandler, aTimeout, this);
-          }
-        ]]>
-        </body>
-      </method>
-
-    <!-- Private -->
-      <method name="_moveByOffsetFromUserEvent">
-        <parameter name="aOffset"/>
-        <parameter name="aEvent"/>
-        <body>
-        <![CDATA[
-          if (!aEvent.defaultPrevented) {
-            this._userSelecting = true;
-            this._mayReverse = true;
-            this.moveByOffset(aOffset, !aEvent.ctrlKey, aEvent.shiftKey);
-            this._userSelecting = false;
-            this._mayReverse = false;
-            aEvent.preventDefault();
-          }
-        ]]>
-        </body>
-      </method>
-
-      <method name="_canUserSelect">
-        <parameter name="aItem"/>
-        <body>
-        <![CDATA[
-          var style = document.defaultView.getComputedStyle(aItem);
-          return style.display != "none" && style.visibility == "visible";
-        ]]>
-        </body>
-      </method>
-
-      <method name="_selectTimeoutHandler">
-        <parameter name="aMe"/>
-        <body>
-          aMe._fireOnSelect();
-          aMe._selectTimeout = null;
-        </body>
-      </method>
-
-      <field name="_suppressOnSelect">false</field>
-      <field name="_userSelecting">false</field>
-      <field name="_mayReverse">false</field>
-      <field name="_selectTimeout">null</field>
-      <field name="_currentItem">null</field>
-      <field name="_selectionStart">null</field>
-    </implementation>
-
-    <handlers>
-      <handler event="keypress" keycode="VK_UP" modifiers="control shift any"
-               action="this._moveByOffsetFromUserEvent(-1, event);"
-               group="system"/>
-      <handler event="keypress" keycode="VK_DOWN" modifiers="control shift any"
-               action="this._moveByOffsetFromUserEvent(1, event);"
-               group="system"/>
-      <handler event="keypress" keycode="VK_HOME" modifiers="control shift any"
-               group="system">
-        <![CDATA[
-          this._mayReverse = true;
-          this._moveByOffsetFromUserEvent(-this.currentIndex, event);
-          this._mayReverse = false;
-        ]]>
-      </handler>
-      <handler event="keypress" keycode="VK_END" modifiers="control shift any"
-               group="system">
-        <![CDATA[
-          this._mayReverse = true;
-          this._moveByOffsetFromUserEvent(this.getRowCount() - this.currentIndex - 1, event);
-          this._mayReverse = false;
-        ]]>
-      </handler>
-      <handler event="keypress" keycode="VK_PAGE_UP" modifiers="control shift any"
-               group="system">
-        <![CDATA[
-          this._mayReverse = true;
-          this._moveByOffsetFromUserEvent(this.scrollOnePage(-1), event);
-          this._mayReverse = false;
-        ]]>
-      </handler>
-      <handler event="keypress" keycode="VK_PAGE_DOWN" modifiers="control shift any"
-               group="system">
-        <![CDATA[
-          this._mayReverse = true;
-          this._moveByOffsetFromUserEvent(this.scrollOnePage(1), event);
-          this._mayReverse = false;
-        ]]>
-      </handler>
-      <handler event="keypress" key=" " modifiers="control" phase="target">
-      <![CDATA[
-        if (this.currentItem && this.selType == "multiple")
-          this.toggleItemSelection(this.currentItem);
-      ]]>
-      </handler>
-      <handler event="focus">
-      <![CDATA[
-        if (this.getRowCount() > 0) {
-          if (this.currentIndex == -1) {
-            this.currentIndex = this.getIndexOfFirstVisibleRow();
-          } else {
-            this.currentItem._fireEvent("DOMMenuItemActive");
-          }
-        }
-        this._lastKeyTime = 0;
-      ]]>
-      </handler>
-      <handler event="keypress" phase="target">
-      <![CDATA[
-        if (this.disableKeyNavigation || !event.charCode ||
-            event.altKey || event.ctrlKey || event.metaKey)
-          return;
-
-        if (event.timeStamp - this._lastKeyTime > 1000)
-          this._incrementalString = "";
-
-        var key = String.fromCharCode(event.charCode).toLowerCase();
-        this._incrementalString += key;
-        this._lastKeyTime = event.timeStamp;
-
-        // If all letters in the incremental string are the same, just
-        // try to match the first one
-        var incrementalString = /^(.)\1+$/.test(this._incrementalString) ?
-                                RegExp.$1 : this._incrementalString;
-        var length = incrementalString.length;
-
-        var rowCount = this.getRowCount();
-        var l = this.selectedItems.length;
-        var start = l > 0 ? this.getIndexOfItem(this.selectedItems[l - 1]) : -1;
-        // start from the first element if none was selected or from the one
-        // following the selected one if it's a new or a repeated-letter search
-        if (start == -1 || length == 1)
-          start++;
-
-        for (var i = 0; i < rowCount; i++) {
-          var k = (start + i) % rowCount;
-          var listitem = this.getItemAtIndex(k);
-          if (!this._canUserSelect(listitem))
-            continue;
-          // allow richlistitems to specify the string being searched for
-          var searchText = "searchLabel" in listitem ? listitem.searchLabel :
-                           listitem.getAttribute("label"); // (see also bug 250123)
-          searchText = searchText.substring(0, length).toLowerCase();
-          if (searchText == incrementalString) {
-            this.ensureIndexIsVisible(k);
-            this.timedSelect(listitem, this._selectDelay);
-            break;
-          }
-        }
-      ]]>
-      </handler>
-    </handlers>
-  </binding>
-
-  <binding id="listitem"
-           extends="chrome://global/content/bindings/general.xml#basetext">
-    <implementation implements="nsIDOMXULSelectControlItemElement">
-      <property name="current" onget="return this.getAttribute('current') == 'true';">
-        <setter><![CDATA[
-          if (val)
-            this.setAttribute("current", "true");
-          else
-            this.removeAttribute("current");
-
-          let control = this.control;
-          if (!control || !control.suppressMenuItemEvent) {
-            this._fireEvent(val ? "DOMMenuItemActive" : "DOMMenuItemInactive");
-          }
-
-          return val;
-        ]]></setter>
-      </property>
-
-      <!-- ///////////////// nsIDOMXULSelectControlItemElement ///////////////// -->
-
-      <property name="value" onget="return this.getAttribute('value');"
-                             onset="this.setAttribute('value', val); return val;"/>
-
-      <property name="selected" onget="return this.getAttribute('selected') == 'true';">
-        <setter><![CDATA[
-          if (val)
-            this.setAttribute("selected", "true");
-          else
-            this.removeAttribute("selected");
-
-          return val;
-        ]]></setter>
-      </property>
-
-      <property name="control">
-        <getter><![CDATA[
-          var parent = this.parentNode;
-          while (parent) {
-            if (parent instanceof Ci.nsIDOMXULSelectControlElement)
-              return parent;
-            parent = parent.parentNode;
-          }
-          return null;
-        ]]></getter>
-      </property>
-
-      <method name="_fireEvent">
-        <parameter name="name"/>
-        <body>
-        <![CDATA[
-          var event = document.createEvent("Events");
-          event.initEvent(name, true, true);
-          this.dispatchEvent(event);
-        ]]>
-        </body>
-      </method>
-    </implementation>
-    <handlers>
-      <!-- If there is no modifier key, we select on mousedown, not
-           click, so that drags work correctly. -->
-      <handler event="mousedown">
-      <![CDATA[
-        var control = this.control;
-        if (!control || control.disabled)
-          return;
-        if ((!event.ctrlKey || (/Mac/.test(navigator.platform) && event.button == 2)) &&
-            !event.shiftKey && !event.metaKey) {
-          if (!this.selected) {
-            control.selectItem(this);
-          }
-          control.currentItem = this;
-        }
-      ]]>
-      </handler>
-
-      <!-- On a click (up+down on the same item), deselect everything
-           except this item. -->
-      <handler event="click" button="0">
-      <![CDATA[
-        var control = this.control;
-        if (!control || control.disabled)
-          return;
-        control._userSelecting = true;
-        if (control.selType != "multiple") {
-          control.selectItem(this);
-        } else if (event.ctrlKey || event.metaKey) {
-          control.toggleItemSelection(this);
-          control.currentItem = this;
-        } else if (event.shiftKey) {
-          control.selectItemRange(null, this);
-          control.currentItem = this;
-        } else {
-          /* We want to deselect all the selected items except what was
-            clicked, UNLESS it was a right-click.  We have to do this
-            in click rather than mousedown so that you can drag a
-            selected group of items */
-
-          // use selectItemRange instead of selectItem, because this
-          // doesn't de- and reselect this item if it is selected
-          control.selectItemRange(this, this);
-        }
-        control._userSelecting = false;
-      ]]>
-      </handler>
-    </handlers>
-  </binding>
-</bindings>
--- a/toolkit/content/widgets/richlistbox.xml
+++ b/toolkit/content/widgets/richlistbox.xml
@@ -1,40 +1,42 @@
 <?xml version="1.0"?>
 
 <!-- This Source Code Form is subject to the terms of the Mozilla Public
    - License, v. 2.0. If a copy of the MPL was not distributed with this
    - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
 
+<!-- This file relies on these specific Chrome/XBL globals -->
+<!-- globals ChromeNodeList -->
+
 <bindings id="richlistboxBindings"
           xmlns="http://www.mozilla.org/xbl"
           xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
           xmlns:xbl="http://www.mozilla.org/xbl">
 
   <binding id="richlistbox"
-           extends="chrome://global/content/bindings/listbox.xml#listbox-base">
+           extends="chrome://global/content/bindings/general.xml#basecontrol">
     <content>
       <children includes="listheader"/>
       <xul:scrollbox allowevents="true" orient="vertical" anonid="main-box"
                      flex="1" style="overflow: auto;" xbl:inherits="dir,pack">
         <children/>
       </xul:scrollbox>
     </content>
 
-    <implementation>
+    <implementation implements="nsIDOMXULMultiSelectControlElement">
       <field name="_scrollbox">
         document.getAnonymousElementByAttribute(this, "anonid", "main-box");
       </field>
       <constructor>
         <![CDATA[
           this._refreshSelection();
         ]]>
       </constructor>
 
-    <!-- Overriding baselistbox -->
       <method name="_fireOnSelect">
         <body>
           <![CDATA[
             // make sure not to modify last-selected when suppressing select events
             // (otherwise we'll lose the selection when a template gets rebuilt)
             if (this._suppressOnSelect || this.suppressOnSelect)
               return;
 
@@ -129,44 +131,354 @@
           item.appendChild(label);
 
           this.appendChild(item);
 
           return item;
         </body>
       </method>
 
+      <!-- nsIDOMXULSelectControlElement -->
+      <property name="selectedItem"
+                onset="this.selectItem(val);">
+        <getter>
+        <![CDATA[
+          return this.selectedItems.length > 0 ? this.selectedItems[0] : null;
+        ]]>
+        </getter>
+      </property>
+
+      <!-- nsIDOMXULSelectControlElement -->
+      <property name="selectedIndex">
+        <getter>
+        <![CDATA[
+          if (this.selectedItems.length > 0)
+            return this.getIndexOfItem(this.selectedItems[0]);
+          return -1;
+        ]]>
+        </getter>
+        <setter>
+        <![CDATA[
+          if (val >= 0) {
+            // This is a micro-optimization so that a call to getIndexOfItem or
+            // getItemAtIndex caused by _fireOnSelect (especially for derived
+            // widgets) won't loop the children.
+            this._selecting = {
+              item: this.getItemAtIndex(val),
+              index: val
+            };
+            this.selectItem(this._selecting.item);
+            delete this._selecting;
+          } else {
+            this.clearSelection();
+            this.currentItem = null;
+          }
+        ]]>
+        </setter>
+      </property>
+
+      <!-- nsIDOMXULSelectControlElement -->
+      <property name="value">
+        <getter>
+        <![CDATA[
+          if (this.selectedItems.length > 0)
+            return this.selectedItem.value;
+          return null;
+        ]]>
+        </getter>
+        <setter>
+        <![CDATA[
+          var kids = this.getElementsByAttribute("value", val);
+          if (kids && kids.item(0))
+            this.selectItem(kids[0]);
+          return val;
+        ]]>
+        </setter>
+      </property>
+
+      <!-- nsIDOMXULSelectControlElement -->
       <property name="itemCount" readonly="true"
                 onget="return this.children.length"/>
 
+      <!-- nsIDOMXULSelectControlElement -->
       <method name="getIndexOfItem">
         <parameter name="aItem"/>
         <body>
           <![CDATA[
             // don't search the children, if we're looking for none of them
             if (aItem == null)
               return -1;
             if (this._selecting && this._selecting.item == aItem)
               return this._selecting.index;
             return this.children.indexOf(aItem);
           ]]>
         </body>
       </method>
 
+      <!-- nsIDOMXULSelectControlElement -->
       <method name="getItemAtIndex">
         <parameter name="aIndex"/>
         <body>
           <![CDATA[
             if (this._selecting && this._selecting.index == aIndex)
               return this._selecting.item;
             return this.children[aIndex] || null;
           ]]>
         </body>
       </method>
 
+      <!-- nsIDOMXULMultiSelectControlElement -->
+      <property name="selType"
+                onget="return this.getAttribute('seltype');"
+                onset="this.setAttribute('seltype', val); return val;"/>
+
+      <!-- nsIDOMXULMultiSelectControlElement -->
+      <property name="currentItem" onget="return this._currentItem;">
+        <setter>
+          if (this._currentItem == val)
+            return val;
+
+          if (this._currentItem)
+            this._currentItem.current = false;
+          this._currentItem = val;
+
+          if (val)
+            val.current = true;
+
+          return val;
+        </setter>
+      </property>
+
+      <!-- nsIDOMXULMultiSelectControlElement -->
+      <property name="currentIndex">
+        <getter>
+          return this.currentItem ? this.getIndexOfItem(this.currentItem) : -1;
+        </getter>
+        <setter>
+        <![CDATA[
+          if (val >= 0)
+            this.currentItem = this.getItemAtIndex(val);
+          else
+            this.currentItem = null;
+        ]]>
+        </setter>
+      </property>
+
+      <!-- nsIDOMXULMultiSelectControlElement -->
+      <field name="selectedItems">new ChromeNodeList()</field>
+
+      <!-- nsIDOMXULMultiSelectControlElement -->
+      <method name="addItemToSelection">
+        <parameter name="aItem"/>
+        <body>
+        <![CDATA[
+          if (this.selType != "multiple" && this.selectedCount)
+            return;
+
+          if (aItem.selected)
+            return;
+
+          this.selectedItems.append(aItem);
+          aItem.selected = true;
+
+          this._fireOnSelect();
+        ]]>
+        </body>
+      </method>
+
+      <!-- nsIDOMXULMultiSelectControlElement -->
+      <method name="removeItemFromSelection">
+        <parameter name="aItem"/>
+        <body>
+        <![CDATA[
+          if (!aItem.selected)
+            return;
+
+          this.selectedItems.remove(aItem);
+          aItem.selected = false;
+          this._fireOnSelect();
+        ]]>
+        </body>
+      </method>
+
+      <!-- nsIDOMXULMultiSelectControlElement -->
+      <method name="toggleItemSelection">
+        <parameter name="aItem"/>
+        <body>
+        <![CDATA[
+          if (aItem.selected)
+            this.removeItemFromSelection(aItem);
+          else
+            this.addItemToSelection(aItem);
+        ]]>
+        </body>
+      </method>
+
+      <!-- nsIDOMXULMultiSelectControlElement -->
+      <method name="selectItem">
+        <parameter name="aItem"/>
+        <body>
+        <![CDATA[
+          if (!aItem)
+            return;
+
+          if (this.selectedItems.length == 1 && this.selectedItems[0] == aItem)
+            return;
+
+          this._selectionStart = null;
+
+          var suppress = this._suppressOnSelect;
+          this._suppressOnSelect = true;
+
+          this.clearSelection();
+          this.addItemToSelection(aItem);
+          this.currentItem = aItem;
+
+          this._suppressOnSelect = suppress;
+          this._fireOnSelect();
+        ]]>
+        </body>
+      </method>
+
+      <!-- nsIDOMXULMultiSelectControlElement -->
+      <method name="selectItemRange">
+        <parameter name="aStartItem"/>
+        <parameter name="aEndItem"/>
+        <body>
+        <![CDATA[
+          if (this.selType != "multiple")
+            return;
+
+          if (!aStartItem)
+            aStartItem = this._selectionStart ?
+              this._selectionStart : this.currentItem;
+
+          if (!aStartItem)
+            aStartItem = aEndItem;
+
+          var suppressSelect = this._suppressOnSelect;
+          this._suppressOnSelect = true;
+
+          this._selectionStart = aStartItem;
+
+          var currentItem;
+          var startIndex = this.getIndexOfItem(aStartItem);
+          var endIndex = this.getIndexOfItem(aEndItem);
+          if (endIndex < startIndex) {
+            currentItem = aEndItem;
+            aEndItem = aStartItem;
+            aStartItem = currentItem;
+          } else {
+            currentItem = aStartItem;
+          }
+
+          while (currentItem) {
+            this.addItemToSelection(currentItem);
+            if (currentItem == aEndItem) {
+              currentItem = this.getNextItem(currentItem, 1);
+              break;
+            }
+            currentItem = this.getNextItem(currentItem, 1);
+          }
+
+          // Clear around new selection
+          // Don't use clearSelection() because it causes a lot of noise
+          // with respect to selection removed notifications used by the
+          // accessibility API support.
+          var userSelecting = this._userSelecting;
+          this._userSelecting = false; // that's US automatically unselecting
+          for (; currentItem; currentItem = this.getNextItem(currentItem, 1))
+            this.removeItemFromSelection(currentItem);
+
+          for (currentItem = this.getItemAtIndex(0); currentItem != aStartItem;
+               currentItem = this.getNextItem(currentItem, 1))
+            this.removeItemFromSelection(currentItem);
+          this._userSelecting = userSelecting;
+
+          this._suppressOnSelect = suppressSelect;
+
+          this._fireOnSelect();
+        ]]>
+        </body>
+      </method>
+
+      <!-- nsIDOMXULMultiSelectControlElement -->
+      <method name="selectAll">
+        <body>
+          this._selectionStart = null;
+
+          var suppress = this._suppressOnSelect;
+          this._suppressOnSelect = true;
+
+          var item = this.getItemAtIndex(0);
+          while (item) {
+            this.addItemToSelection(item);
+            item = this.getNextItem(item, 1);
+          }
+
+          this._suppressOnSelect = suppress;
+          this._fireOnSelect();
+        </body>
+      </method>
+
+      <!-- nsIDOMXULMultiSelectControlElement -->
+      <method name="invertSelection">
+        <body>
+          this._selectionStart = null;
+
+          var suppress = this._suppressOnSelect;
+          this._suppressOnSelect = true;
+
+          var item = this.getItemAtIndex(0);
+          while (item) {
+            if (item.selected)
+              this.removeItemFromSelection(item);
+            else
+              this.addItemToSelection(item);
+            item = this.getNextItem(item, 1);
+          }
+
+          this._suppressOnSelect = suppress;
+          this._fireOnSelect();
+        </body>
+      </method>
+
+      <!-- nsIDOMXULMultiSelectControlElement -->
+      <method name="clearSelection">
+        <body>
+        <![CDATA[
+          if (this.selectedItems) {
+            while (this.selectedItems.length > 0) {
+              let item = this.selectedItems[0];
+              item.selected = false;
+              this.selectedItems.remove(item);
+            }
+          }
+
+          this._selectionStart = null;
+          this._fireOnSelect();
+        ]]>
+        </body>
+      </method>
+
+      <!-- nsIDOMXULMultiSelectControlElement -->
+      <property name="selectedCount" readonly="true"
+                onget="return this.selectedItems.length;"/>
+
+      <!-- nsIDOMXULMultiSelectControlElement -->
+      <method name="getSelectedItem">
+        <parameter name="aIndex"/>
+        <body>
+        <![CDATA[
+          return aIndex < this.selectedItems.length ?
+            this.selectedItems[aIndex] : null;
+        ]]>
+        </body>
+      </method>
+
       <method name="ensureIndexIsVisible">
         <parameter name="aIndex"/>
         <body>
           <![CDATA[
             return this.ensureElementIsVisible(this.getItemAtIndex(aIndex));
           ]]>
         </body>
       </method>
@@ -250,17 +562,16 @@
               index = ix;
             }
 
             return index != this.currentIndex ? index - this.currentIndex : aDirection;
           ]]>
         </body>
       </method>
 
-    <!-- richlistbox specific -->
       <property name="children" readonly="true">
         <getter>
           <![CDATA[
             let iface = Ci.nsIDOMXULSelectControlItemElement;
             let children = Array.from(this.childNodes)
                                 .filter(node => node instanceof iface);
             if (this.dir == "reverse" && this._mayReverse) {
               children.reverse();
@@ -369,30 +680,224 @@
 
             // Partially visible items are also considered visible
             return (aItem.boxObject.y + aItem.boxObject.height > y) &&
                    (aItem.boxObject.y < y + this._scrollbox.boxObject.height);
           ]]>
         </body>
       </method>
 
+      <property name="disableKeyNavigation"
+                onget="return this.hasAttribute('disableKeyNavigation');">
+        <setter>
+          if (val)
+            this.setAttribute("disableKeyNavigation", "true");
+          else
+            this.removeAttribute("disableKeyNavigation");
+          return val;
+        </setter>
+      </property>
+
+      <property name="suppressOnSelect"
+                onget="return this.getAttribute('suppressonselect') == 'true';"
+                onset="this.setAttribute('suppressonselect', val);"/>
+
+      <property name="_selectDelay"
+                onset="this.setAttribute('_selectDelay', val);"
+                onget="return this.getAttribute('_selectDelay') || 50;"/>
+
+      <method name="_moveByOffsetFromUserEvent">
+        <parameter name="aOffset"/>
+        <parameter name="aEvent"/>
+        <body>
+        <![CDATA[
+          if (!aEvent.defaultPrevented) {
+            this._userSelecting = true;
+            this._mayReverse = true;
+            this.moveByOffset(aOffset, !aEvent.ctrlKey, aEvent.shiftKey);
+            this._userSelecting = false;
+            this._mayReverse = false;
+            aEvent.preventDefault();
+          }
+        ]]>
+        </body>
+      </method>
+
+      <method name="_canUserSelect">
+        <parameter name="aItem"/>
+        <body>
+        <![CDATA[
+          var style = document.defaultView.getComputedStyle(aItem);
+          return style.display != "none" && style.visibility == "visible";
+        ]]>
+        </body>
+      </method>
+
+      <method name="_selectTimeoutHandler">
+        <parameter name="aMe"/>
+        <body>
+          aMe._fireOnSelect();
+          aMe._selectTimeout = null;
+        </body>
+      </method>
+
+      <method name="timedSelect">
+        <parameter name="aItem"/>
+        <parameter name="aTimeout"/>
+        <body>
+        <![CDATA[
+          var suppress = this._suppressOnSelect;
+          if (aTimeout != -1)
+            this._suppressOnSelect = true;
+
+          this.selectItem(aItem);
+
+          this._suppressOnSelect = suppress;
+
+          if (aTimeout != -1) {
+            if (this._selectTimeout)
+              window.clearTimeout(this._selectTimeout);
+            this._selectTimeout =
+              window.setTimeout(this._selectTimeoutHandler, aTimeout, this);
+          }
+        ]]>
+        </body>
+      </method>
+
       <field name="_currentIndex">null</field>
+      <field name="_lastKeyTime">0</field>
+      <field name="_incrementalString">""</field>
+      <field name="_suppressOnSelect">false</field>
+      <field name="_userSelecting">false</field>
+      <field name="_mayReverse">false</field>
+      <field name="_selectTimeout">null</field>
+      <field name="_currentItem">null</field>
+      <field name="_selectionStart">null</field>
 
       <!-- For backwards-compatibility and for convenience.
         Use ensureElementIsVisible instead -->
       <method name="ensureSelectedElementIsVisible">
         <body>
           <![CDATA[
             return this.ensureElementIsVisible(this.selectedItem);
           ]]>
         </body>
       </method>
     </implementation>
 
     <handlers>
+      <handler event="keypress" keycode="VK_UP" modifiers="control shift any"
+               action="this._moveByOffsetFromUserEvent(-1, event);"
+               group="system"/>
+
+      <handler event="keypress" keycode="VK_DOWN" modifiers="control shift any"
+               action="this._moveByOffsetFromUserEvent(1, event);"
+               group="system"/>
+
+      <handler event="keypress" keycode="VK_HOME" modifiers="control shift any"
+               group="system">
+        <![CDATA[
+          this._mayReverse = true;
+          this._moveByOffsetFromUserEvent(-this.currentIndex, event);
+          this._mayReverse = false;
+        ]]>
+      </handler>
+
+      <handler event="keypress" keycode="VK_END" modifiers="control shift any"
+               group="system">
+        <![CDATA[
+          this._mayReverse = true;
+          this._moveByOffsetFromUserEvent(this.getRowCount() - this.currentIndex - 1, event);
+          this._mayReverse = false;
+        ]]>
+      </handler>
+
+      <handler event="keypress" keycode="VK_PAGE_UP" modifiers="control shift any"
+               group="system">
+        <![CDATA[
+          this._mayReverse = true;
+          this._moveByOffsetFromUserEvent(this.scrollOnePage(-1), event);
+          this._mayReverse = false;
+        ]]>
+      </handler>
+
+      <handler event="keypress" keycode="VK_PAGE_DOWN" modifiers="control shift any"
+               group="system">
+        <![CDATA[
+          this._mayReverse = true;
+          this._moveByOffsetFromUserEvent(this.scrollOnePage(1), event);
+          this._mayReverse = false;
+        ]]>
+      </handler>
+
+      <handler event="keypress" key=" " modifiers="control" phase="target">
+        <![CDATA[
+          if (this.currentItem && this.selType == "multiple")
+            this.toggleItemSelection(this.currentItem);
+        ]]>
+      </handler>
+
+      <handler event="focus">
+        <![CDATA[
+          if (this.getRowCount() > 0) {
+            if (this.currentIndex == -1) {
+              this.currentIndex = this.getIndexOfFirstVisibleRow();
+            } else {
+              this.currentItem._fireEvent("DOMMenuItemActive");
+            }
+          }
+          this._lastKeyTime = 0;
+        ]]>
+      </handler>
+
+      <handler event="keypress" phase="target">
+        <![CDATA[
+          if (this.disableKeyNavigation || !event.charCode ||
+              event.altKey || event.ctrlKey || event.metaKey)
+            return;
+
+          if (event.timeStamp - this._lastKeyTime > 1000)
+            this._incrementalString = "";
+
+          var key = String.fromCharCode(event.charCode).toLowerCase();
+          this._incrementalString += key;
+          this._lastKeyTime = event.timeStamp;
+
+          // If all letters in the incremental string are the same, just
+          // try to match the first one
+          var incrementalString = /^(.)\1+$/.test(this._incrementalString) ?
+                                  RegExp.$1 : this._incrementalString;
+          var length = incrementalString.length;
+
+          var rowCount = this.getRowCount();
+          var l = this.selectedItems.length;
+          var start = l > 0 ? this.getIndexOfItem(this.selectedItems[l - 1]) : -1;
+          // start from the first element if none was selected or from the one
+          // following the selected one if it's a new or a repeated-letter search
+          if (start == -1 || length == 1)
+            start++;
+
+          for (var i = 0; i < rowCount; i++) {
+            var k = (start + i) % rowCount;
+            var listitem = this.getItemAtIndex(k);
+            if (!this._canUserSelect(listitem))
+              continue;
+            // allow richlistitems to specify the string being searched for
+            var searchText = "searchLabel" in listitem ? listitem.searchLabel :
+                             listitem.getAttribute("label"); // (see also bug 250123)
+            searchText = searchText.substring(0, length).toLowerCase();
+            if (searchText == incrementalString) {
+              this.ensureIndexIsVisible(k);
+              this.timedSelect(listitem, this._selectDelay);
+              break;
+            }
+          }
+        ]]>
+      </handler>
+
       <handler event="click">
         <![CDATA[
           // clicking into nothing should unselect
           if (event.originalTarget == this._scrollbox) {
             this.clearSelection();
             this.currentItem = null;
           }
         ]]>
@@ -410,18 +915,18 @@
               break;
           }
         ]]>
       </handler>
     </handlers>
   </binding>
 
   <binding id="richlistitem"
-           extends="chrome://global/content/bindings/listbox.xml#listitem">
-    <implementation>
+           extends="chrome://global/content/bindings/general.xml#basetext">
+    <implementation implements="nsIDOMXULSelectControlItemElement">
       <field name="selectedByMouseOver">false</field>
 
       <destructor>
         <![CDATA[
           var control = this.control;
           if (!control)
             return;
           // When we are destructed and we are current or selected, unselect ourselves
@@ -433,16 +938,17 @@
             control.removeItemFromSelection(this);
             control._suppressOnSelect = suppressSelect;
           }
           if (this.current)
             control.currentItem = null;
         ]]>
       </destructor>
 
+      <!-- nsIDOMXULSelectControlItemElement -->
       <property name="label" readonly="true">
         <!-- Setter purposely not implemented; the getter returns a
              concatentation of label text to expose via accessibility APIs -->
         <getter>
           <![CDATA[
             const XULNS =
               "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
             return Array.map(this.getElementsByTagNameNS(XULNS, "label"),
@@ -465,11 +971,116 @@
               this.setAttribute("searchlabel", val);
             else
               // fall back to the label property (default value)
               this.removeAttribute("searchlabel");
             return val;
           ]]>
         </setter>
       </property>
+
+      <!-- nsIDOMXULSelectControlItemElement -->
+      <property name="value" onget="return this.getAttribute('value');"
+                             onset="this.setAttribute('value', val); return val;"/>
+
+      <!-- nsIDOMXULSelectControlItemElement -->
+      <property name="selected" onget="return this.getAttribute('selected') == 'true';">
+        <setter><![CDATA[
+          if (val)
+            this.setAttribute("selected", "true");
+          else
+            this.removeAttribute("selected");
+
+          return val;
+        ]]></setter>
+      </property>
+
+      <!-- nsIDOMXULSelectControlItemElement -->
+      <property name="control">
+        <getter><![CDATA[
+          var parent = this.parentNode;
+          while (parent) {
+            if (parent instanceof Ci.nsIDOMXULSelectControlElement)
+              return parent;
+            parent = parent.parentNode;
+          }
+          return null;
+        ]]></getter>
+      </property>
+
+      <property name="current" onget="return this.getAttribute('current') == 'true';">
+        <setter><![CDATA[
+          if (val)
+            this.setAttribute("current", "true");
+          else
+            this.removeAttribute("current");
+
+          let control = this.control;
+          if (!control || !control.suppressMenuItemEvent) {
+            this._fireEvent(val ? "DOMMenuItemActive" : "DOMMenuItemInactive");
+          }
+
+          return val;
+        ]]></setter>
+      </property>
+
+      <method name="_fireEvent">
+        <parameter name="name"/>
+        <body>
+        <![CDATA[
+          var event = document.createEvent("Events");
+          event.initEvent(name, true, true);
+          this.dispatchEvent(event);
+        ]]>
+        </body>
+      </method>
     </implementation>
+
+    <handlers>
+      <!-- If there is no modifier key, we select on mousedown, not
+           click, so that drags work correctly. -->
+      <handler event="mousedown">
+        <![CDATA[
+          var control = this.control;
+          if (!control || control.disabled)
+            return;
+          if ((!event.ctrlKey || (/Mac/.test(navigator.platform) && event.button == 2)) &&
+              !event.shiftKey && !event.metaKey) {
+            if (!this.selected) {
+              control.selectItem(this);
+            }
+            control.currentItem = this;
+          }
+        ]]>
+      </handler>
+
+      <!-- On a click (up+down on the same item), deselect everything
+           except this item. -->
+      <handler event="click" button="0">
+        <![CDATA[
+          var control = this.control;
+          if (!control || control.disabled)
+            return;
+          control._userSelecting = true;
+          if (control.selType != "multiple") {
+            control.selectItem(this);
+          } else if (event.ctrlKey || event.metaKey) {
+            control.toggleItemSelection(this);
+            control.currentItem = this;
+          } else if (event.shiftKey) {
+            control.selectItemRange(null, this);
+            control.currentItem = this;
+          } else {
+            /* We want to deselect all the selected items except what was
+              clicked, UNLESS it was a right-click.  We have to do this
+              in click rather than mousedown so that you can drag a
+              selected group of items */
+
+            // use selectItemRange instead of selectItem, because this
+            // doesn't de- and reselect this item if it is selected
+            control.selectItemRange(this, this);
+          }
+          control._userSelecting = false;
+        ]]>
+      </handler>
+    </handlers>
   </binding>
 </bindings>