Bug 1226272 - Part 1: Make devtools tab draggable and reorderable. r?jdescottes draft
authorDaisuke Akatsuka <dakatsuka@mozilla.com>
Thu, 19 Apr 2018 18:41:56 +0900
changeset 784900 29b30610e52fa58c6c1f05455270524b75c2b0b5
parent 783966 789e30ff2e3d6e1fcfce1a373c1e5635488d24da
child 784901 23d2ddeaf2f3510452cea715b5fd11cce2d86a6f
push id107071
push userbmo:dakatsuka@mozilla.com
push dateThu, 19 Apr 2018 09:52:38 +0000
reviewersjdescottes
bugs1226272
milestone61.0a1
Bug 1226272 - Part 1: Make devtools tab draggable and reorderable. r?jdescottes MozReview-Commit-ID: 3EdbVvG69H8
devtools/client/framework/components/toolbox-tabs.js
devtools/client/framework/moz.build
devtools/client/framework/toolbox-tabs-order-manager.js
devtools/client/themes/toolbox.css
--- a/devtools/client/framework/components/toolbox-tabs.js
+++ b/devtools/client/framework/components/toolbox-tabs.js
@@ -7,16 +7,17 @@ const { Component, createFactory } = req
 const dom = require("devtools/client/shared/vendor/react-dom-factories");
 const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
 const {findDOMNode} = require("devtools/client/shared/vendor/react-dom");
 const {button, div} = dom;
 
 const Menu = require("devtools/client/framework/menu");
 const MenuItem = require("devtools/client/framework/menu-item");
 const ToolboxTab = createFactory(require("devtools/client/framework/components/toolbox-tab"));
+const ToolboxTabsOrderManager = require("devtools/client/framework/toolbox-tabs-order-manager");
 
 // 26px is chevron devtools button width.(i.e. tools-chevronmenu)
 const CHEVRON_BUTTON_WIDTH = 26;
 
 class ToolboxTabs extends Component {
   // See toolbox-toolbar propTypes for details on the props used here.
   static get propTypes() {
     return {
@@ -41,16 +42,18 @@ class ToolboxTabs extends Component {
 
     // Map with tool Id and its width size. This lifecycle is out of React's
     // lifecycle. If a tool is registered, ToolboxTabs will add target tool id
     // to this map. ToolboxTabs will never remove tool id from this cache.
     this._cachedToolTabsWidthMap = new Map();
 
     this._resizeTimerId = null;
     this.resizeHandler = this.resizeHandler.bind(this);
+
+    this._tabsOrderManager = new ToolboxTabsOrderManager();
   }
 
   componentDidMount() {
     window.addEventListener("resize", this.resizeHandler);
     this.updateCachedToolTabsWidthMap();
     this.updateOverflowedTabs();
   }
 
@@ -64,16 +67,20 @@ class ToolboxTabs extends Component {
 
   componentDidUpdate(prevProps, prevState) {
     if (this.shouldUpdateToolboxTabs(prevProps, this.props)) {
       this.updateCachedToolTabsWidthMap();
       this.updateOverflowedTabs();
     }
   }
 
+  componentWillUnmount() {
+    this._tabsOrderManager.destroy();
+  }
+
   /**
    * Check if two array of ids are the same or not.
    */
   equalToolIdArray(prevPanels, nextPanels) {
     if (prevPanels.length !== nextPanels.length) {
       return false;
     }
 
@@ -248,17 +255,18 @@ class ToolboxTabs extends Component {
     });
 
     return div(
       {
         className: "toolbox-tabs-wrapper"
       },
       div(
         {
-          className: "toolbox-tabs"
+          className: "toolbox-tabs",
+          onMouseDown: (e) => this._tabsOrderManager.onMouseDown(e),
         },
         tabs,
         (this.state.overflowedTabIds.length > 0)
           ? this.renderToolsChevronButton() : null
       )
     );
   }
 }
--- a/devtools/client/framework/moz.build
+++ b/devtools/client/framework/moz.build
@@ -22,14 +22,15 @@ DevToolsModules(
     'sidebar.js',
     'source-map-url-service.js',
     'target-from-url.js',
     'target.js',
     'toolbox-highlighter-utils.js',
     'toolbox-host-manager.js',
     'toolbox-hosts.js',
     'toolbox-options.js',
+    'toolbox-tabs-order-manager.js',
     'toolbox.js',
     'ToolboxProcess.jsm',
 )
 
 with Files('**'):
     BUG_COMPONENT = ('Firefox', 'Developer Tools: Framework')
new file mode 100644
--- /dev/null
+++ b/devtools/client/framework/toolbox-tabs-order-manager.js
@@ -0,0 +1,112 @@
+/* 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/. */
+
+"use strict";
+
+/**
+ * Manage the order of devtools tabs.
+ */
+class ToolboxTabsOrderManager {
+  constructor() {
+    this.onMouseDown = this.onMouseDown.bind(this);
+    this.onMouseMove = this.onMouseMove.bind(this);
+    this.onMouseOut = this.onMouseOut.bind(this);
+    this.onMouseUp = this.onMouseUp.bind(this);
+  }
+
+  destroy() {
+    this.onMouseUp();
+  }
+
+  onMouseDown(e) {
+    if (!e.target.classList.contains("devtools-tab")) {
+      return;
+    }
+
+    this.dragStartX = e.pageX;
+    this.dragTarget = e.target;
+    this.previousPageX = e.pageX;
+    this.toolboxContainerElement = this.dragTarget.closest("#toolbox-container");
+    this.toolboxTabsElement = this.dragTarget.closest(".toolbox-tabs");
+
+    this.dragTarget.ownerDocument.addEventListener("mousemove", this.onMouseMove);
+    this.dragTarget.ownerDocument.addEventListener("mouseout", this.onMouseOut);
+    this.dragTarget.ownerDocument.addEventListener("mouseup", this.onMouseUp);
+
+    this.toolboxContainerElement.classList.add("tabs-reordering");
+  }
+
+  onMouseMove(e) {
+    const tabsElement = this.toolboxTabsElement;
+    const diffPageX = e.pageX - this.previousPageX;
+    const dragTargetCenterX =
+      this.dragTarget.offsetLeft + diffPageX + this.dragTarget.clientWidth / 2;
+    let isDragTargetPreviousSibling = false;
+
+    for (const tabElement of tabsElement.querySelectorAll(".devtools-tab")) {
+      if (tabElement === this.dragTarget) {
+        isDragTargetPreviousSibling = true;
+        continue;
+      }
+
+      const anotherElementCenterX =
+        tabElement.offsetLeft + tabElement.clientWidth / 2;
+
+      if (Math.abs(dragTargetCenterX - anotherElementCenterX) <
+          tabElement.clientWidth / 3) {
+        const xBefore = this.dragTarget.offsetLeft;
+
+        if (isDragTargetPreviousSibling) {
+          tabsElement.insertBefore(this.dragTarget, tabElement.nextSibling);
+        } else {
+          tabsElement.insertBefore(this.dragTarget, tabElement);
+        }
+
+        const xAfter = this.dragTarget.offsetLeft;
+        this.dragStartX += xAfter - xBefore;
+        break;
+      }
+    }
+
+    let distance = e.pageX - this.dragStartX;
+
+    if ((!this.dragTarget.previousSibling && distance < 0) ||
+        (!this.dragTarget.nextSibling && distance > 0)) {
+      // If the drag target is already edge of the tabs and the mouse will make the
+      // element to move to same direction more, keep the position.
+      distance = 0;
+    }
+
+    this.dragTarget.style.left = `${ distance }px`;
+    this.previousPageX = e.pageX;
+  }
+
+  onMouseOut(e) {
+    if (e.pageX <= 0 || this.dragTarget.ownerDocument.width <= e.pageX ||
+        e.pageY <= 0 || this.dragTarget.ownerDocument.height <= e.pageY) {
+      this.onMouseUp();
+    }
+  }
+
+  onMouseUp() {
+    if (!this.dragTarget) {
+      // The case in here has two type:
+      // 1. Although destroy method was called, it was not during reordering.
+      // 2. Although mouse event occur, destroy method was called during reordering.
+      return;
+    }
+
+    this.dragTarget.ownerDocument.removeEventListener("mousemove", this.onMouseMove);
+    this.dragTarget.ownerDocument.removeEventListener("mouseout", this.onMouseOut);
+    this.dragTarget.ownerDocument.removeEventListener("mouseup", this.onMouseUp);
+
+    this.toolboxContainerElement.classList.remove("tabs-reordering");
+    this.dragTarget.style.left = null;
+    this.dragTarget = null;
+    this.toolboxContainerElement = null;
+    this.toolboxTabsElement = null;
+  }
+}
+
+module.exports = ToolboxTabsOrderManager;
--- a/devtools/client/themes/toolbox.css
+++ b/devtools/client/themes/toolbox.css
@@ -285,8 +285,13 @@
 
 /**
  * Enrure that selected toolbox panel's contents are keyboard accessible as they
  * are explicitly made not to be when hidden (default).
  */
 .toolbox-panel[selected] * {
   -moz-user-focus: normal;
 }
+
+/* Toolbox tabs reordering */
+#toolbox-container.tabs-reordering > .theme-body {
+  pointer-events: none;
+}