Bug 1226272 - Part 1: Make devtools tab draggable and reorderable. r=jdescottes
authorDaisuke Akatsuka <dakatsuka@mozilla.com>
Thu, 19 Apr 2018 18:41:56 +0900
changeset 468146 dd92dd2d5ae74177f2db6a6463940bfe776f996f
parent 468145 210ee7ebb398f40a51969ee316d2ed9e4d864f84
child 468147 59a62840bad1551a12d7a6535829a9cec94bbde5
push id9165
push userasasaki@mozilla.com
push dateThu, 26 Apr 2018 21:04:54 +0000
treeherdermozilla-beta@064c3804de2e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjdescottes
bugs1226272
milestone61.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 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
@@ -291,8 +291,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;
+}