Bug 1321472, move the drag-scrolling behaviour into the menulist binding so that both remote <select> elements and <menulist> can share this behaviour, r=mconley
authorNeil Deakin <neil@mozilla.com>
Thu, 19 Jan 2017 10:04:20 -0500
changeset 375143 30e79c2a8338ad87bbda72d4cd20045605186b9e
parent 375142 8a0dc12ec71fca672ed3f33939a9cf0e2284c99d
child 375144 efeda43ab7b20d9216bf72b5f97a3dd715312e2a
push id6996
push userjlorenzo@mozilla.com
push dateMon, 06 Mar 2017 20:48:21 +0000
treeherdermozilla-beta@d89512dab048 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmconley
bugs1321472
milestone53.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 1321472, move the drag-scrolling behaviour into the menulist binding so that both remote <select> elements and <menulist> can share this behaviour, r=mconley
toolkit/content/widgets/popup.xml
toolkit/modules/SelectParentHelper.jsm
--- a/toolkit/content/widgets/popup.xml
+++ b/toolkit/content/widgets/popup.xml
@@ -638,11 +638,120 @@
   </binding>
 
   <binding id="popup-scrollbars" extends="chrome://global/content/bindings/popup.xml#popup">
     <content>
       <xul:scrollbox class="popup-internal-box" flex="1" orient="vertical" style="overflow: auto;">
         <children/>
       </xul:scrollbox>
     </content>
+    <implementation>
+      <field name="AUTOSCROLL_INTERVAL">25</field>
+      <field name="NOT_DRAGGING">0</field>
+      <field name="DRAG_OVER_BUTTON">-1</field>
+      <field name="DRAG_OVER_POPUP">1</field>
+
+      <field name="_draggingState">this.NOT_DRAGGING</field>
+      <field name="_scrollTimer">0</field>
+
+      <method name="enableDragScrolling">
+        <!-- when overItem is true, drag started over menuitem; when false, drag
+             started while the popup was opening.
+          -->
+        <parameter name="overItem"/>
+        <body>
+        <![CDATA[
+          if (!this._draggingState) {
+            this.setCaptureAlways();
+            this._draggingState = overItem ? this.DRAG_OVER_POPUP : this.DRAG_OVER_BUTTON;
+          }
+        ]]>
+        </body>
+      </method>
+      <method name="_clearScrollTimer">
+        <body>
+        <![CDATA[
+          if (this._scrollTimer) {
+            this.ownerDocument.defaultView.clearInterval(this._scrollTimer);
+            this._scrollTimer = 0;
+          }
+        ]]>
+        </body>
+      </method>
+    </implementation>
+    <handlers>
+      <handler event="popupshown">
+        // Enable drag scrolling even when the mouse wasn't used. The mousemove
+        // handler will remove it if the mouse isn't down.
+        this.enableDragScrolling(false);
+      </handler>
+
+      <handler event="popuphidden">
+      <![CDATA[
+        this._draggingState = this.NOT_DRAGGING;
+        this._clearScrollTimer();
+        this.releaseCapture();
+      ]]>
+      </handler>
+
+      <handler event="mousedown" button="0">
+      <![CDATA[
+        if (event.target.localName == "menuitem" ||
+            event.target.localName == "menu" ||
+            event.target.localName == "menucaption") {
+          this.enableDragScrolling(true);
+        }
+      ]]>
+      </handler>
+      <handler event="mouseup" button="0">
+      <![CDATA[
+        this._draggingState = this.NOT_DRAGGING;
+        this._clearScrollTimer();
+      ]]>
+      </handler>
+      <handler event="mousemove">
+      <![CDATA[
+        if (!this._draggingState) {
+          return;
+        }
+
+        this._clearScrollTimer();
+
+        // If the user released the mouse before the popup opens, we will
+        // still be capturing, so check that the button is still pressed. If
+        // not, release the capture and do nothing else. This also handles if
+        // the dropdown was opened via the keyboard.
+        if (!(event.buttons & 1)) {
+          this._draggingState = this.NOT_DRAGGING;
+          menupopup.releaseCapture();
+          return;
+        }
+
+        // If dragging outside the top or bottom edge of the popup, but within
+        // the popup area horizontally, scroll the list in that direction. The
+        // _draggingState flag is used to ensure that scrolling does not start
+        // until the mouse has moved over the popup first, preventing scrolling
+        // while over the dropdown button.
+        let popupRect = this.getOuterScreenRect();
+        if (event.screenX >= popupRect.left && event.screenX <= popupRect.right) {
+          if (this._draggingState == this.DRAG_OVER_BUTTON) {
+            if (event.screenY > popupRect.top && event.screenY < popupRect.bottom) {
+              this._draggingState = this.DRAG_OVER_POPUP;
+            }
+          }
+
+          if (this._draggingState == this.DRAG_OVER_POPUP &&
+              (event.screenY <= popupRect.top || event.screenY >= popupRect.bottom)) {
+            let scrollAmount = event.screenY <= popupRect.top ? -1 : 1;
+            this.scrollBox.scrollByIndex(scrollAmount);
+
+            let win = this.ownerDocument.defaultView;
+            this._scrollTimer = win.setInterval(() => {
+              this.scrollBox.scrollByIndex(scrollAmount);
+            }, this.AUTOSCROLL_INTERVAL);
+          }
+        }
+      ]]>
+      </handler>
+    </handlers>
   </binding>
 
 </bindings>
--- a/toolkit/modules/SelectParentHelper.jsm
+++ b/toolkit/modules/SelectParentHelper.jsm
@@ -10,37 +10,26 @@ this.EXPORTED_SYMBOLS = [
 
 const {utils: Cu} = Components;
 const {AppConstants} = Cu.import("resource://gre/modules/AppConstants.jsm", {});
 const {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
 
 // Maximum number of rows to display in the select dropdown.
 const MAX_ROWS = 20;
 
-// Interval between autoscrolls
-const AUTOSCROLL_INTERVAL = 25;
-
 // Minimum elements required to show select search
 const SEARCH_MINIMUM_ELEMENTS = 40;
 
-// Dragging states
-const NOT_DRAGGING = 0;
-const DRAG_OVER_SELECT = -1;
-const DRAG_OVER_POPUP = 1;
-
 var currentBrowser = null;
 var currentMenulist = null;
 var currentZoom = 1;
 var closedWithEnter = false;
 var selectRect;
 
 this.SelectParentHelper = {
-  draggingState: NOT_DRAGGING,
-  scrollTimer: 0,
-
   populate(menulist, items, selectedIndex, zoom) {
     // Clear the current contents of the popup
     menulist.menupopup.textContent = "";
     currentZoom = zoom;
     currentMenulist = menulist;
     populateChildren(menulist, items, selectedIndex, zoom);
   },
 
@@ -73,52 +62,27 @@ this.SelectParentHelper = {
     menupopup.classList.toggle("isOpenedViaTouch", isOpenedViaTouch);
 
     let constraintRect = browser.getBoundingClientRect();
     constraintRect = new win.DOMRect(constraintRect.left + win.mozInnerScreenX,
                                      constraintRect.top + win.mozInnerScreenY,
                                      constraintRect.width, constraintRect.height);
     menupopup.setConstraintRect(constraintRect);
     menupopup.openPopupAtScreenRect(AppConstants.platform == "macosx" ? "selection" : "after_start", rect.left, rect.top, rect.width, rect.height, false, false);
-
-    // Set up for dragging
-    this.enableDragScrolling(false);
   },
 
   hide(menulist, browser) {
     if (currentBrowser == browser) {
       menulist.menupopup.hidePopup();
     }
   },
 
-  enableDragScrolling(overOption) {
-    if (this.draggingState) {
-      return;
-    }
-
-    currentMenulist.menupopup.setCaptureAlways();
-    this.draggingState = overOption ? DRAG_OVER_POPUP : DRAG_OVER_SELECT;
-    currentMenulist.menupopup.addEventListener("mousemove", this);
-  },
-
-  clearScrollTimer() {
-    if (this.scrollTimer) {
-      let win = currentBrowser.ownerDocument.defaultView;
-      win.clearInterval(this.scrollTimer);
-      this.scrollTimer = 0;
-    }
-  },
-
   handleEvent(event) {
     switch (event.type) {
       case "mouseup":
-        this.draggingState = NOT_DRAGGING;
-        this.clearScrollTimer();
-        currentMenulist.menupopup.removeEventListener("mousemove", this);
-
         function inRect(rect, x, y) {
           return x >= rect.left && x <= rect.left + rect.width && y >= rect.top && y <= rect.top + rect.height;
         }
 
         let x = event.screenX, y = event.screenY;
         let onAnchor = !inRect(currentMenulist.menupopup.getOuterScreenRect(), x, y) &&
                         inRect(selectRect, x, y) && currentMenulist.menupopup.state == "open";
         currentBrowser.messageManager.sendAsyncMessage("Forms:MouseUp", { onAnchor });
@@ -127,66 +91,16 @@ this.SelectParentHelper = {
       case "mouseover":
         currentBrowser.messageManager.sendAsyncMessage("Forms:MouseOver", {});
         break;
 
       case "mouseout":
         currentBrowser.messageManager.sendAsyncMessage("Forms:MouseOut", {});
         break;
 
-      case "mousedown":
-        if (event.target.localName == "menuitem" ||
-            event.target.localName == "menu" ||
-            event.target.localName == "menucaption") {
-          this.enableDragScrolling(true);
-        }
-        break;
-
-      case "mousemove":
-        let menupopup = currentMenulist.menupopup;
-
-        this.clearScrollTimer();
-
-        // If the user released the mouse before the popup opens, we will
-        // still be capturing, so check that the button is still pressed. If
-        // not, release the capture and do nothing else. This also handles if
-        // the dropdown was opened via the keyboard.
-        if (!(event.buttons & 1)) {
-          currentMenulist.menupopup.removeEventListener("mousemove", this);
-          menupopup.releaseCapture();
-          return;
-        }
-
-        // If dragging outside the top or bottom edge of the popup, but within
-        // the popup area horizontally, scroll the list in that direction. The
-        // this.draggingState flag is used to ensure that scrolling does not
-        // start until the mouse has moved over the popup first, preventing
-        // scrolling while over the dropdown button.
-        let popupRect = menupopup.getOuterScreenRect();
-        if (event.screenX >= popupRect.left && event.screenX <= popupRect.right) {
-          if (this.draggingState == DRAG_OVER_SELECT) {
-            if (event.screenY > popupRect.top && event.screenY < popupRect.bottom) {
-              this.draggingState = DRAG_OVER_POPUP;
-            }
-          }
-
-          if (this.draggingState == DRAG_OVER_POPUP &&
-              (event.screenY <= popupRect.top || event.screenY >= popupRect.bottom)) {
-            let scrollAmount = event.screenY <= popupRect.top ? -1 : 1;
-            menupopup.scrollBox.scrollByIndex(scrollAmount);
-
-            let win = currentBrowser.ownerDocument.defaultView;
-            this.scrollTimer = win.setInterval(function() {
-              menupopup.scrollBox.scrollByIndex(scrollAmount);
-            }, AUTOSCROLL_INTERVAL);
-          }
-        }
-
-        break;
-
       case "keydown":
         if (event.keyCode == event.DOM_VK_RETURN) {
           closedWithEnter = true;
         }
         break;
 
       case "command":
         if (event.target.hasAttribute("value")) {
@@ -202,19 +116,16 @@ this.SelectParentHelper = {
           currentMenulist.menupopup.hidePopup();
         }
         break;
 
       case "popuphidden":
         currentBrowser.messageManager.sendAsyncMessage("Forms:DismissedDropDown", {});
         let popup = event.target;
         this._unregisterListeners(currentBrowser, popup);
-        this.draggingState = NOT_DRAGGING;
-        this.clearScrollTimer();
-        popup.releaseCapture();
         popup.parentNode.hidden = true;
         currentBrowser = null;
         currentMenulist = null;
         currentZoom = 1;
         break;
     }
   },
 
@@ -230,29 +141,27 @@ this.SelectParentHelper = {
       let selectedIndex = msg.data.selectedIndex;
       this.populate(currentMenulist, options, selectedIndex, currentZoom);
     }
   },
 
   _registerListeners(browser, popup) {
     popup.addEventListener("command", this);
     popup.addEventListener("popuphidden", this);
-    popup.addEventListener("mousedown", this);
     popup.addEventListener("mouseover", this);
     popup.addEventListener("mouseout", this);
     browser.ownerDocument.defaultView.addEventListener("mouseup", this, true);
     browser.ownerDocument.defaultView.addEventListener("keydown", this, true);
     browser.ownerDocument.defaultView.addEventListener("fullscreen", this, true);
     browser.messageManager.addMessageListener("Forms:UpdateDropDown", this);
   },
 
   _unregisterListeners(browser, popup) {
     popup.removeEventListener("command", this);
     popup.removeEventListener("popuphidden", this);
-    popup.removeEventListener("mousedown", this);
     popup.removeEventListener("mouseover", this);
     popup.removeEventListener("mouseout", this);
     browser.ownerDocument.defaultView.removeEventListener("mouseup", this, true);
     browser.ownerDocument.defaultView.removeEventListener("keydown", this, true);
     browser.ownerDocument.defaultView.removeEventListener("fullscreen", this, true);
     browser.messageManager.removeMessageListener("Forms:UpdateDropDown", this);
   },