Bug 1226272 - Part 1: Make devtools tab draggable and reorderable. r? draft
authorDaisuke Akatsuka <dakatsuka@mozilla.com>
Thu, 12 Apr 2018 19:39:05 +0900
changeset 780983 e6201fc911e6444bde065a08586e082614dcca56
parent 779089 b4bc6b2401738b78fd47127a4c716bb9178e1a09
child 780984 a3d97a5001239d5ea20e126f21680588aba8dab6
push id106179
push userbmo:dakatsuka@mozilla.com
push dateThu, 12 Apr 2018 10:40:27 +0000
bugs1226272
milestone61.0a1
Bug 1226272 - Part 1: Make devtools tab draggable and reorderable. r? MozReview-Commit-ID: 3fdHXX8Tljn
devtools/client/framework/moz.build
devtools/client/framework/toolbox-tabs-order-manager.js
devtools/client/framework/toolbox.js
--- 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,101 @@
+/* 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(tabsElement) {
+    this.tabsElement = tabsElement;
+
+    this.onMouseDown = this.onMouseDown.bind(this);
+    this.onMouseMove = this.onMouseMove.bind(this);
+    this.onMouseOut = this.onMouseOut.bind(this);
+    this.onMouseUp = this.onMouseUp.bind(this);
+
+    let order = 0;
+
+    for (const tabElement of this.tabsElement.childNodes) {
+      tabElement.style.order = order;
+      order++;
+    }
+
+    this.tabsElement.addEventListener("mousedown", this.onMouseDown);
+  }
+
+  destroy() {
+    this.tabsElement.removeEventListener("mousedown", this.onMouseDown);
+    this.tabsElement = null;
+  }
+
+  onMouseDown(e) {
+    if (!e.target.classList.contains("devtools-tab")) {
+      return;
+    }
+
+    this.dragStartX = e.pageX;
+    this.dragTarget = e.target;
+    this.maxOrder = -Number.MAX_VALUE;
+    this.minOrder = Number.MAX_VALUE;
+
+    for (const tabElement of this.tabsElement.childNodes) {
+      const order = parseInt(tabElement.style.order, 10);
+      this.maxOrder = Math.max(this.maxOrder, order);
+      this.minOrder = Math.min(this.minOrder, order);
+    }
+
+    this.tabsElement.ownerDocument.addEventListener("mousemove", this.onMouseMove);
+    this.tabsElement.ownerDocument.addEventListener("mouseout", this.onMouseOut);
+    this.tabsElement.ownerDocument.addEventListener("mouseup", this.onMouseUp);
+  }
+
+  onMouseMove(e) {
+    for (const tabElement of this.tabsElement.childNodes) {
+      if (tabElement === this.dragTarget) {
+        continue;
+      }
+
+      const bounds = tabElement.getBoundingClientRect();
+      const sx = bounds.left + bounds.width * 0.2;
+
+      if (sx < e.pageX && e.pageX < sx + bounds.width * 0.6) {
+        const order = this.dragTarget.style.order;
+        this.dragTarget.style.order = tabElement.style.order;
+        tabElement.style.order = order;
+        this.dragStartX += tabElement.style.order < this.dragTarget.style.order
+                             ? tabElement.clientWidth : -tabElement.clientWidth;
+        break;
+      }
+    }
+
+    let distance = e.pageX - this.dragStartX;
+
+    if ((this.dragTarget.style.order == this.minOrder && distance < 0) ||
+        (this.dragTarget.style.order == this.maxOrder && distance > 0)) {
+      distance = 0;
+    }
+
+    this.dragTarget.style.left = `${ distance }px`;
+  }
+
+  onMouseOut(e) {
+    if (e.pageX <= 0 || this.tabsElement.ownerDocument.width <= e.pageX ||
+        e.pageY <= 0 || this.tabsElement.ownerDocument.height <= e.pageY) {
+      this.onMouseUp();
+    }
+  }
+
+  onMouseUp() {
+    this.dragTarget.style.left = "unset";
+    this.dragTarget = null;
+
+    this.tabsElement.ownerDocument.removeEventListener("mousemove", this.onMouseMove);
+    this.tabsElement.ownerDocument.removeEventListener("mouseout", this.onMouseOut);
+    this.tabsElement.ownerDocument.removeEventListener("mouseup", this.onMouseUp);
+  }
+}
+
+exports.ToolboxTabsOrderManager = ToolboxTabsOrderManager;
--- a/devtools/client/framework/toolbox.js
+++ b/devtools/client/framework/toolbox.js
@@ -68,16 +68,18 @@ loader.lazyRequireGetter(this, "HUDServi
 loader.lazyRequireGetter(this, "viewSource",
   "devtools/client/shared/view-source");
 loader.lazyRequireGetter(this, "StyleSheetsFront",
   "devtools/shared/fronts/stylesheets", true);
 loader.lazyRequireGetter(this, "buildHarLog",
   "devtools/client/netmonitor/src/har/har-builder-utils", true);
 loader.lazyRequireGetter(this, "getKnownDeviceFront",
   "devtools/shared/fronts/device", true);
+loader.lazyRequireGetter(this, "ToolboxTabsOrderManager",
+  "devtools/client/framework/toolbox-tabs-order-manager", true);
 
 loader.lazyGetter(this, "domNodeConstants", () => {
   return require("devtools/shared/dom-node-constants");
 });
 
 loader.lazyGetter(this, "registerHarOverlay", () => {
   return require("devtools/client/netmonitor/src/har/toolbox-overlay").register;
 });
@@ -557,16 +559,19 @@ Toolbox.prototype = {
 
       // If in testing environment, wait for performance connection to finish,
       // so we don't have to explicitly wait for this in tests; ideally, all tests
       // will handle this on their own, but each have their own tear down function.
       if (flags.testing) {
         await performanceFrontConnection;
       }
 
+      this._tabsOrderManager =
+        new ToolboxTabsOrderManager(this.doc.querySelector(".toolbox-tabs"));
+
       this.emit("ready");
       this._isOpenDeferred.resolve();
     }.bind(this))().catch(console.error);
   },
 
   /**
    * loading React modules when needed (to avoid performance penalties
    * during Firefox start up time).
@@ -2662,16 +2667,20 @@ Toolbox.prototype = {
         this._updateTextBoxMenuItems, true);
       this.textBoxContextMenuPopup = null;
     }
     if (this._componentMount) {
       this._componentMount.removeEventListener("keypress", this._onToolbarArrowKeypress);
       this.ReactDOM.unmountComponentAtNode(this._componentMount);
       this._componentMount = null;
     }
+    if (this._tabsOrderManager) {
+      this._tabsOrderManager.destroy();
+      this._tabsOrderManager = null;
+    }
 
     let outstanding = [];
     for (let [id, panel] of this._toolPanels) {
       try {
         gDevTools.emit(id + "-destroy", this, panel);
         this.emit(id + "-destroy", panel);
 
         outstanding.push(panel.destroy());