toolkit/content/widgets/richlistbox.xml
author Mark Striemer <mstriemer@mozilla.com>
Sun, 14 Oct 2018 19:20:16 +0000
changeset 441189 0a45a5634cf18f31b1ab848f4c71b34cabe80ec5
parent 441184 45299a26e20a51f8b616a97024f6204d4210ecd1
child 445841 71321c9675989891fd3e2f142acee1ef62566637
permissions -rw-r--r--
Bug 1490366 - Part 3: Select the first item in richlistbox on focus r=dao Differential Revision: https://phabricator.services.mozilla.com/D8282

<?xml version="1.0"?>

<!-- This Source Code Form is subject to the terms of the Mozilla Public
   - License, v. 2.0. If a copy of the MPL was not distributed with this
   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->

<!-- This file relies on these specific Chrome/XBL globals -->
<!-- globals ChromeNodeList -->

<bindings id="richlistboxBindings"
          xmlns="http://www.mozilla.org/xbl"
          xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
          xmlns:xbl="http://www.mozilla.org/xbl">

  <binding id="richlistbox"
           extends="chrome://global/content/bindings/general.xml#basecontrol">
    <content>
      <children includes="listheader"/>
      <xul:scrollbox allowevents="true" orient="vertical" anonid="main-box"
                     flex="1" style="overflow: auto;" xbl:inherits="dir,pack">
        <children/>
      </xul:scrollbox>
    </content>

    <implementation implements="nsIDOMXULMultiSelectControlElement">
      <field name="_scrollbox">
        document.getAnonymousElementByAttribute(this, "anonid", "main-box");
      </field>
      <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>

      <!-- We override base-listbox here because those methods don't take dir
           into account on listbox (which doesn't support dir yet) -->
      <method name="getNextItem">
        <parameter name="aStartItem"/>
        <parameter name="aDelta"/>
        <body>
        <![CDATA[
          var prop = this.dir == "reverse" && this._mayReverse ?
                                                "previousSibling" :
                                                "nextSibling";
          while (aStartItem) {
            aStartItem = aStartItem[prop];
            if (aStartItem && aStartItem instanceof
                Ci.nsIDOMXULSelectControlItemElement &&
                (!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[
          var prop = this.dir == "reverse" && this._mayReverse ?
                                                "nextSibling" :
                                                "previousSibling";
          while (aStartItem) {
            aStartItem = aStartItem[prop];
            if (aStartItem && aStartItem instanceof
                Ci.nsIDOMXULSelectControlItemElement &&
                (!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.children.length"/>

      <!-- nsIDOMXULSelectControlElement -->
      <method name="getIndexOfItem">
        <parameter name="aItem"/>
        <body>
          <![CDATA[
            // don't search the children, if we're looking for none of them
            if (aItem == null)
              return -1;
            if (this._selecting && this._selecting.item == aItem)
              return this._selecting.index;
            return this.children.indexOf(aItem);
          ]]>
        </body>
      </method>

      <!-- nsIDOMXULSelectControlElement -->
      <method name="getItemAtIndex">
        <parameter name="aIndex"/>
        <body>
          <![CDATA[
            if (this._selecting && this._selecting.index == aIndex)
              return this._selecting.item;
            return this.children[aIndex] || null;
          ]]>
        </body>
      </method>

      <!-- nsIDOMXULMultiSelectControlElement -->
      <property name="selType"
                onget="return this.getAttribute('seltype');"
                onset="this.setAttribute('seltype', val); return val;"/>

      <!-- nsIDOMXULMultiSelectControlElement -->
      <property name="currentItem" onget="return this._currentItem;">
        <setter>
          if (this._currentItem == val)
            return val;

          if (this._currentItem)
            this._currentItem.current = false;
          this._currentItem = val;

          if (val)
            val.current = true;

          return val;
        </setter>
      </property>

      <!-- nsIDOMXULMultiSelectControlElement -->
      <property name="currentIndex">
        <getter>
          return this.currentItem ? this.getIndexOfItem(this.currentItem) : -1;
        </getter>
        <setter>
        <![CDATA[
          if (val >= 0)
            this.currentItem = this.getItemAtIndex(val);
          else
            this.currentItem = null;
        ]]>
        </setter>
      </property>

      <!-- nsIDOMXULMultiSelectControlElement -->
      <field name="selectedItems">new ChromeNodeList()</field>

      <!-- nsIDOMXULMultiSelectControlElement -->
      <method name="addItemToSelection">
        <parameter name="aItem"/>
        <body>
        <![CDATA[
          if (this.selType != "multiple" && this.selectedCount)
            return;

          if (aItem.selected)
            return;

          this.selectedItems.append(aItem);
          aItem.selected = true;

          this._fireOnSelect();
        ]]>
        </body>
      </method>

      <!-- nsIDOMXULMultiSelectControlElement -->
      <method name="removeItemFromSelection">
        <parameter name="aItem"/>
        <body>
        <![CDATA[
          if (!aItem.selected)
            return;

          this.selectedItems.remove(aItem);
          aItem.selected = false;
          this._fireOnSelect();
        ]]>
        </body>
      </method>

      <!-- nsIDOMXULMultiSelectControlElement -->
      <method name="toggleItemSelection">
        <parameter name="aItem"/>
        <body>
        <![CDATA[
          if (aItem.selected)
            this.removeItemFromSelection(aItem);
          else
            this.addItemToSelection(aItem);
        ]]>
        </body>
      </method>

      <!-- nsIDOMXULMultiSelectControlElement -->
      <method name="selectItem">
        <parameter name="aItem"/>
        <body>
        <![CDATA[
          if (!aItem)
            return;

          if (this.selectedItems.length == 1 && this.selectedItems[0] == aItem)
            return;

          this._selectionStart = null;

          var suppress = this._suppressOnSelect;
          this._suppressOnSelect = true;

          this.clearSelection();
          this.addItemToSelection(aItem);
          this.currentItem = aItem;

          this._suppressOnSelect = suppress;
          this._fireOnSelect();
        ]]>
        </body>
      </method>

      <!-- nsIDOMXULMultiSelectControlElement -->
      <method name="selectItemRange">
        <parameter name="aStartItem"/>
        <parameter name="aEndItem"/>
        <body>
        <![CDATA[
          if (this.selType != "multiple")
            return;

          if (!aStartItem)
            aStartItem = this._selectionStart ?
              this._selectionStart : this.currentItem;

          if (!aStartItem)
            aStartItem = aEndItem;

          var suppressSelect = this._suppressOnSelect;
          this._suppressOnSelect = true;

          this._selectionStart = aStartItem;

          var currentItem;
          var startIndex = this.getIndexOfItem(aStartItem);
          var endIndex = this.getIndexOfItem(aEndItem);
          if (endIndex < startIndex) {
            currentItem = aEndItem;
            aEndItem = aStartItem;
            aStartItem = currentItem;
          } else {
            currentItem = aStartItem;
          }

          while (currentItem) {
            this.addItemToSelection(currentItem);
            if (currentItem == aEndItem) {
              currentItem = this.getNextItem(currentItem, 1);
              break;
            }
            currentItem = this.getNextItem(currentItem, 1);
          }

          // Clear around new selection
          // Don't use clearSelection() because it causes a lot of noise
          // with respect to selection removed notifications used by the
          // accessibility API support.
          var userSelecting = this._userSelecting;
          this._userSelecting = false; // that's US automatically unselecting
          for (; currentItem; currentItem = this.getNextItem(currentItem, 1))
            this.removeItemFromSelection(currentItem);

          for (currentItem = this.getItemAtIndex(0); currentItem != aStartItem;
               currentItem = this.getNextItem(currentItem, 1))
            this.removeItemFromSelection(currentItem);
          this._userSelecting = userSelecting;

          this._suppressOnSelect = suppressSelect;

          this._fireOnSelect();
        ]]>
        </body>
      </method>

      <!-- nsIDOMXULMultiSelectControlElement -->
      <method name="selectAll">
        <body>
          this._selectionStart = null;

          var suppress = this._suppressOnSelect;
          this._suppressOnSelect = true;

          var item = this.getItemAtIndex(0);
          while (item) {
            this.addItemToSelection(item);
            item = this.getNextItem(item, 1);
          }

          this._suppressOnSelect = suppress;
          this._fireOnSelect();
        </body>
      </method>

      <!-- nsIDOMXULMultiSelectControlElement -->
      <method name="invertSelection">
        <body>
          this._selectionStart = null;

          var suppress = this._suppressOnSelect;
          this._suppressOnSelect = true;

          var item = this.getItemAtIndex(0);
          while (item) {
            if (item.selected)
              this.removeItemFromSelection(item);
            else
              this.addItemToSelection(item);
            item = this.getNextItem(item, 1);
          }

          this._suppressOnSelect = suppress;
          this._fireOnSelect();
        </body>
      </method>

      <!-- nsIDOMXULMultiSelectControlElement -->
      <method name="clearSelection">
        <body>
        <![CDATA[
          if (this.selectedItems) {
            while (this.selectedItems.length > 0) {
              let item = this.selectedItems[0];
              item.selected = false;
              this.selectedItems.remove(item);
            }
          }

          this._selectionStart = null;
          this._fireOnSelect();
        ]]>
        </body>
      </method>

      <!-- nsIDOMXULMultiSelectControlElement -->
      <property name="selectedCount" readonly="true"
                onget="return this.selectedItems.length;"/>

      <!-- nsIDOMXULMultiSelectControlElement -->
      <method name="getSelectedItem">
        <parameter name="aIndex"/>
        <body>
        <![CDATA[
          return aIndex < this.selectedItems.length ?
            this.selectedItems[aIndex] : null;
        ]]>
        </body>
      </method>

      <method name="ensureIndexIsVisible">
        <parameter name="aIndex"/>
        <body>
          <![CDATA[
            return this.ensureElementIsVisible(this.getItemAtIndex(aIndex));
          ]]>
        </body>
      </method>

      <method name="ensureElementIsVisible">
        <parameter name="aElement"/>
        <body>
          <![CDATA[
            if (!aElement)
              return;
            var targetRect = aElement.getBoundingClientRect();
            var scrollRect = this._scrollbox.getBoundingClientRect();
            var offset = targetRect.top - scrollRect.top;
            if (offset >= 0) {
              // scrollRect.bottom wouldn't take a horizontal scroll bar into account
              let scrollRectBottom = scrollRect.top + this._scrollbox.clientHeight;
              offset = targetRect.bottom - scrollRectBottom;
              if (offset <= 0)
                return;
            }
            this._scrollbox.scrollTop += offset;
          ]]>
        </body>
      </method>

      <method name="scrollToIndex">
        <parameter name="aIndex"/>
        <body>
          <![CDATA[
            var item = this.getItemAtIndex(aIndex);
            if (item)
              this._scrollbox.scrollToElement(item);
          ]]>
        </body>
      </method>

      <method name="getIndexOfFirstVisibleRow">
        <body>
          <![CDATA[
            var children = this.children;

            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.children.length;
          ]]>
        </body>
      </method>

      <method name="scrollOnePage">
        <parameter name="aDirection"/> <!-- Must be -1 or 1 -->
        <body>
          <![CDATA[
            var children = this.children;

            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._scrollbox.scrollBy(0, this._scrollbox.boxObject.height * 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._scrollbox.boxObject.height;
            var startBorder = this.currentItem.boxObject.y;
            if (aDirection == -1)
              startBorder += this.currentItem.boxObject.height;

            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="children" readonly="true">
        <getter>
          <![CDATA[
            let iface = Ci.nsIDOMXULSelectControlItemElement;
            let children = Array.from(this.childNodes)
                                .filter(node => node.nodeType == 1 && node instanceof iface);
            if (this.dir == "reverse" && this._mayReverse) {
              children.reverse();
            }
            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._scrollbox.boxObject.height) {
                  this.ensureElementIsVisible(currentItem);
                } else {
                  // XXX hack around a bug in ensureElementIsVisible as it will
                  // scroll beyond the last element, bug 493645.
                  var previousElement = this.dir == "reverse" ? currentItem.nextElementSibling :
                                                                currentItem.previousElementSibling;
                  this.ensureElementIsVisible(previousElement);
                }
              }
              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.children;
              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._scrollbox.scrollTop + this._scrollbox.boxObject.y;

            // Partially visible items are also considered visible
            return (aItem.boxObject.y + aItem.boxObject.height > y) &&
                   (aItem.boxObject.y < y + this._scrollbox.boxObject.height);
          ]]>
        </body>
      </method>

      <property name="disableKeyNavigation"
                onget="return this.hasAttribute('disableKeyNavigation');">
        <setter>
          if (val)
            this.setAttribute("disableKeyNavigation", "true");
          else
            this.removeAttribute("disableKeyNavigation");
          return val;
        </setter>
      </property>

      <property name="suppressOnSelect"
                onget="return this.getAttribute('suppressonselect') == 'true';"
                onset="this.setAttribute('suppressonselect', val);"/>

      <property name="_selectDelay"
                onset="this.setAttribute('_selectDelay', val);"
                onget="return this.getAttribute('_selectDelay') || 50;"/>

      <method name="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._mayReverse = true;
            this.moveByOffset(aOffset, !aEvent.ctrlKey, aEvent.shiftKey);
            this._userSelecting = false;
            this._mayReverse = false;
            aEvent.preventDefault();
          }
        ]]>
        </body>
      </method>

      <method name="_canUserSelect">
        <parameter name="aItem"/>
        <body>
        <![CDATA[
          var style = document.defaultView.getComputedStyle(aItem);
          return style.display != "none" && style.visibility == "visible";
        ]]>
        </body>
      </method>

      <method name="_selectTimeoutHandler">
        <parameter name="aMe"/>
        <body>
          aMe._fireOnSelect();
          aMe._selectTimeout = null;
        </body>
      </method>

      <method name="timedSelect">
        <parameter name="aItem"/>
        <parameter name="aTimeout"/>
        <body>
        <![CDATA[
          var suppress = this._suppressOnSelect;
          if (aTimeout != -1)
            this._suppressOnSelect = true;

          this.selectItem(aItem);

          this._suppressOnSelect = suppress;

          if (aTimeout != -1) {
            if (this._selectTimeout)
              window.clearTimeout(this._selectTimeout);
            this._selectTimeout =
              window.setTimeout(this._selectTimeoutHandler, aTimeout, this);
          }
        ]]>
        </body>
      </method>

      <field name="_currentIndex">null</field>
      <field name="_lastKeyTime">0</field>
      <field name="_incrementalString">""</field>
      <field name="_suppressOnSelect">false</field>
      <field name="_userSelecting">false</field>
      <field name="_mayReverse">false</field>
      <field name="_selectTimeout">null</field>
      <field name="_currentItem">null</field>
      <field name="_selectionStart">null</field>

      <!-- For backwards-compatibility and for convenience.
        Use ensureElementIsVisible instead -->
      <method name="ensureSelectedElementIsVisible">
        <body>
          <![CDATA[
            return this.ensureElementIsVisible(this.selectedItem);
          ]]>
        </body>
      </method>
    </implementation>

    <handlers>
      <handler event="keypress" keycode="VK_UP" modifiers="control shift any"
               action="this._moveByOffsetFromUserEvent(-1, event);"
               group="system"/>

      <handler event="keypress" keycode="VK_DOWN" modifiers="control shift any"
               action="this._moveByOffsetFromUserEvent(1, event);"
               group="system"/>

      <handler event="keypress" keycode="VK_HOME" modifiers="control shift any"
               group="system">
        <![CDATA[
          this._mayReverse = true;
          this._moveByOffsetFromUserEvent(-this.currentIndex, event);
          this._mayReverse = false;
        ]]>
      </handler>

      <handler event="keypress" keycode="VK_END" modifiers="control shift any"
               group="system">
        <![CDATA[
          this._mayReverse = true;
          this._moveByOffsetFromUserEvent(this.getRowCount() - this.currentIndex - 1, event);
          this._mayReverse = false;
        ]]>
      </handler>

      <handler event="keypress" keycode="VK_PAGE_UP" modifiers="control shift any"
               group="system">
        <![CDATA[
          this._mayReverse = true;
          this._moveByOffsetFromUserEvent(this.scrollOnePage(-1), event);
          this._mayReverse = false;
        ]]>
      </handler>

      <handler event="keypress" keycode="VK_PAGE_DOWN" modifiers="control shift any"
               group="system">
        <![CDATA[
          this._mayReverse = true;
          this._moveByOffsetFromUserEvent(this.scrollOnePage(1), event);
          this._mayReverse = false;
        ]]>
      </handler>

      <handler event="keypress" key=" " modifiers="control" phase="target">
        <![CDATA[
          if (this.currentItem && this.selType == "multiple")
            this.toggleItemSelection(this.currentItem);
        ]]>
      </handler>

      <handler event="focus">
        <![CDATA[
          if (this.getRowCount() > 0) {
            if (this.currentIndex == -1) {
              this.currentIndex = this.getIndexOfFirstVisibleRow();
              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 (this.disableKeyNavigation || !event.charCode ||
              event.altKey || event.ctrlKey || event.metaKey)
            return;

          if (event.timeStamp - this._lastKeyTime > 1000)
            this._incrementalString = "";

          var key = String.fromCharCode(event.charCode).toLowerCase();
          this._incrementalString += key;
          this._lastKeyTime = event.timeStamp;

          // If all letters in the incremental string are the same, just
          // try to match the first one
          var incrementalString = /^(.)\1+$/.test(this._incrementalString) ?
                                  RegExp.$1 : this._incrementalString;
          var length = incrementalString.length;

          var rowCount = this.getRowCount();
          var l = this.selectedItems.length;
          var start = l > 0 ? this.getIndexOfItem(this.selectedItems[l - 1]) : -1;
          // start from the first element if none was selected or from the one
          // following the selected one if it's a new or a repeated-letter search
          if (start == -1 || length == 1)
            start++;

          for (var i = 0; i < rowCount; i++) {
            var k = (start + i) % rowCount;
            var listitem = this.getItemAtIndex(k);
            if (!this._canUserSelect(listitem))
              continue;
            // allow richlistitems to specify the string being searched for
            var searchText = "searchLabel" in listitem ? listitem.searchLabel :
                             listitem.getAttribute("label"); // (see also bug 250123)
            searchText = searchText.substring(0, length).toLowerCase();
            if (searchText == incrementalString) {
              this.ensureIndexIsVisible(k);
              this.timedSelect(listitem, this._selectDelay);
              break;
            }
          }
        ]]>
      </handler>

      <handler event="click">
        <![CDATA[
          // clicking into nothing should unselect
          if (event.originalTarget == this._scrollbox) {
            this.clearSelection();
            this.currentItem = null;
          }
        ]]>
      </handler>

      <handler event="MozSwipeGesture">
        <![CDATA[
          // Only handle swipe gestures up and down
          switch (event.direction) {
            case event.DIRECTION_DOWN:
              this._scrollbox.scrollTop = this._scrollbox.scrollHeight;
              break;
            case event.DIRECTION_UP:
              this._scrollbox.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;
          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>

      <!-- 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>

      <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>

      <!-- nsIDOMXULSelectControlItemElement -->
      <property name="value" onget="return this.getAttribute('value');"
                             onset="this.setAttribute('value', val); return val;"/>

      <!-- nsIDOMXULSelectControlItemElement -->
      <property name="selected" onget="return this.getAttribute('selected') == 'true';">
        <setter><![CDATA[
          if (val)
            this.setAttribute("selected", "true");
          else
            this.removeAttribute("selected");

          return val;
        ]]></setter>
      </property>

      <!-- nsIDOMXULSelectControlItemElement -->
      <property name="control">
        <getter><![CDATA[
          var parent = this.parentNode;
          while (parent) {
            if (parent instanceof Ci.nsIDOMXULSelectControlElement)
              return parent;
            parent = parent.parentNode;
          }
          return null;
        ]]></getter>
      </property>

      <property name="current" onget="return this.getAttribute('current') == 'true';">
        <setter><![CDATA[
          if (val)
            this.setAttribute("current", "true");
          else
            this.removeAttribute("current");

          let control = this.control;
          if (!control || !control.suppressMenuItemEvent) {
            this._fireEvent(val ? "DOMMenuItemActive" : "DOMMenuItemInactive");
          }

          return val;
        ]]></setter>
      </property>

      <method name="_fireEvent">
        <parameter name="name"/>
        <body>
        <![CDATA[
          var event = document.createEvent("Events");
          event.initEvent(name, true, true);
          this.dispatchEvent(event);
        ]]>
        </body>
      </method>
    </implementation>

    <handlers>
      <!-- If there is no modifier key, we select on mousedown, not
           click, so that drags work correctly. -->
      <handler event="mousedown">
        <![CDATA[
          var control = this.control;
          if (!control || control.disabled)
            return;
          if ((!event.ctrlKey || (/Mac/.test(navigator.platform) && event.button == 2)) &&
              !event.shiftKey && !event.metaKey) {
            if (!this.selected) {
              control.selectItem(this);
            }
            control.currentItem = this;
          }
        ]]>
      </handler>

      <!-- On a click (up+down on the same item), deselect everything
           except this item. -->
      <handler event="click" button="0">
        <![CDATA[
          var control = this.control;
          if (!control || control.disabled)
            return;
          control._userSelecting = true;
          if (control.selType != "multiple") {
            control.selectItem(this);
          } else if (event.ctrlKey || event.metaKey) {
            control.toggleItemSelection(this);
            control.currentItem = this;
          } else if (event.shiftKey) {
            control.selectItemRange(null, this);
            control.currentItem = this;
          } else {
            /* We want to deselect all the selected items except what was
              clicked, UNLESS it was a right-click.  We have to do this
              in click rather than mousedown so that you can drag a
              selected group of items */

            // use selectItemRange instead of selectItem, because this
            // doesn't de- and reselect this item if it is selected
            control.selectItemRange(this, this);
          }
          control._userSelecting = false;
        ]]>
      </handler>
    </handlers>
  </binding>
</bindings>