Bug 1472558 - Convert "richlistbox" to Custom Element. r=bgrins
authorPaolo Amadini <paolo.mozmail@amadzone.org>
Mon, 31 Dec 2018 07:54:10 +0000
changeset 509592 ff84fa85610239d5e0fc4e91cc3c03989c4f6d6e
parent 509591 3b8916cd3020886a3b7a3e6d05c245db14a26dc3
child 509593 ca06e78452b02ba2979842b7259812257cde1633
push id10547
push userffxbld-merge
push dateMon, 21 Jan 2019 13:03:58 +0000
treeherdermozilla-beta@24ec1916bffe [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbgrins
bugs1472558
milestone66.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1472558 - Convert "richlistbox" to Custom Element. r=bgrins Differential Revision: https://phabricator.services.mozilla.com/D15502
accessible/xul/XULSelectControlAccessible.cpp
toolkit/content/customElements.js
toolkit/content/jar.mn
toolkit/content/widgets/richlistbox.js
toolkit/content/widgets/richlistbox.xml
toolkit/content/xul.css
--- a/accessible/xul/XULSelectControlAccessible.cpp
+++ b/accessible/xul/XULSelectControlAccessible.cpp
@@ -40,17 +40,17 @@ void XULSelectControlAccessible::Shutdow
 }
 
 ////////////////////////////////////////////////////////////////////////////////
 // XULSelectControlAccessible: SelectAccessible
 
 void XULSelectControlAccessible::SelectedItems(nsTArray<Accessible*>* aItems) {
   // For XUL multi-select control
   nsCOMPtr<nsIDOMXULMultiSelectControlElement> xulMultiSelect =
-      do_QueryInterface(mSelectControl);
+      mSelectControl->AsXULMultiSelectControl();
   if (xulMultiSelect) {
     int32_t length = 0;
     xulMultiSelect->GetSelectedCount(&length);
     for (int32_t index = 0; index < length; index++) {
       RefPtr<Element> element;
       xulMultiSelect->MultiGetSelectedItem(index, getter_AddRefs(element));
       Accessible* item = mDoc->GetAccessible(element);
       if (item) aItems->AppendElement(item);
--- a/toolkit/content/customElements.js
+++ b/toolkit/content/customElements.js
@@ -301,16 +301,17 @@ if (!isDummyDocument) {
     "chrome://global/content/elements/tabbox.js",
     "chrome://global/content/elements/tree.js",
   ]) {
     Services.scriptloader.loadSubScript(script, window);
   }
 
   for (let [tag, script] of [
     ["findbar", "chrome://global/content/elements/findbar.js"],
+    ["richlistbox", "chrome://global/content/elements/richlistbox.js"],
     ["stringbundle", "chrome://global/content/elements/stringbundle.js"],
     ["printpreview-toolbar", "chrome://global/content/printPreviewToolbar.js"],
     ["editor", "chrome://global/content/elements/editor.js"],
   ]) {
     customElements.setElementCreationCallback(tag, () => {
       Services.scriptloader.loadSubScript(script, window);
     });
   }
--- a/toolkit/content/jar.mn
+++ b/toolkit/content/jar.mn
@@ -93,16 +93,17 @@ toolkit.jar:
 *  content/global/bindings/wizard.xml          (widgets/wizard.xml)
    content/global/elements/datetimebox.js      (widgets/datetimebox.js)
    content/global/elements/findbar.js          (widgets/findbar.js)
    content/global/elements/editor.js          (widgets/editor.js)
    content/global/elements/general.js          (widgets/general.js)
    content/global/elements/notificationbox.js  (widgets/notificationbox.js)
    content/global/elements/pluginProblem.js    (widgets/pluginProblem.js)
    content/global/elements/radio.js            (widgets/radio.js)
+   content/global/elements/richlistbox.js      (widgets/richlistbox.js)
    content/global/elements/marquee.css         (widgets/marquee.css)
    content/global/elements/marquee.js          (widgets/marquee.js)
    content/global/elements/stringbundle.js     (widgets/stringbundle.js)
    content/global/elements/tabbox.js           (widgets/tabbox.js)
    content/global/elements/textbox.js          (widgets/textbox.js)
    content/global/elements/videocontrols.js    (widgets/videocontrols.js)
    content/global/elements/tree.js             (widgets/tree.js)
 #ifdef XP_MACOSX
copy from toolkit/content/widgets/richlistbox.xml
copy to toolkit/content/widgets/richlistbox.js
--- a/toolkit/content/widgets/richlistbox.xml
+++ b/toolkit/content/widgets/richlistbox.js
@@ -1,1097 +1,830 @@
-<?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 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 -->
+"use strict";
 
-<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">
+// This is loaded into all XUL windows. Wrap in a block to prevent
+// leaking to window scope.
+{
 
-  <binding id="richlistbox"
-           extends="chrome://global/content/bindings/general.xml#basecontrol">
-    <content allowevents="true" orient="vertical"/>
+class MozRichListBox extends MozElements.BaseControl {
+  constructor() {
+    super();
 
-    <implementation implements="nsIDOMXULMultiSelectControlElement">
-      <constructor>
-        <![CDATA[
-          this._refreshSelection();
-        ]]>
-      </constructor>
+    this.addEventListener("keypress", event => {
+      if (event.altKey || event.metaKey) {
+        return;
+      }
 
-      <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;
+      switch (event.keyCode) {
+        case KeyEvent.DOM_VK_UP:
+          this._moveByOffsetFromUserEvent(-1, event);
+          break;
+        case KeyEvent.DOM_VK_DOWN:
+          this._moveByOffsetFromUserEvent(1, event);
+          break;
+        case KeyEvent.DOM_VK_HOME:
+          this._moveByOffsetFromUserEvent(-this.currentIndex, event);
+          break;
+        case KeyEvent.DOM_VK_END:
+          this._moveByOffsetFromUserEvent(this.getRowCount() - this.currentIndex - 1, event);
+          break;
+        case KeyEvent.DOM_VK_PAGE_UP:
+          this._moveByOffsetFromUserEvent(this.scrollOnePage(-1), event);
+          break;
+        case KeyEvent.DOM_VK_PAGE_DOWN:
+          this._moveByOffsetFromUserEvent(this.scrollOnePage(1), event);
+          break;
+      }
+    }, { mozSystemGroup: true });
 
-            // remember the current item and all selected items with IDs
-            var state = this.currentItem ? this.currentItem.id : "";
-            if (this.selType == "multiple" && this.selectedCount) {
-              let getId = function getId(aItem) { return aItem.id; };
-              state += " " + [...this.selectedItems].filter(getId).map(getId).join(" ");
-            }
-            if (state)
-              this.setAttribute("last-selected", state);
-            else
-              this.removeAttribute("last-selected");
-
-            // preserve the index just in case no IDs are available
-            if (this.currentIndex > -1)
-              this._currentIndex = this.currentIndex + 1;
-
-            var event = document.createEvent("Events");
-            event.initEvent("select", true, true);
-            this.dispatchEvent(event);
-
-            // always call this (allows a commandupdater without controller)
-            document.commandDispatcher.updateCommands("richlistbox-select");
-          ]]>
-        </body>
-      </method>
+    this.addEventListener("keypress", event => {
+      if (event.target != this) {
+        return;
+      }
 
-      <method name="getNextItem">
-        <parameter name="aStartItem"/>
-        <parameter name="aDelta"/>
-        <body>
-        <![CDATA[
-          while (aStartItem) {
-            aStartItem = aStartItem.nextSibling;
-            if (aStartItem && aStartItem.localName == "richlistitem" &&
-                (!this._userSelecting || this._canUserSelect(aStartItem))) {
-              --aDelta;
-              if (aDelta == 0)
-                return aStartItem;
-            }
-          }
-          return null;
-        ]]>
-        </body>
-      </method>
+      if (event.key == " " &&
+          event.ctrlKey && !event.shiftKey && !event.altKey && !event.metaKey &&
+          this.currentItem && this.selType == "multiple") {
+        this.toggleItemSelection(this.currentItem);
+      }
+
+      if (!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;
 
-      <method name="getPreviousItem">
-        <parameter name="aStartItem"/>
-        <parameter name="aDelta"/>
-        <body>
-        <![CDATA[
-          while (aStartItem) {
-            aStartItem = aStartItem.previousSibling;
-            if (aStartItem && aStartItem.localName == "richlistitem" &&
-                (!this._userSelecting || this._canUserSelect(aStartItem))) {
-              --aDelta;
-              if (aDelta == 0)
-                return aStartItem;
-            }
-          }
-          return null;
-        ]]>
-        </body>
-      </method>
+      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++;
+      }
 
-      <method name="appendItem">
-        <parameter name="aLabel"/>
-        <parameter name="aValue"/>
-        <body>
-          var item =
-            this.ownerDocument.createXULElement("richlistitem");
-          item.setAttribute("value", aValue);
-
-          var label = this.ownerDocument.createXULElement("label");
-          label.setAttribute("value", aLabel);
-          label.setAttribute("flex", "1");
-          label.setAttribute("crop", "end");
-          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>
+      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;
+        }
+      }
+    });
 
-      <!-- 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;
+    this.addEventListener("focus", event => {
+      if (this.getRowCount() > 0) {
+        if (this.currentIndex == -1) {
+          this.currentIndex = this.getIndexOfFirstVisibleRow();
+          let currentItem = this.getItemAtIndex(this.currentIndex);
+          if (currentItem) {
+            this.selectItem(currentItem);
           }
-        ]]>
-        </setter>
-      </property>
+        } else {
+          this.currentItem._fireEvent("DOMMenuItemActive");
+        }
+      }
+      this._lastKeyTime = 0;
+    });
+
+    this.addEventListener("click", event => {
+      // clicking into nothing should unselect
+      if (event.originalTarget == this) {
+        this.clearSelection();
+        this.currentItem = null;
+      }
+    });
 
-      <!-- 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>
+    this.addEventListener("MozSwipeGesture", event => {
+      // Only handle swipe gestures up and down
+      switch (event.direction) {
+        case event.DIRECTION_DOWN:
+          this.scrollTop = this.scrollHeight;
+          break;
+        case event.DIRECTION_UP:
+          this.scrollTop = 0;
+          break;
+      }
+    });
+  }
 
-      <!-- nsIDOMXULSelectControlElement -->
-      <property name="itemCount" readonly="true"
-                onget="return this.itemChildren.length"/>
+  connectedCallback() {
+    if (this.delayConnectedCallback()) {
+      return;
+    }
+
+    this.setAttribute("allowevents", "true");
 
-      <!-- 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.itemChildren.indexOf(aItem);
-          ]]>
-        </body>
-      </method>
+    this.selectedItems = new ChromeNodeList();
+    this._currentIndex = null;
+    this._lastKeyTime = 0;
+    this._incrementalString = "";
+    this._suppressOnSelect = false;
+    this._userSelecting = false;
+    this._selectTimeout = null;
+    this._currentItem = null;
+    this._selectionStart = null;
 
-      <!-- nsIDOMXULSelectControlElement -->
-      <method name="getItemAtIndex">
-        <parameter name="aIndex"/>
-        <body>
-          <![CDATA[
-            if (this._selecting && this._selecting.index == aIndex)
-              return this._selecting.item;
-            return this.itemChildren[aIndex] || null;
-          ]]>
-        </body>
-      </method>
-
-      <!-- nsIDOMXULMultiSelectControlElement -->
-      <property name="selType"
-                onget="return this.getAttribute('seltype');"
-                onset="this.setAttribute('seltype', val); return val;"/>
+    this._refreshSelection();
+  }
 
-      <!-- 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>
+  // nsIDOMXULSelectControlElement
+  set selectedItem(val) {
+    this.selectItem(val);
+  }
+  get selectedItem() {
+    return this.selectedItems.length > 0 ? this.selectedItems[0] : null;
+  }
 
-      <!-- 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>
+  // nsIDOMXULSelectControlElement
+  set selectedIndex(val) {
+    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;
+    }
+  }
+  get selectedIndex() {
+    if (this.selectedItems.length > 0) {
+      return this.getIndexOfItem(this.selectedItems[0]);
+    }
+    return -1;
+  }
 
-      <!-- nsIDOMXULMultiSelectControlElement -->
-      <method name="addItemToSelection">
-        <parameter name="aItem"/>
-        <body>
-        <![CDATA[
-          if (this.selType != "multiple" && this.selectedCount)
-            return;
+  // nsIDOMXULSelectControlElement
+  set value(val) {
+    var kids = this.getElementsByAttribute("value", val);
+    if (kids && kids.item(0)) {
+      this.selectItem(kids[0]);
+    }
+    return val;
+  }
+  get value() {
+    if (this.selectedItems.length > 0) {
+      return this.selectedItem.value;
+    }
+    return null;
+  }
 
-          if (aItem.selected)
-            return;
-
-          this.selectedItems.append(aItem);
-          aItem.selected = true;
-
-          this._fireOnSelect();
-        ]]>
-        </body>
-      </method>
+  // nsIDOMXULSelectControlElement
+  get itemCount() {
+    return this.itemChildren.length;
+  }
 
-      <!-- nsIDOMXULMultiSelectControlElement -->
-      <method name="removeItemFromSelection">
-        <parameter name="aItem"/>
-        <body>
-        <![CDATA[
-          if (!aItem.selected)
-            return;
+  // nsIDOMXULSelectControlElement
+  set selType(val) {
+    this.setAttribute("seltype", val);
+    return val;
+  }
+  get selType() {
+    return this.getAttribute("seltype");
+  }
 
-          this.selectedItems.remove(aItem);
-          aItem.selected = false;
-          this._fireOnSelect();
-        ]]>
-        </body>
-      </method>
+  // nsIDOMXULSelectControlElement
+  set currentItem(val) {
+    if (this._currentItem == val) {
+      return val;
+    }
 
-      <!-- nsIDOMXULMultiSelectControlElement -->
-      <method name="toggleItemSelection">
-        <parameter name="aItem"/>
-        <body>
-        <![CDATA[
-          if (aItem.selected)
-            this.removeItemFromSelection(aItem);
-          else
-            this.addItemToSelection(aItem);
-        ]]>
-        </body>
-      </method>
+    if (this._currentItem) {
+      this._currentItem.current = false;
+    }
+    this._currentItem = val;
 
-      <!-- nsIDOMXULMultiSelectControlElement -->
-      <method name="selectItem">
-        <parameter name="aItem"/>
-        <body>
-        <![CDATA[
-          if (!aItem)
-            return;
+    if (val) {
+      val.current = true;
+    }
 
-          if (this.selectedItems.length == 1 && this.selectedItems[0] == aItem)
-            return;
-
-          this._selectionStart = null;
-
-          var suppress = this._suppressOnSelect;
-          this._suppressOnSelect = true;
+    return val;
+  }
+  get currentItem() {
+    return this._currentItem;
+  }
 
-          this.clearSelection();
-          this.addItemToSelection(aItem);
-          this.currentItem = aItem;
-
-          this._suppressOnSelect = suppress;
-          this._fireOnSelect();
-        ]]>
-        </body>
-      </method>
+  // nsIDOMXULSelectControlElement
+  set currentIndex(val) {
+    if (val >= 0) {
+      this.currentItem = this.getItemAtIndex(val);
+    } else {
+      this.currentItem = null;
+    }
+  }
+  get currentIndex() {
+    return this.currentItem ? this.getIndexOfItem(this.currentItem) : -1;
+  }
 
-      <!-- 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;
+  // nsIDOMXULSelectControlElement
+  get selectedCount() {
+    return this.selectedItems.length;
+  }
 
-          if (!aStartItem)
-            aStartItem = aEndItem;
-
-          var suppressSelect = this._suppressOnSelect;
-          this._suppressOnSelect = true;
-
-          this._selectionStart = aStartItem;
+  get itemChildren() {
+    let children = Array.from(this.children)
+      .filter(node => node.localName == "richlistitem");
+    return children;
+  }
 
-          var currentItem;
-          var startIndex = this.getIndexOfItem(aStartItem);
-          var endIndex = this.getIndexOfItem(aEndItem);
-          if (endIndex < startIndex) {
-            currentItem = aEndItem;
-            aEndItem = aStartItem;
-            aStartItem = currentItem;
-          } else {
-            currentItem = aStartItem;
-          }
+  set suppressOnSelect(val) {
+    this.setAttribute("suppressonselect", val);
+  }
+  get suppressOnSelect() {
+    return this.getAttribute("suppressonselect") == "true";
+  }
 
-          while (currentItem) {
-            this.addItemToSelection(currentItem);
-            if (currentItem == aEndItem) {
-              currentItem = this.getNextItem(currentItem, 1);
-              break;
-            }
-            currentItem = this.getNextItem(currentItem, 1);
-          }
+  set _selectDelay(val) {
+    this.setAttribute("_selectDelay", val);
+  }
+  get _selectDelay() {
+    return this.getAttribute("_selectDelay") || 50;
+  }
 
-          // 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;
+  _fireOnSelect() {
+    // 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;
+    }
 
-          this._fireOnSelect();
-        ]]>
-        </body>
-      </method>
-
-      <!-- nsIDOMXULMultiSelectControlElement -->
-      <method name="selectAll">
-        <body>
-          this._selectionStart = null;
+    // remember the current item and all selected items with IDs
+    var state = this.currentItem ? this.currentItem.id : "";
+    if (this.selType == "multiple" && this.selectedCount) {
+      let getId = function getId(aItem) { return aItem.id; };
+      state += " " + [...this.selectedItems].filter(getId).map(getId).join(" ");
+    }
+    if (state) {
+      this.setAttribute("last-selected", state);
+    } else {
+      this.removeAttribute("last-selected");
+    }
 
-          var suppress = this._suppressOnSelect;
-          this._suppressOnSelect = true;
+    // preserve the index just in case no IDs are available
+    if (this.currentIndex > -1) {
+      this._currentIndex = this.currentIndex + 1;
+    }
 
-          var item = this.getItemAtIndex(0);
-          while (item) {
-            this.addItemToSelection(item);
-            item = this.getNextItem(item, 1);
-          }
+    var event = document.createEvent("Events");
+    event.initEvent("select", true, true);
+    this.dispatchEvent(event);
 
-          this._suppressOnSelect = suppress;
-          this._fireOnSelect();
-        </body>
-      </method>
+    // always call this (allows a commandupdater without controller)
+    document.commandDispatcher.updateCommands("richlistbox-select");
+  }
 
-      <!-- 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>
+  getNextItem(aStartItem, aDelta) {
+    while (aStartItem) {
+      aStartItem = aStartItem.nextSibling;
+      if (aStartItem && aStartItem.localName == "richlistitem" &&
+        (!this._userSelecting || this._canUserSelect(aStartItem))) {
+        --aDelta;
+        if (aDelta == 0) {
+          return aStartItem;
+        }
+      }
+    }
+    return null;
+  }
 
-      <!-- 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>
+  getPreviousItem(aStartItem, aDelta) {
+    while (aStartItem) {
+      aStartItem = aStartItem.previousSibling;
+      if (aStartItem && aStartItem.localName == "richlistitem" &&
+        (!this._userSelecting || this._canUserSelect(aStartItem))) {
+        --aDelta;
+        if (aDelta == 0) {
+          return aStartItem;
+        }
+      }
+    }
+    return null;
+  }
 
-      <!-- nsIDOMXULMultiSelectControlElement -->
-      <property name="selectedCount" readonly="true"
-                onget="return this.selectedItems.length;"/>
+  appendItem(aLabel, aValue) {
+    var item =
+      this.ownerDocument.createXULElement("richlistitem");
+    item.setAttribute("value", aValue);
 
-      <!-- nsIDOMXULMultiSelectControlElement -->
-      <method name="getSelectedItem">
-        <parameter name="aIndex"/>
-        <body>
-        <![CDATA[
-          return aIndex < this.selectedItems.length ?
-            this.selectedItems[aIndex] : null;
-        ]]>
-        </body>
-      </method>
+    var label = this.ownerDocument.createXULElement("label");
+    label.setAttribute("value", aLabel);
+    label.setAttribute("flex", "1");
+    label.setAttribute("crop", "end");
+    item.appendChild(label);
 
-      <method name="ensureIndexIsVisible">
-        <parameter name="aIndex"/>
-        <body>
-          <![CDATA[
-            return this.ensureElementIsVisible(this.getItemAtIndex(aIndex));
-          ]]>
-        </body>
-      </method>
+    this.appendChild(item);
+
+    return item;
+  }
 
-      <method name="ensureElementIsVisible">
-        <parameter name="aElement"/>
-        <parameter name="aAlignToTop"/>
-        <body>
-          <![CDATA[
-            if (!aElement) {
-              return;
-            }
+  // nsIDOMXULSelectControlElement
+  getIndexOfItem(aItem) {
+    // 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.itemChildren.indexOf(aItem);
+  }
 
-            // These calculations assume that there is no padding on the
-            // "richlistbox" element, although there might be a margin.
-            var targetRect = aElement.getBoundingClientRect();
-            var scrollRect = this.getBoundingClientRect();
-            var offset = targetRect.top - scrollRect.top;
-            if (!aAlignToTop && offset >= 0) {
-              // scrollRect.bottom wouldn't take a horizontal scroll bar into account
-              let scrollRectBottom = scrollRect.top + this.clientHeight;
-              offset = targetRect.bottom - scrollRectBottom;
-              if (offset <= 0)
-                return;
-            }
-            this.scrollTop += offset;
-          ]]>
-        </body>
-      </method>
+  // nsIDOMXULSelectControlElement
+  getItemAtIndex(aIndex) {
+    if (this._selecting && this._selecting.index == aIndex) {
+      return this._selecting.item;
+    }
+    return this.itemChildren[aIndex] || null;
+  }
 
-      <method name="scrollToIndex">
-        <parameter name="aIndex"/>
-        <body>
-          <![CDATA[
-            var item = this.getItemAtIndex(aIndex);
-            if (item) {
-              this.ensureElementIsVisible(item, true);
-            }
-          ]]>
-        </body>
-      </method>
+  // nsIDOMXULMultiSelectControlElement
+  addItemToSelection(aItem) {
+    if (this.selType != "multiple" && this.selectedCount) {
+      return;
+    }
+
+    if (aItem.selected) {
+      return;
+    }
+
+    this.selectedItems.append(aItem);
+    aItem.selected = true;
 
-      <method name="getIndexOfFirstVisibleRow">
-        <body>
-          <![CDATA[
-            var children = this.itemChildren;
+    this._fireOnSelect();
+  }
 
-            for (var ix = 0; ix < children.length; ix++)
-              if (this._isItemVisible(children[ix]))
-                return ix;
+  // nsIDOMXULMultiSelectControlElement
+  removeItemFromSelection(aItem) {
+    if (!aItem.selected) {
+      return;
+    }
 
-            return -1;
-          ]]>
-        </body>
-      </method>
+    this.selectedItems.remove(aItem);
+    aItem.selected = false;
+    this._fireOnSelect();
+  }
 
-      <method name="getRowCount">
-        <body>
-          <![CDATA[
-            return this.itemChildren.length;
-          ]]>
-        </body>
-      </method>
-
-      <method name="scrollOnePage">
-        <parameter name="aDirection"/> <!-- Must be -1 or 1 -->
-        <body>
-          <![CDATA[
-            var children = this.itemChildren;
+  // nsIDOMXULMultiSelectControlElement
+  toggleItemSelection(aItem) {
+    if (aItem.selected) {
+      this.removeItemFromSelection(aItem);
+    } else {
+      this.addItemToSelection(aItem);
+    }
+  }
 
-            if (children.length == 0)
-              return 0;
+  // nsIDOMXULMultiSelectControlElement
+  selectItem(aItem) {
+    if (!aItem) {
+      return;
+    }
 
-            // If nothing is selected, we just select the first element
-            // at the extreme we're moving away from
-            if (!this.currentItem)
-              return aDirection == -1 ? children.length : 0;
-
-            // If the current item is visible, scroll by one page so that
-            // the new current item is at approximately the same position as
-            // the existing current item.
-            if (this._isItemVisible(this.currentItem))
-              this.scrollBy(0, this.clientHeight * aDirection);
+    if (this.selectedItems.length == 1 && this.selectedItems[0] == aItem) {
+      return;
+    }
 
-            // Figure out, how many items fully fit into the view port
-            // (including the currently selected one), and determine
-            // the index of the first one lying (partially) outside
-            var height = this.clientHeight;
-            var startBorder = this.currentItem.boxObject.y;
-            if (aDirection == -1)
-              startBorder += this.currentItem.clientHeight;
+    this._selectionStart = null;
+
+    var suppress = this._suppressOnSelect;
+    this._suppressOnSelect = true;
+
+    this.clearSelection();
+    this.addItemToSelection(aItem);
+    this.currentItem = aItem;
 
-            var index = this.currentIndex;
-            for (var ix = index; 0 <= ix && ix < children.length; ix += aDirection) {
-              var boxObject = children[ix].boxObject;
-              if (boxObject.height == 0)
-                continue; // hidden children have a y of 0
-              var endBorder = boxObject.y + (aDirection == -1 ? boxObject.height : 0);
-              if ((endBorder - startBorder) * aDirection > height)
-                break; // we've reached the desired distance
-              index = ix;
-            }
+    this._suppressOnSelect = suppress;
+    this._fireOnSelect();
+  }
 
-            return index != this.currentIndex ? index - this.currentIndex : aDirection;
-          ]]>
-        </body>
-      </method>
+  // nsIDOMXULMultiSelectControlElement
+  selectItemRange(aStartItem, aEndItem) {
+    if (this.selType != "multiple") {
+      return;
+    }
 
-      <property name="itemChildren" readonly="true">
-        <getter>
-          <![CDATA[
-            let children = Array.from(this.children)
-                                .filter(node => node.localName == "richlistitem");
-            return children;
-          ]]>
-        </getter>
-      </property>
+    if (!aStartItem) {
+      aStartItem = this._selectionStart ? this._selectionStart
+                                        : this.currentItem;
+    }
 
-      <method name="_refreshSelection">
-        <body>
-          <![CDATA[
-            // when this method is called, we know that either the currentItem
-            // and selectedItems we have are null (ctor) or a reference to an
-            // element no longer in the DOM (template).
+    if (!aStartItem) {
+      aStartItem = aEndItem;
+    }
+
+    var suppressSelect = this._suppressOnSelect;
+    this._suppressOnSelect = true;
 
-            // first look for the last-selected attribute
-            var state = this.getAttribute("last-selected");
-            if (state) {
-              var ids = state.split(" ");
+    this._selectionStart = aStartItem;
 
-              var suppressSelect = this._suppressOnSelect;
-              this._suppressOnSelect = true;
-              this.clearSelection();
-              for (let i = 1; i < ids.length; i++) {
-                var selectedItem = document.getElementById(ids[i]);
-                if (selectedItem)
-                  this.addItemToSelection(selectedItem);
-              }
-
-              var currentItem = document.getElementById(ids[0]);
-              if (!currentItem && this._currentIndex)
-                currentItem = this.getItemAtIndex(Math.min(
-                  this._currentIndex - 1, this.getRowCount()));
-              if (currentItem) {
-                this.currentItem = currentItem;
-                if (this.selType != "multiple" && this.selectedCount == 0)
-                  this.selectedItem = currentItem;
+    var currentItem;
+    var startIndex = this.getIndexOfItem(aStartItem);
+    var endIndex = this.getIndexOfItem(aEndItem);
+    if (endIndex < startIndex) {
+      currentItem = aEndItem;
+      aEndItem = aStartItem;
+      aStartItem = currentItem;
+    } else {
+      currentItem = aStartItem;
+    }
 
-                if (this.clientHeight) {
-                  this.ensureElementIsVisible(currentItem);
-                } else {
-                  // XXX hack around a bug in ensureElementIsVisible as it will
-                  // scroll beyond the last element, bug 493645.
-                  this.ensureElementIsVisible(currentItem.previousElementSibling);
-                }
-              }
-              this._suppressOnSelect = suppressSelect;
-              // XXX actually it's just a refresh, but at least
-              // the Extensions manager expects this:
-              this._fireOnSelect();
-              return;
-            }
+    while (currentItem) {
+      this.addItemToSelection(currentItem);
+      if (currentItem == aEndItem) {
+        currentItem = this.getNextItem(currentItem, 1);
+        break;
+      }
+      currentItem = this.getNextItem(currentItem, 1);
+    }
 
-            // try to restore the selected items according to their IDs
-            // (applies after a template rebuild, if last-selected was not set)
-            if (this.selectedItems) {
-              let itemIds = [];
-              for (let i = this.selectedCount - 1; i >= 0; i--) {
-                let selectedItem = this.selectedItems[i];
-                itemIds.push(selectedItem.id);
-                this.selectedItems.remove(selectedItem);
-              }
-              for (let i = 0; i < itemIds.length; i++) {
-                let selectedItem = document.getElementById(itemIds[i]);
-                if (selectedItem) {
-                  this.selectedItems.append(selectedItem);
-                }
-              }
-            }
-            if (this.currentItem && this.currentItem.id)
-              this.currentItem = document.getElementById(this.currentItem.id);
-            else
-              this.currentItem = null;
+    // 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;
 
-            // if we have no previously current item or if the above check fails to
-            // find the previous nodes (which causes it to clear selection)
-            if (!this.currentItem && this.selectedCount == 0) {
-              this.currentIndex = this._currentIndex ? this._currentIndex - 1 : 0;
+    this._fireOnSelect();
+  }
 
-              // cf. listbox constructor:
-              // select items according to their attributes
-              var children = this.itemChildren;
-              for (let i = 0; i < children.length; ++i) {
-                if (children[i].getAttribute("selected") == "true")
-                  this.selectedItems.append(children[i]);
-              }
-            }
+  // nsIDOMXULMultiSelectControlElement
+  selectAll() {
+    this._selectionStart = null;
+
+    var suppress = this._suppressOnSelect;
+    this._suppressOnSelect = true;
 
-            if (this.selType != "multiple" && this.selectedCount == 0)
-              this.selectedItem = this.currentItem;
-          ]]>
-        </body>
-      </method>
+    var item = this.getItemAtIndex(0);
+    while (item) {
+      this.addItemToSelection(item);
+      item = this.getNextItem(item, 1);
+    }
 
-      <method name="_isItemVisible">
-        <parameter name="aItem"/>
-        <body>
-          <![CDATA[
-            if (!aItem)
-              return false;
+    this._suppressOnSelect = suppress;
+    this._fireOnSelect();
+  }
 
-            var y = this.scrollTop + this.boxObject.y;
+  // nsIDOMXULMultiSelectControlElement
+  invertSelection() {
+    this._selectionStart = null;
 
-            // Partially visible items are also considered visible
-            return (aItem.boxObject.y + aItem.clientHeight > y) &&
-                   (aItem.boxObject.y < y + this.clientHeight);
-          ]]>
-        </body>
-      </method>
+    var suppress = this._suppressOnSelect;
+    this._suppressOnSelect = true;
 
-      <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;"/>
+    var item = this.getItemAtIndex(0);
+    while (item) {
+      if (item.selected) {
+        this.removeItemFromSelection(item);
+      } else {
+        this.addItemToSelection(item);
+      }
+      item = this.getNextItem(item, 1);
+    }
 
-      <method name="moveByOffset">
-        <parameter name="aOffset"/>
-        <parameter name="aIsSelecting"/>
-        <parameter name="aIsSelectingRange"/>
-        <body>
-        <![CDATA[
-          if ((aIsSelectingRange || !aIsSelecting) &&
-              this.selType != "multiple")
-            return;
-
-          var newIndex = this.currentIndex + aOffset;
-          if (newIndex < 0)
-            newIndex = 0;
-
-          var numItems = this.getRowCount();
-          if (newIndex > numItems - 1)
-            newIndex = numItems - 1;
+    this._suppressOnSelect = suppress;
+    this._fireOnSelect();
+  }
 
-          var newItem = this.getItemAtIndex(newIndex);
-          // make sure that the item is actually visible/selectable
-          if (this._userSelecting && newItem && !this._canUserSelect(newItem))
-            newItem =
-              aOffset > 0 ? this.getNextItem(newItem, 1) || this.getPreviousItem(newItem, 1) :
-                            this.getPreviousItem(newItem, 1) || this.getNextItem(newItem, 1);
-          if (newItem) {
-            this.ensureIndexIsVisible(this.getIndexOfItem(newItem));
-            if (aIsSelectingRange)
-              this.selectItemRange(null, newItem);
-            else if (aIsSelecting)
-              this.selectItem(newItem);
+  // nsIDOMXULMultiSelectControlElement
+  clearSelection() {
+    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();
+  }
 
-            this.currentItem = newItem;
-          }
-        ]]>
-        </body>
-      </method>
+  // nsIDOMXULMultiSelectControlElement
+  getSelectedItem(aIndex) {
+    return aIndex < this.selectedItems.length ?
+      this.selectedItems[aIndex] : null;
+  }
 
-      <method name="_moveByOffsetFromUserEvent">
-        <parameter name="aOffset"/>
-        <parameter name="aEvent"/>
-        <body>
-        <![CDATA[
-          if (!aEvent.defaultPrevented) {
-            this._userSelecting = true;
-            this.moveByOffset(aOffset, !aEvent.ctrlKey, aEvent.shiftKey);
-            this._userSelecting = false;
-            aEvent.preventDefault();
-          }
-        ]]>
-        </body>
-      </method>
+  ensureIndexIsVisible(aIndex) {
+    return this.ensureElementIsVisible(this.getItemAtIndex(aIndex));
+  }
+
+  ensureElementIsVisible(aElement, aAlignToTop) {
+    if (!aElement) {
+      return;
+    }
 
-      <method name="_canUserSelect">
-        <parameter name="aItem"/>
-        <body>
-        <![CDATA[
-          var style = document.defaultView.getComputedStyle(aItem);
-          return style.display != "none" && style.visibility == "visible" &&
-                 style.MozUserInput != "none";
-        ]]>
-        </body>
-      </method>
+    // These calculations assume that there is no padding on the
+    // "richlistbox" element, although there might be a margin.
+    var targetRect = aElement.getBoundingClientRect();
+    var scrollRect = this.getBoundingClientRect();
+    var offset = targetRect.top - scrollRect.top;
+    if (!aAlignToTop && offset >= 0) {
+      // scrollRect.bottom wouldn't take a horizontal scroll bar into account
+      let scrollRectBottom = scrollRect.top + this.clientHeight;
+      offset = targetRect.bottom - scrollRectBottom;
+      if (offset <= 0) {
+        return;
+      }
+    }
+    this.scrollTop += offset;
+  }
 
-      <method name="_selectTimeoutHandler">
-        <parameter name="aMe"/>
-        <body>
-          aMe._fireOnSelect();
-          aMe._selectTimeout = null;
-        </body>
-      </method>
+  scrollToIndex(aIndex) {
+    var item = this.getItemAtIndex(aIndex);
+    if (item) {
+      this.ensureElementIsVisible(item, true);
+    }
+  }
 
-      <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;
+  getIndexOfFirstVisibleRow() {
+    var children = this.itemChildren;
 
-          if (aTimeout != -1) {
-            if (this._selectTimeout)
-              window.clearTimeout(this._selectTimeout);
-            this._selectTimeout =
-              window.setTimeout(this._selectTimeoutHandler, aTimeout, this);
-          }
-        ]]>
-        </body>
-      </method>
+    for (var ix = 0; ix < children.length; ix++) {
+      if (this._isItemVisible(children[ix])) {
+        return ix;
+      }
+    }
 
-      <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="_selectTimeout">null</field>
-      <field name="_currentItem">null</field>
-      <field name="_selectionStart">null</field>
+    return -1;
+  }
+
+  getRowCount() {
+    return this.itemChildren.length;
+  }
+
+  scrollOnePage(aDirection) {
+    var children = this.itemChildren;
 
-      <!-- For backwards-compatibility and for convenience.
-        Use ensureElementIsVisible instead -->
-      <method name="ensureSelectedElementIsVisible">
-        <body>
-          <![CDATA[
-            return this.ensureElementIsVisible(this.selectedItem);
-          ]]>
-        </body>
-      </method>
-    </implementation>
+    if (children.length == 0) {
+      return 0;
+    }
 
-    <handlers>
-      <handler event="keypress" keycode="VK_UP" modifiers="control shift any"
-               action="this._moveByOffsetFromUserEvent(-1, event);"
-               group="system"/>
+    // If nothing is selected, we just select the first element
+    // at the extreme we're moving away from
+    if (!this.currentItem) {
+      return aDirection == -1 ? children.length : 0;
+    }
 
-      <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._moveByOffsetFromUserEvent(-this.currentIndex, event);
-        ]]>
-      </handler>
+    // If the current item is visible, scroll by one page so that
+    // the new current item is at approximately the same position as
+    // the existing current item.
+    if (this._isItemVisible(this.currentItem)) {
+      this.scrollBy(0, this.clientHeight * aDirection);
+    }
 
-      <handler event="keypress" keycode="VK_END" modifiers="control shift any"
-               group="system">
-        <![CDATA[
-          this._moveByOffsetFromUserEvent(this.getRowCount() - this.currentIndex - 1, event);
-        ]]>
-      </handler>
-
-      <handler event="keypress" keycode="VK_PAGE_UP" modifiers="control shift any"
-               group="system">
-        <![CDATA[
-          this._moveByOffsetFromUserEvent(this.scrollOnePage(-1), event);
-        ]]>
-      </handler>
+    // Figure out, how many items fully fit into the view port
+    // (including the currently selected one), and determine
+    // the index of the first one lying (partially) outside
+    var height = this.clientHeight;
+    var startBorder = this.currentItem.boxObject.y;
+    if (aDirection == -1) {
+      startBorder += this.currentItem.clientHeight;
+    }
 
-      <handler event="keypress" keycode="VK_PAGE_DOWN" modifiers="control shift any"
-               group="system">
-        <![CDATA[
-          this._moveByOffsetFromUserEvent(this.scrollOnePage(1), event);
-        ]]>
-      </handler>
-
-      <handler event="keypress" key=" " modifiers="control" phase="target">
-        <![CDATA[
-          if (this.currentItem && this.selType == "multiple")
-            this.toggleItemSelection(this.currentItem);
-        ]]>
-      </handler>
+    var index = this.currentIndex;
+    for (var ix = index; 0 <= ix && ix < children.length; ix += aDirection) {
+      var boxObject = children[ix].boxObject;
+      if (boxObject.height == 0) {
+        continue; // hidden children have a y of 0
+      }
+      var endBorder = boxObject.y + (aDirection == -1 ? boxObject.height : 0);
+      if ((endBorder - startBorder) * aDirection > height) {
+        break; // we've reached the desired distance
+      }
+      index = ix;
+    }
 
-      <handler event="focus">
-        <![CDATA[
-          if (this.getRowCount() > 0) {
-            if (this.currentIndex == -1) {
-              this.currentIndex = this.getIndexOfFirstVisibleRow();
-              let currentItem = this.getItemAtIndex(this.currentIndex);
-              if (currentItem) {
-                this.selectItem(currentItem);
-              }
-            } else {
-              this.currentItem._fireEvent("DOMMenuItemActive");
-            }
-          }
-          this._lastKeyTime = 0;
-        ]]>
-      </handler>
+    return index != this.currentIndex ? index - this.currentIndex : aDirection;
+  }
+
+  _refreshSelection() {
+    // when this method is called, we know that either the currentItem
+    // and selectedItems we have are null (ctor) or a reference to an
+    // element no longer in the DOM (template).
 
-      <handler event="keypress" phase="target">
-        <![CDATA[
-          if (!event.charCode || event.altKey || event.ctrlKey || event.metaKey)
-            return;
-
-          if (event.timeStamp - this._lastKeyTime > 1000)
-            this._incrementalString = "";
+    // first look for the last-selected attribute
+    var state = this.getAttribute("last-selected");
+    if (state) {
+      var ids = state.split(" ");
 
-          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 suppressSelect = this._suppressOnSelect;
+      this._suppressOnSelect = true;
+      this.clearSelection();
+      for (let i = 1; i < ids.length; i++) {
+        var selectedItem = document.getElementById(ids[i]);
+        if (selectedItem) {
+          this.addItemToSelection(selectedItem);
+        }
+      }
 
-          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++;
+      var currentItem = document.getElementById(ids[0]);
+      if (!currentItem && this._currentIndex) {
+        currentItem = this.getItemAtIndex(Math.min(
+          this._currentIndex - 1, this.getRowCount()));
+      }
+      if (currentItem) {
+        this.currentItem = currentItem;
+        if (this.selType != "multiple" && this.selectedCount == 0) {
+          this.selectedItem = currentItem;
+        }
 
-          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) {
-            this.clearSelection();
-            this.currentItem = null;
-          }
-        ]]>
-      </handler>
+        if (this.clientHeight) {
+          this.ensureElementIsVisible(currentItem);
+        } else {
+          // XXX hack around a bug in ensureElementIsVisible as it will
+          // scroll beyond the last element, bug 493645.
+          this.ensureElementIsVisible(currentItem.previousElementSibling);
+        }
+      }
+      this._suppressOnSelect = suppressSelect;
+      // XXX actually it's just a refresh, but at least
+      // the Extensions manager expects this:
+      this._fireOnSelect();
+      return;
+    }
 
-      <handler event="MozSwipeGesture">
-        <![CDATA[
-          // Only handle swipe gestures up and down
-          switch (event.direction) {
-            case event.DIRECTION_DOWN:
-              this.scrollTop = this.scrollHeight;
-              break;
-            case event.DIRECTION_UP:
-              this.scrollTop = 0;
-              break;
-          }
-        ]]>
-      </handler>
-    </handlers>
-  </binding>
+    // try to restore the selected items according to their IDs
+    // (applies after a template rebuild, if last-selected was not set)
+    if (this.selectedItems) {
+      let itemIds = [];
+      for (let i = this.selectedCount - 1; i >= 0; i--) {
+        let selectedItem = this.selectedItems[i];
+        itemIds.push(selectedItem.id);
+        this.selectedItems.remove(selectedItem);
+      }
+      for (let i = 0; i < itemIds.length; i++) {
+        let selectedItem = document.getElementById(itemIds[i]);
+        if (selectedItem) {
+          this.selectedItems.append(selectedItem);
+        }
+      }
+    }
+    if (this.currentItem && this.currentItem.id) {
+      this.currentItem = document.getElementById(this.currentItem.id);
+    } else {
+      this.currentItem = null;
+    }
 
-  <binding id="richlistitem"
-           extends="chrome://global/content/bindings/general.xml#basetext">
-    <implementation implements="nsIDOMXULSelectControlItemElement">
-      <field name="selectedByMouseOver">false</field>
+    // if we have no previously current item or if the above check fails to
+    // find the previous nodes (which causes it to clear selection)
+    if (!this.currentItem && this.selectedCount == 0) {
+      this.currentIndex = this._currentIndex ? this._currentIndex - 1 : 0;
 
-      <destructor>
-        <![CDATA[
-          var control = this.control;
-          if (!control)
-            return;
-          // When we are destructed and we are current or selected, unselect ourselves
-          // so that richlistbox's selection doesn't point to something not in the DOM.
-          // We don't want to reset last-selected, so we set _suppressOnSelect.
-          if (this.selected) {
-            var suppressSelect = control._suppressOnSelect;
-            control._suppressOnSelect = true;
-            control.removeItemFromSelection(this);
-            control._suppressOnSelect = suppressSelect;
-          }
-          if (this.current)
-            control.currentItem = null;
-        ]]>
-      </destructor>
+      // cf. listbox constructor:
+      // select items according to their attributes
+      var children = this.itemChildren;
+      for (let i = 0; i < children.length; ++i) {
+        if (children[i].getAttribute("selected") == "true") {
+          this.selectedItems.append(children[i]);
+        }
+      }
+    }
 
-      <!-- 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"),
-                             label => label.value)
-                        .join(" ");
-          ]]>
-        </getter>
-      </property>
+    if (this.selType != "multiple" && this.selectedCount == 0) {
+      this.selectedItem = this.currentItem;
+    }
+  }
+
+  _isItemVisible(aItem) {
+    if (!aItem) {
+      return false;
+    }
 
-      <property name="searchLabel">
-        <getter>
-          <![CDATA[
-            return this.hasAttribute("searchlabel") ?
-                   this.getAttribute("searchlabel") : this.label;
-          ]]>
-        </getter>
-        <setter>
-          <![CDATA[
-            if (val !== null)
-              this.setAttribute("searchlabel", val);
-            else
-              // fall back to the label property (default value)
-              this.removeAttribute("searchlabel");
-            return val;
-          ]]>
-        </setter>
-      </property>
+    var y = this.scrollTop + this.boxObject.y;
+
+    // Partially visible items are also considered visible
+    return (aItem.boxObject.y + aItem.clientHeight > y) &&
+      (aItem.boxObject.y < y + this.clientHeight);
+  }
+
+  moveByOffset(aOffset, aIsSelecting, aIsSelectingRange) {
+    if ((aIsSelectingRange || !aIsSelecting) &&
+      this.selType != "multiple") {
+      return;
+    }
 
-      <!-- nsIDOMXULSelectControlItemElement -->
-      <property name="value" onget="return this.getAttribute('value');"
-                             onset="this.setAttribute('value', val); return val;"/>
+    var newIndex = this.currentIndex + aOffset;
+    if (newIndex < 0) {
+      newIndex = 0;
+    }
 
-      <!-- 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>
+    var numItems = this.getRowCount();
+    if (newIndex > numItems - 1) {
+      newIndex = numItems - 1;
+    }
 
-      <!-- nsIDOMXULSelectControlItemElement -->
-      <property name="control">
-        <getter><![CDATA[
-          var parent = this.parentNode;
-          while (parent) {
-            if (parent.localName == "richlistbox")
-              return parent;
-            parent = parent.parentNode;
-          }
-          return null;
-        ]]></getter>
-      </property>
+    var newItem = this.getItemAtIndex(newIndex);
+    // make sure that the item is actually visible/selectable
+    if (this._userSelecting && newItem && !this._canUserSelect(newItem)) {
+      newItem =
+      aOffset > 0 ? this.getNextItem(newItem, 1) || this.getPreviousItem(newItem, 1) :
+      this.getPreviousItem(newItem, 1) || this.getNextItem(newItem, 1);
+    }
+    if (newItem) {
+      this.ensureIndexIsVisible(this.getIndexOfItem(newItem));
+      if (aIsSelectingRange) {
+        this.selectItemRange(null, newItem);
+      } else if (aIsSelecting) {
+        this.selectItem(newItem);
+      }
 
-      <property name="current" onget="return this.getAttribute('current') == 'true';">
-        <setter><![CDATA[
-          if (val)
-            this.setAttribute("current", "true");
-          else
-            this.removeAttribute("current");
+      this.currentItem = newItem;
+    }
+  }
 
-          let control = this.control;
-          if (!control || !control.suppressMenuItemEvent) {
-            this._fireEvent(val ? "DOMMenuItemActive" : "DOMMenuItemInactive");
-          }
-
-          return val;
-        ]]></setter>
-      </property>
+  _moveByOffsetFromUserEvent(aOffset, aEvent) {
+    if (!aEvent.defaultPrevented) {
+      this._userSelecting = true;
+      this.moveByOffset(aOffset, !aEvent.ctrlKey, aEvent.shiftKey);
+      this._userSelecting = false;
+      aEvent.preventDefault();
+    }
+  }
 
-      <method name="_fireEvent">
-        <parameter name="name"/>
-        <body>
-        <![CDATA[
-          var event = document.createEvent("Events");
-          event.initEvent(name, true, true);
-          this.dispatchEvent(event);
-        ]]>
-        </body>
-      </method>
-    </implementation>
+  _canUserSelect(aItem) {
+    var style = document.defaultView.getComputedStyle(aItem);
+    return style.display != "none" && style.visibility == "visible" &&
+      style.MozUserInput != "none";
+  }
+
+  _selectTimeoutHandler(aMe) {
+    aMe._fireOnSelect();
+    aMe._selectTimeout = null;
+  }
+
+  timedSelect(aItem, aTimeout) {
+    var suppress = this._suppressOnSelect;
+    if (aTimeout != -1) {
+      this._suppressOnSelect = true;
+    }
+
+    this.selectItem(aItem);
 
-    <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>
+    this._suppressOnSelect = suppress;
+
+    if (aTimeout != -1) {
+      if (this._selectTimeout) {
+        window.clearTimeout(this._selectTimeout);
+      }
+      this._selectTimeout =
+        window.setTimeout(this._selectTimeoutHandler, aTimeout, this);
+    }
+  }
 
-      <!-- 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 */
+  /**
+   * For backwards-compatibility and for convenience.
+   * Use ensureElementIsVisible instead
+   */
+  ensureSelectedElementIsVisible() {
+    return this.ensureElementIsVisible(this.selectedItem);
+  }
+}
 
-            // 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>
+MozXULElement.implementCustomInterface(MozRichListBox, [
+  Ci.nsIDOMXULSelectControlElement,
+  Ci.nsIDOMXULMultiSelectControlElement,
+]);
+
+customElements.define("richlistbox", MozRichListBox);
+
+}
--- a/toolkit/content/widgets/richlistbox.xml
+++ b/toolkit/content/widgets/richlistbox.xml
@@ -7,934 +7,16 @@
 <!-- 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/general.xml#basecontrol">
-    <content allowevents="true" orient="vertical"/>
-
-    <implementation implements="nsIDOMXULMultiSelectControlElement">
-      <constructor>
-        <![CDATA[
-          this._refreshSelection();
-        ]]>
-      </constructor>
-
-      <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;
-
-            // remember the current item and all selected items with IDs
-            var state = this.currentItem ? this.currentItem.id : "";
-            if (this.selType == "multiple" && this.selectedCount) {
-              let getId = function getId(aItem) { return aItem.id; };
-              state += " " + [...this.selectedItems].filter(getId).map(getId).join(" ");
-            }
-            if (state)
-              this.setAttribute("last-selected", state);
-            else
-              this.removeAttribute("last-selected");
-
-            // preserve the index just in case no IDs are available
-            if (this.currentIndex > -1)
-              this._currentIndex = this.currentIndex + 1;
-
-            var event = document.createEvent("Events");
-            event.initEvent("select", true, true);
-            this.dispatchEvent(event);
-
-            // always call this (allows a commandupdater without controller)
-            document.commandDispatcher.updateCommands("richlistbox-select");
-          ]]>
-        </body>
-      </method>
-
-      <method name="getNextItem">
-        <parameter name="aStartItem"/>
-        <parameter name="aDelta"/>
-        <body>
-        <![CDATA[
-          while (aStartItem) {
-            aStartItem = aStartItem.nextSibling;
-            if (aStartItem && aStartItem.localName == "richlistitem" &&
-                (!this._userSelecting || this._canUserSelect(aStartItem))) {
-              --aDelta;
-              if (aDelta == 0)
-                return aStartItem;
-            }
-          }
-          return null;
-        ]]>
-        </body>
-      </method>
-
-      <method name="getPreviousItem">
-        <parameter name="aStartItem"/>
-        <parameter name="aDelta"/>
-        <body>
-        <![CDATA[
-          while (aStartItem) {
-            aStartItem = aStartItem.previousSibling;
-            if (aStartItem && aStartItem.localName == "richlistitem" &&
-                (!this._userSelecting || this._canUserSelect(aStartItem))) {
-              --aDelta;
-              if (aDelta == 0)
-                return aStartItem;
-            }
-          }
-          return null;
-        ]]>
-        </body>
-      </method>
-
-      <method name="appendItem">
-        <parameter name="aLabel"/>
-        <parameter name="aValue"/>
-        <body>
-          var item =
-            this.ownerDocument.createXULElement("richlistitem");
-          item.setAttribute("value", aValue);
-
-          var label = this.ownerDocument.createXULElement("label");
-          label.setAttribute("value", aLabel);
-          label.setAttribute("flex", "1");
-          label.setAttribute("crop", "end");
-          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.itemChildren.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.itemChildren.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.itemChildren[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>
-
-      <method name="ensureElementIsVisible">
-        <parameter name="aElement"/>
-        <parameter name="aAlignToTop"/>
-        <body>
-          <![CDATA[
-            if (!aElement) {
-              return;
-            }
-
-            // These calculations assume that there is no padding on the
-            // "richlistbox" element, although there might be a margin.
-            var targetRect = aElement.getBoundingClientRect();
-            var scrollRect = this.getBoundingClientRect();
-            var offset = targetRect.top - scrollRect.top;
-            if (!aAlignToTop && offset >= 0) {
-              // scrollRect.bottom wouldn't take a horizontal scroll bar into account
-              let scrollRectBottom = scrollRect.top + this.clientHeight;
-              offset = targetRect.bottom - scrollRectBottom;
-              if (offset <= 0)
-                return;
-            }
-            this.scrollTop += offset;
-          ]]>
-        </body>
-      </method>
-
-      <method name="scrollToIndex">
-        <parameter name="aIndex"/>
-        <body>
-          <![CDATA[
-            var item = this.getItemAtIndex(aIndex);
-            if (item) {
-              this.ensureElementIsVisible(item, true);
-            }
-          ]]>
-        </body>
-      </method>
-
-      <method name="getIndexOfFirstVisibleRow">
-        <body>
-          <![CDATA[
-            var children = this.itemChildren;
-
-            for (var ix = 0; ix < children.length; ix++)
-              if (this._isItemVisible(children[ix]))
-                return ix;
-
-            return -1;
-          ]]>
-        </body>
-      </method>
-
-      <method name="getRowCount">
-        <body>
-          <![CDATA[
-            return this.itemChildren.length;
-          ]]>
-        </body>
-      </method>
-
-      <method name="scrollOnePage">
-        <parameter name="aDirection"/> <!-- Must be -1 or 1 -->
-        <body>
-          <![CDATA[
-            var children = this.itemChildren;
-
-            if (children.length == 0)
-              return 0;
-
-            // If nothing is selected, we just select the first element
-            // at the extreme we're moving away from
-            if (!this.currentItem)
-              return aDirection == -1 ? children.length : 0;
-
-            // If the current item is visible, scroll by one page so that
-            // the new current item is at approximately the same position as
-            // the existing current item.
-            if (this._isItemVisible(this.currentItem))
-              this.scrollBy(0, this.clientHeight * aDirection);
-
-            // Figure out, how many items fully fit into the view port
-            // (including the currently selected one), and determine
-            // the index of the first one lying (partially) outside
-            var height = this.clientHeight;
-            var startBorder = this.currentItem.boxObject.y;
-            if (aDirection == -1)
-              startBorder += this.currentItem.clientHeight;
-
-            var index = this.currentIndex;
-            for (var ix = index; 0 <= ix && ix < children.length; ix += aDirection) {
-              var boxObject = children[ix].boxObject;
-              if (boxObject.height == 0)
-                continue; // hidden children have a y of 0
-              var endBorder = boxObject.y + (aDirection == -1 ? boxObject.height : 0);
-              if ((endBorder - startBorder) * aDirection > height)
-                break; // we've reached the desired distance
-              index = ix;
-            }
-
-            return index != this.currentIndex ? index - this.currentIndex : aDirection;
-          ]]>
-        </body>
-      </method>
-
-      <property name="itemChildren" readonly="true">
-        <getter>
-          <![CDATA[
-            let children = Array.from(this.children)
-                                .filter(node => node.localName == "richlistitem");
-            return children;
-          ]]>
-        </getter>
-      </property>
-
-      <method name="_refreshSelection">
-        <body>
-          <![CDATA[
-            // when this method is called, we know that either the currentItem
-            // and selectedItems we have are null (ctor) or a reference to an
-            // element no longer in the DOM (template).
-
-            // first look for the last-selected attribute
-            var state = this.getAttribute("last-selected");
-            if (state) {
-              var ids = state.split(" ");
-
-              var suppressSelect = this._suppressOnSelect;
-              this._suppressOnSelect = true;
-              this.clearSelection();
-              for (let i = 1; i < ids.length; i++) {
-                var selectedItem = document.getElementById(ids[i]);
-                if (selectedItem)
-                  this.addItemToSelection(selectedItem);
-              }
-
-              var currentItem = document.getElementById(ids[0]);
-              if (!currentItem && this._currentIndex)
-                currentItem = this.getItemAtIndex(Math.min(
-                  this._currentIndex - 1, this.getRowCount()));
-              if (currentItem) {
-                this.currentItem = currentItem;
-                if (this.selType != "multiple" && this.selectedCount == 0)
-                  this.selectedItem = currentItem;
-
-                if (this.clientHeight) {
-                  this.ensureElementIsVisible(currentItem);
-                } else {
-                  // XXX hack around a bug in ensureElementIsVisible as it will
-                  // scroll beyond the last element, bug 493645.
-                  this.ensureElementIsVisible(currentItem.previousElementSibling);
-                }
-              }
-              this._suppressOnSelect = suppressSelect;
-              // XXX actually it's just a refresh, but at least
-              // the Extensions manager expects this:
-              this._fireOnSelect();
-              return;
-            }
-
-            // try to restore the selected items according to their IDs
-            // (applies after a template rebuild, if last-selected was not set)
-            if (this.selectedItems) {
-              let itemIds = [];
-              for (let i = this.selectedCount - 1; i >= 0; i--) {
-                let selectedItem = this.selectedItems[i];
-                itemIds.push(selectedItem.id);
-                this.selectedItems.remove(selectedItem);
-              }
-              for (let i = 0; i < itemIds.length; i++) {
-                let selectedItem = document.getElementById(itemIds[i]);
-                if (selectedItem) {
-                  this.selectedItems.append(selectedItem);
-                }
-              }
-            }
-            if (this.currentItem && this.currentItem.id)
-              this.currentItem = document.getElementById(this.currentItem.id);
-            else
-              this.currentItem = null;
-
-            // if we have no previously current item or if the above check fails to
-            // find the previous nodes (which causes it to clear selection)
-            if (!this.currentItem && this.selectedCount == 0) {
-              this.currentIndex = this._currentIndex ? this._currentIndex - 1 : 0;
-
-              // cf. listbox constructor:
-              // select items according to their attributes
-              var children = this.itemChildren;
-              for (let i = 0; i < children.length; ++i) {
-                if (children[i].getAttribute("selected") == "true")
-                  this.selectedItems.append(children[i]);
-              }
-            }
-
-            if (this.selType != "multiple" && this.selectedCount == 0)
-              this.selectedItem = this.currentItem;
-          ]]>
-        </body>
-      </method>
-
-      <method name="_isItemVisible">
-        <parameter name="aItem"/>
-        <body>
-          <![CDATA[
-            if (!aItem)
-              return false;
-
-            var y = this.scrollTop + this.boxObject.y;
-
-            // Partially visible items are also considered visible
-            return (aItem.boxObject.y + aItem.clientHeight > y) &&
-                   (aItem.boxObject.y < y + this.clientHeight);
-          ]]>
-        </body>
-      </method>
-
-      <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="moveByOffset">
-        <parameter name="aOffset"/>
-        <parameter name="aIsSelecting"/>
-        <parameter name="aIsSelectingRange"/>
-        <body>
-        <![CDATA[
-          if ((aIsSelectingRange || !aIsSelecting) &&
-              this.selType != "multiple")
-            return;
-
-          var newIndex = this.currentIndex + aOffset;
-          if (newIndex < 0)
-            newIndex = 0;
-
-          var numItems = this.getRowCount();
-          if (newIndex > numItems - 1)
-            newIndex = numItems - 1;
-
-          var newItem = this.getItemAtIndex(newIndex);
-          // make sure that the item is actually visible/selectable
-          if (this._userSelecting && newItem && !this._canUserSelect(newItem))
-            newItem =
-              aOffset > 0 ? this.getNextItem(newItem, 1) || this.getPreviousItem(newItem, 1) :
-                            this.getPreviousItem(newItem, 1) || this.getNextItem(newItem, 1);
-          if (newItem) {
-            this.ensureIndexIsVisible(this.getIndexOfItem(newItem));
-            if (aIsSelectingRange)
-              this.selectItemRange(null, newItem);
-            else if (aIsSelecting)
-              this.selectItem(newItem);
-
-            this.currentItem = newItem;
-          }
-        ]]>
-        </body>
-      </method>
-
-      <method name="_moveByOffsetFromUserEvent">
-        <parameter name="aOffset"/>
-        <parameter name="aEvent"/>
-        <body>
-        <![CDATA[
-          if (!aEvent.defaultPrevented) {
-            this._userSelecting = true;
-            this.moveByOffset(aOffset, !aEvent.ctrlKey, aEvent.shiftKey);
-            this._userSelecting = 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" &&
-                 style.MozUserInput != "none";
-        ]]>
-        </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="_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._moveByOffsetFromUserEvent(-this.currentIndex, event);
-        ]]>
-      </handler>
-
-      <handler event="keypress" keycode="VK_END" modifiers="control shift any"
-               group="system">
-        <![CDATA[
-          this._moveByOffsetFromUserEvent(this.getRowCount() - this.currentIndex - 1, event);
-        ]]>
-      </handler>
-
-      <handler event="keypress" keycode="VK_PAGE_UP" modifiers="control shift any"
-               group="system">
-        <![CDATA[
-          this._moveByOffsetFromUserEvent(this.scrollOnePage(-1), event);
-        ]]>
-      </handler>
-
-      <handler event="keypress" keycode="VK_PAGE_DOWN" modifiers="control shift any"
-               group="system">
-        <![CDATA[
-          this._moveByOffsetFromUserEvent(this.scrollOnePage(1), event);
-        ]]>
-      </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();
-              let currentItem = this.getItemAtIndex(this.currentIndex);
-              if (currentItem) {
-                this.selectItem(currentItem);
-              }
-            } else {
-              this.currentItem._fireEvent("DOMMenuItemActive");
-            }
-          }
-          this._lastKeyTime = 0;
-        ]]>
-      </handler>
-
-      <handler event="keypress" phase="target">
-        <![CDATA[
-          if (!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) {
-            this.clearSelection();
-            this.currentItem = null;
-          }
-        ]]>
-      </handler>
-
-      <handler event="MozSwipeGesture">
-        <![CDATA[
-          // Only handle swipe gestures up and down
-          switch (event.direction) {
-            case event.DIRECTION_DOWN:
-              this.scrollTop = this.scrollHeight;
-              break;
-            case event.DIRECTION_UP:
-              this.scrollTop = 0;
-              break;
-          }
-        ]]>
-      </handler>
-    </handlers>
-  </binding>
-
   <binding id="richlistitem"
            extends="chrome://global/content/bindings/general.xml#basetext">
     <implementation implements="nsIDOMXULSelectControlItemElement">
       <field name="selectedByMouseOver">false</field>
 
       <destructor>
         <![CDATA[
           var control = this.control;
--- a/toolkit/content/xul.css
+++ b/toolkit/content/xul.css
@@ -717,17 +717,16 @@ wizardpage {
 
 .wizard-buttons {
   -moz-binding: url("chrome://global/content/bindings/wizard.xml#wizard-buttons");
 }
 
 /********** Rich Listbox ********/
 
 richlistbox {
-  -moz-binding: url('chrome://global/content/bindings/richlistbox.xml#richlistbox');
   -moz-user-focus: normal;
   -moz-box-orient: vertical;
 }
 
 richlistitem {
   -moz-binding: url('chrome://global/content/bindings/richlistbox.xml#richlistitem');
 }