Bug 1517040 - Port bug 1472558: fork richlistbox.xml until we have our code converted. rs=bustage-fix,jorgk
authorArshad Khan <arshdkhn1@gmail.com>
Fri, 04 Jan 2019 21:32:40 +0530
changeset 33260 c39f35e517ff
parent 33259 c6f13700602b
child 33261 fc3f22843429
push id2368
push userclokep@gmail.com
push dateMon, 28 Jan 2019 21:12:50 +0000
treeherdercomm-beta@56d23c07d815 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbustage-fix, jorgk
bugs1517040, 1472558
Bug 1517040 - Port bug 1472558: fork richlistbox.xml until we have our code converted. rs=bustage-fix,jorgk
calendar/base/content/dialogs/calendar-event-dialog-attendees.xml
calendar/base/content/dialogs/calendar-event-dialog-freebusy.xml
calendar/base/content/dialogs/calendar-invitations-list.xml
calendar/base/content/widgets/calendar-subscriptions-list.xml
common/bindings/richlistbox.xml
mail/base/content/aboutAddonsExtra.js
mail/base/content/mailWidgets.xml
mail/base/jar.mn
--- a/calendar/base/content/dialogs/calendar-event-dialog-attendees.xml
+++ b/calendar/base/content/dialogs/calendar-event-dialog-attendees.xml
@@ -7,17 +7,17 @@
   <!ENTITY % dtd1 SYSTEM "chrome://calendar/locale/global.dtd" > %dtd1;
   <!ENTITY % dtd2 SYSTEM "chrome://calendar/locale/calendar.dtd" > %dtd2;
   <!ENTITY % dtd3 SYSTEM "chrome://calendar/locale/calendar-event-dialog.dtd"> %dtd3;
 ]>
 
 <bindings xmlns="http://www.mozilla.org/xbl"
           xmlns:xbl="http://www.mozilla.org/xbl"
           xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
-  <binding id="attendees-list" extends="chrome://global/content/bindings/richlistbox.xml#richlistbox">
+  <binding id="attendees-list" extends="chrome://messenger/content/richlistbox.xml#xbl-richlistbox">
     <implementation>
       <field name="mMaxAttendees">0</field>
       <field name="mContentHeight">0</field>
       <field name="mRowHeight">0</field>
       <field name="mNumColumns">3</field>
       <field name="mIsOffline">0</field>
       <field name="mIsReadOnly">false</field>
       <field name="mIsInvitation">false</field>
--- a/calendar/base/content/dialogs/calendar-event-dialog-freebusy.xml
+++ b/calendar/base/content/dialogs/calendar-event-dialog-freebusy.xml
@@ -210,17 +210,17 @@
             }
 
             return val;
         ]]></setter>
       </property>
     </implementation>
   </binding>
 
-  <binding id="freebusy-timebar" extends="chrome://global/content/bindings/richlistbox.xml#richlistbox">
+  <binding id="freebusy-timebar" extends="chrome://messenger/content/richlistbox.xml#xbl-richlistbox">
     <implementation>
       <field name="mNumDays">0</field>
       <field name="mRange">0</field>
       <field name="mStartDate">null</field>
       <field name="mEndDate">null</field>
       <field name="mDayOffset">0</field>
       <field name="mScrollOffset">0</field>
       <field name="mStartHour">0</field>
@@ -891,17 +891,17 @@
                 this.mStartHour = Preferences.get("calendar.view.daystarthour", 8);
                 this.mEndHour = Preferences.get("calendar.view.dayendhour", 19);
             }
         ]]></body>
       </method>
     </implementation>
   </binding>
 
-  <binding id="freebusy-grid" extends="chrome://global/content/bindings/richlistbox.xml#richlistbox">
+  <binding id="freebusy-grid" extends="chrome://messenger/content/richlistbox.xml#xbl-richlistbox">
     <implementation>
       <field name="mContentHeight">0</field>
       <field name="mRowHeight">0</field>
       <field name="mMaxFreeBusy">0</field>
       <field name="mPendingRequests">null</field>
       <field name="mStartDate">null</field>
       <field name="mEndDate">null</field>
       <field name="mScrollOffset">0</field>
--- a/calendar/base/content/dialogs/calendar-invitations-list.xml
+++ b/calendar/base/content/dialogs/calendar-invitations-list.xml
@@ -7,17 +7,17 @@
   <!ENTITY % dtd1 SYSTEM "chrome://calendar/locale/calendar-invitations-dialog.dtd" >  %dtd1;
 ]>
 
 <bindings id="calendar-invitations-list-bindings"
           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="calendar-invitations-richlistbox"
-           extends="chrome://global/content/bindings/richlistbox.xml#richlistbox"
+           extends="chrome://messenger/content/richlistbox.xml#xbl-richlistbox"
            xbl:inherits="flex">
     <implementation>
       <!-- methods -->
       <method name="addCalendarItem">
         <parameter name="aItem"/>
         <body><![CDATA[
             let newNode = createXULElement("calendar-invitations-richlistitem");
             this.appendChild(newNode);
--- a/calendar/base/content/widgets/calendar-subscriptions-list.xml
+++ b/calendar/base/content/widgets/calendar-subscriptions-list.xml
@@ -6,17 +6,17 @@
 
 <bindings id="calendar-subscriptions-list-bindings"
           xmlns="http://www.mozilla.org/xbl"
           xmlns:html="http://www.w3.org/1999/xhtml"
           xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
           xmlns:xbl="http://www.mozilla.org/xbl">
 
   <binding id="calendar-subscriptions-richlistbox"
-           extends="chrome://global/content/bindings/richlistbox.xml#richlistbox"
+           extends="chrome://messenger/content/richlistbox.xml#xbl-richlistbox"
            xbl:inherits="flex">
 
     <implementation>
       <method name="addCalendar">
         <parameter name="aCalendar"/>
         <parameter name="bSubscribed"/>
         <body><![CDATA[
             let newNode = createXULElement("calendar-subscriptions-richlistitem");
new file mode 100644
--- /dev/null
+++ b/common/bindings/richlistbox.xml
@@ -0,0 +1,958 @@
+<?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="xbl-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(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.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[
+          var prop = this.dir == "reverse" && this._mayReverse ?
+                                                "nextSibling" :
+                                                "previousSibling";
+          while (aStartItem) {
+            aStartItem = aStartItem[prop];
+            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"/>
+        <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.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._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="itemChildren" readonly="true">
+        <getter>
+          <![CDATA[
+            let children = Array.from(this.children)
+                                .filter(node => node.localName == "richlistitem");
+            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.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._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="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 (!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>
+</bindings>
--- a/mail/base/content/aboutAddonsExtra.js
+++ b/mail/base/content/aboutAddonsExtra.js
@@ -12,16 +12,21 @@ gStrings.mailExt =
 (function() {
   window.isCorrectlySigned = function() { return true; };
 
   let contentStylesheet = document.createProcessingInstruction(
     "xml-stylesheet",
     'href="chrome://messenger/content/extensionsOverlay.css" type="text/css"');
   document.insertBefore(contentStylesheet, document.documentElement);
 
+  let bindingsStylesheet = document.createProcessingInstruction(
+    "xml-stylesheet",
+    'href="chrome://messenger/content/bindings.css" type="text/css"');
+  document.insertBefore(bindingsStylesheet, document.documentElement);
+
   // Add navigation buttons for back and forward on the addons page.
   let hbox = document.createElement("hbox");
   hbox.setAttribute("id", "nav-header");
   hbox.setAttribute("align", "center");
   hbox.setAttribute("pack", "center");
 
   let backButton = document.createElement("toolbarbutton");
   backButton.setAttribute("id", "back-btn");
--- a/mail/base/content/mailWidgets.xml
+++ b/mail/base/content/mailWidgets.xml
@@ -15,17 +15,17 @@
 ]>
 
 <bindings id="mailBindings"
           xmlns="http://www.mozilla.org/xbl"
           xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
           xmlns:nc="http://home.netscape.com/NC-rdf#"
           xmlns:xbl="http://www.mozilla.org/xbl">
 
-  <binding id="attachmentlist-base" extends="chrome://global/content/bindings/richlistbox.xml#richlistbox">
+  <binding id="attachmentlist-base" extends="chrome://messenger/content/richlistbox.xml#xbl-richlistbox">
     <implementation>
       <constructor><![CDATA[
         let children = Array.from(this._childNodes);
 
         children.filter(aChild => aChild.getAttribute("selected") == "true")
                 .forEach(this.selectedItems.append, this.selectedItems);
 
         children.filter(aChild => !aChild.hasAttribute("context"))
--- a/mail/base/jar.mn
+++ b/mail/base/jar.mn
@@ -39,16 +39,17 @@ messenger.jar:
 *   content/messenger/datetimepicker.xml            (../../common/bindings/datetimepicker.xml)
     content/messenger/generalBindings.xml           (../../common/bindings/generalBindings.xml)
     content/messenger/generalBindings.js            (../../common/bindings/generalBindings.js)
     content/messenger/menubutton.xml                (../../common/bindings/menubutton.xml)
     content/messenger/menulist.css                  (../../common/bindings/menulist.css)
     content/messenger/menulist.xml                  (../../common/bindings/menulist.xml)
     content/messenger/notificationbox.xml           (../../common/bindings/notificationbox.xml)
     content/messenger/numberbox.xml                 (../../common/bindings/numberbox.xml)
+    content/messenger/richlistbox.xml               (../../common/bindings/richlistbox.xml)
     content/messenger/spinbuttons.xml               (../../common/bindings/spinbuttons.xml)
 *   content/messenger/textbox.xml                   (../../common/bindings/textbox.xml)
 *   content/messenger/toolbar.xml                   (../../common/bindings/toolbar.xml)
     content/messenger/attachmentList.css            (content/attachmentList.css)
 *   content/messenger/bindings.css                  (content/bindings.css)
     content/messenger/nsDragAndDrop.js              (content/nsDragAndDrop.js)
     content/messenger/editContactPanel.js           (content/editContactPanel.js)
     content/messenger/msgMail3PaneWindow.js         (content/msgMail3PaneWindow.js)