Bug 1226272 - Part 1: Make devtools tab draggable and reorderable. r?jdescottes
MozReview-Commit-ID: 3EdbVvG69H8
--- 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;
+}