284 lines
10 KiB
JavaScript
284 lines
10 KiB
JavaScript
/* 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";
|
|
|
|
const { AddonManager } = ChromeUtils.importESModule(
|
|
"resource://gre/modules/AddonManager.sys.mjs",
|
|
// AddonManager is a singleton, never create two instances of it.
|
|
{ global: "shared" }
|
|
);
|
|
const {
|
|
gDevTools,
|
|
} = require("resource://devtools/client/framework/devtools.js");
|
|
const PREFERENCE_NAME = "devtools.toolbox.tabsOrder";
|
|
|
|
/**
|
|
* Manage the order of devtools tabs.
|
|
*/
|
|
class ToolboxTabsOrderManager {
|
|
constructor(toolbox, onOrderUpdated, panelDefinitions) {
|
|
this.toolbox = toolbox;
|
|
this.onOrderUpdated = onOrderUpdated;
|
|
this.currentPanelDefinitions = panelDefinitions || [];
|
|
|
|
this.onMouseDown = this.onMouseDown.bind(this);
|
|
this.onMouseMove = this.onMouseMove.bind(this);
|
|
this.onMouseUp = this.onMouseUp.bind(this);
|
|
|
|
Services.prefs.addObserver(PREFERENCE_NAME, this.onOrderUpdated);
|
|
}
|
|
|
|
async destroy() {
|
|
Services.prefs.removeObserver(PREFERENCE_NAME, this.onOrderUpdated);
|
|
|
|
// Call mouseUp() to clear the state to prepare for in case a dragging was in progress
|
|
// when the destroy() was called.
|
|
await this.onMouseUp();
|
|
}
|
|
|
|
insertBefore(target) {
|
|
const xBefore = this.dragTarget.offsetLeft;
|
|
this.toolboxTabsElement.insertBefore(this.dragTarget, target);
|
|
const xAfter = this.dragTarget.offsetLeft;
|
|
this.dragStartX += xAfter - xBefore;
|
|
this.isOrderUpdated = true;
|
|
}
|
|
|
|
isFirstTab(tabElement) {
|
|
return !tabElement.previousSibling;
|
|
}
|
|
|
|
isLastTab(tabElement) {
|
|
return (
|
|
!tabElement.nextSibling ||
|
|
tabElement.nextSibling.id === "tools-chevron-menu-button"
|
|
);
|
|
}
|
|
|
|
isRTL() {
|
|
return this.toolbox.direction === "rtl";
|
|
}
|
|
|
|
async saveOrderPreference() {
|
|
const tabs = [...this.toolboxTabsElement.querySelectorAll(".devtools-tab")];
|
|
const tabIds = tabs.map(tab => tab.dataset.extensionId || tab.dataset.id);
|
|
// Concat the overflowed tabs id since they are not contained in visible tabs.
|
|
// The overflowed tabs cannot be reordered so we just append the id from current
|
|
// panel definitions on their order.
|
|
const overflowedTabIds = this.currentPanelDefinitions
|
|
.filter(definition => !tabs.some(tab => tab.dataset.id === definition.id))
|
|
.map(definition => definition.extensionId || definition.id);
|
|
const currentTabIds = tabIds.concat(overflowedTabIds);
|
|
const dragTargetId =
|
|
this.dragTarget.dataset.extensionId || this.dragTarget.dataset.id;
|
|
const prefIds = getTabsOrderFromPreference();
|
|
const absoluteIds = toAbsoluteOrder(prefIds, currentTabIds, dragTargetId);
|
|
|
|
// Remove panel id which is not in panel definitions and addons list.
|
|
const extensions = await AddonManager.getAllAddons();
|
|
const definitions = gDevTools.getToolDefinitionArray();
|
|
const result = absoluteIds.filter(
|
|
id =>
|
|
definitions.find(d => id === (d.extensionId || d.id)) ||
|
|
extensions.find(e => id === e.id)
|
|
);
|
|
|
|
Services.prefs.setCharPref(PREFERENCE_NAME, result.join(","));
|
|
}
|
|
|
|
setCurrentPanelDefinitions(currentPanelDefinitions) {
|
|
this.currentPanelDefinitions = currentPanelDefinitions;
|
|
}
|
|
|
|
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.isOrderUpdated = false;
|
|
this.eventTarget = this.dragTarget.ownerGlobal.top;
|
|
|
|
this.eventTarget.addEventListener("mousemove", this.onMouseMove);
|
|
this.eventTarget.addEventListener("mouseup", this.onMouseUp);
|
|
|
|
this.toolboxContainerElement.classList.add("tabs-reordering");
|
|
}
|
|
|
|
onMouseMove(e) {
|
|
const diffPageX = e.pageX - this.previousPageX;
|
|
let dragTargetCenterX =
|
|
this.dragTarget.offsetLeft + diffPageX + this.dragTarget.clientWidth / 2;
|
|
let isDragTargetPreviousSibling = false;
|
|
|
|
const tabElements =
|
|
this.toolboxTabsElement.querySelectorAll(".devtools-tab");
|
|
|
|
// Calculate the minimum and maximum X-offset that can be valid for the drag target.
|
|
const firstElement = tabElements[0];
|
|
const firstElementCenterX =
|
|
firstElement.offsetLeft + firstElement.clientWidth / 2;
|
|
const lastElement = tabElements[tabElements.length - 1];
|
|
const lastElementCenterX =
|
|
lastElement.offsetLeft + lastElement.clientWidth / 2;
|
|
const max = Math.max(firstElementCenterX, lastElementCenterX);
|
|
const min = Math.min(firstElementCenterX, lastElementCenterX);
|
|
|
|
// Normalize the target center X so to remain between the first and last tab.
|
|
dragTargetCenterX = Math.min(max, dragTargetCenterX);
|
|
dragTargetCenterX = Math.max(min, dragTargetCenterX);
|
|
|
|
for (const tabElement of tabElements) {
|
|
if (tabElement === this.dragTarget) {
|
|
isDragTargetPreviousSibling = true;
|
|
continue;
|
|
}
|
|
|
|
// Is the dragTarget near the center of the other tab?
|
|
const anotherCenterX = tabElement.offsetLeft + tabElement.clientWidth / 2;
|
|
const distanceWithDragTarget = Math.abs(
|
|
dragTargetCenterX - anotherCenterX
|
|
);
|
|
const isReplaceable = distanceWithDragTarget < tabElement.clientWidth / 3;
|
|
|
|
if (isReplaceable) {
|
|
const replaceableElement = isDragTargetPreviousSibling
|
|
? tabElement.nextSibling
|
|
: tabElement;
|
|
this.insertBefore(replaceableElement);
|
|
break;
|
|
}
|
|
}
|
|
|
|
let distance = e.pageX - this.dragStartX;
|
|
|
|
// To accomodate for RTL locales, we cannot rely on the first/last element of the
|
|
// NodeList. We cannot have negative distances for the leftmost tab, and we cannot
|
|
// have positive distances for the rightmost tab.
|
|
const isFirstTab = this.isFirstTab(this.dragTarget);
|
|
const isLastTab = this.isLastTab(this.dragTarget);
|
|
const isLeftmostTab = this.isRTL() ? isLastTab : isFirstTab;
|
|
const isRightmostTab = this.isRTL() ? isFirstTab : isLastTab;
|
|
|
|
if ((isLeftmostTab && distance < 0) || (isRightmostTab && 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;
|
|
}
|
|
|
|
async 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;
|
|
}
|
|
|
|
if (this.isOrderUpdated) {
|
|
await this.saveOrderPreference();
|
|
|
|
// Log which tabs reordered. The question we want to answer is:
|
|
// "How frequently are the tabs re-ordered, also which tabs get re-ordered?"
|
|
const toolId =
|
|
this.dragTarget.dataset.extensionId || this.dragTarget.dataset.id;
|
|
Glean.devtoolsToolbox.tabsReordered[toolId].add(1);
|
|
}
|
|
|
|
this.eventTarget.removeEventListener("mousemove", this.onMouseMove);
|
|
this.eventTarget.removeEventListener("mouseup", this.onMouseUp);
|
|
|
|
this.toolboxContainerElement.classList.remove("tabs-reordering");
|
|
this.dragTarget.style.left = null;
|
|
this.dragTarget = null;
|
|
this.toolboxContainerElement = null;
|
|
this.toolboxTabsElement = null;
|
|
this.eventTarget = null;
|
|
}
|
|
}
|
|
|
|
function getTabsOrderFromPreference() {
|
|
const pref = Services.prefs.getCharPref(PREFERENCE_NAME, "");
|
|
return pref ? pref.split(",") : [];
|
|
}
|
|
|
|
function sortPanelDefinitions(definitions) {
|
|
const toolIds = getTabsOrderFromPreference();
|
|
|
|
return definitions.sort((a, b) => {
|
|
let orderA = toolIds.indexOf(a.extensionId || a.id);
|
|
let orderB = toolIds.indexOf(b.extensionId || b.id);
|
|
orderA = orderA < 0 ? Number.MAX_VALUE : orderA;
|
|
orderB = orderB < 0 ? Number.MAX_VALUE : orderB;
|
|
return orderA - orderB;
|
|
});
|
|
}
|
|
|
|
/*
|
|
* This function returns absolute tab ids that were merged the both ids that are in
|
|
* preference and tabs.
|
|
* Some tabs added with add-ons etc show/hide depending on conditions.
|
|
* However, all of tabs that include hidden tab always keep the relationship with
|
|
* left side tab, except in case the left tab was target of dragging. If the left
|
|
* tab has been moved, it keeps its relationship with the tab next to it.
|
|
*
|
|
* Case 1: Drag a tab to left
|
|
* currentTabIds: [T1, T2, T3, T4, T5]
|
|
* prefIds : [T1, T2, T3, E1(hidden), T4, T5]
|
|
* drag T4 : [T1, T2, T4, T3, T5]
|
|
* result : [T1, T2, T4, T3, E1, T5]
|
|
*
|
|
* Case 2: Drag a tab to right
|
|
* currentTabIds: [T1, T2, T3, T4, T5]
|
|
* prefIds : [T1, T2, T3, E1(hidden), T4, T5]
|
|
* drag T2 : [T1, T3, T4, T2, T5]
|
|
* result : [T1, T3, E1, T4, T2, T5]
|
|
*
|
|
* Case 3: Hidden tab was left end and drag a tab to left end
|
|
* currentTabIds: [T1, T2, T3, T4, T5]
|
|
* prefIds : [E1(hidden), T1, T2, T3, T4, T5]
|
|
* drag T4 : [T4, T1, T2, T3, T5]
|
|
* result : [E1, T4, T1, T2, T3, T5]
|
|
*
|
|
* Case 4: Hidden tab was right end and drag a tab to right end
|
|
* currentTabIds: [T1, T2, T3, T4, T5]
|
|
* prefIds : [T1, T2, T3, T4, T5, E1(hidden)]
|
|
* drag T1 : [T2, T3, T4, T5, T1]
|
|
* result : [T2, T3, T4, T5, E1, T1]
|
|
*
|
|
* @param Array - prefIds: id array of preference
|
|
* @param Array - currentTabIds: id array of appearanced tabs
|
|
* @param String - dragTargetId: id of dragged target
|
|
* @return Array
|
|
*/
|
|
function toAbsoluteOrder(prefIds, currentTabIds, dragTargetId) {
|
|
currentTabIds = [...currentTabIds];
|
|
let indexAtCurrentTabs = 0;
|
|
|
|
for (const prefId of prefIds) {
|
|
if (prefId === dragTargetId) {
|
|
// do nothing
|
|
} else if (currentTabIds.includes(prefId)) {
|
|
indexAtCurrentTabs = currentTabIds.indexOf(prefId) + 1;
|
|
} else {
|
|
currentTabIds.splice(indexAtCurrentTabs, 0, prefId);
|
|
indexAtCurrentTabs += 1;
|
|
}
|
|
}
|
|
|
|
return currentTabIds;
|
|
}
|
|
|
|
module.exports.ToolboxTabsOrderManager = ToolboxTabsOrderManager;
|
|
module.exports.sortPanelDefinitions = sortPanelDefinitions;
|
|
module.exports.toAbsoluteOrder = toAbsoluteOrder;
|