tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

toolbox-tabs-order-manager.js (10319B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 "use strict";
      6 
      7 const { AddonManager } = ChromeUtils.importESModule(
      8  "resource://gre/modules/AddonManager.sys.mjs",
      9  // AddonManager is a singleton, never create two instances of it.
     10  { global: "shared" }
     11 );
     12 const {
     13  gDevTools,
     14 } = require("resource://devtools/client/framework/devtools.js");
     15 const PREFERENCE_NAME = "devtools.toolbox.tabsOrder";
     16 
     17 /**
     18 * Manage the order of devtools tabs.
     19 */
     20 class ToolboxTabsOrderManager {
     21  constructor(toolbox, onOrderUpdated, panelDefinitions) {
     22    this.toolbox = toolbox;
     23    this.onOrderUpdated = onOrderUpdated;
     24    this.currentPanelDefinitions = panelDefinitions || [];
     25 
     26    this.onMouseDown = this.onMouseDown.bind(this);
     27    this.onMouseMove = this.onMouseMove.bind(this);
     28    this.onMouseUp = this.onMouseUp.bind(this);
     29 
     30    Services.prefs.addObserver(PREFERENCE_NAME, this.onOrderUpdated);
     31  }
     32 
     33  async destroy() {
     34    Services.prefs.removeObserver(PREFERENCE_NAME, this.onOrderUpdated);
     35 
     36    // Call mouseUp() to clear the state to prepare for in case a dragging was in progress
     37    // when the destroy() was called.
     38    await this.onMouseUp();
     39  }
     40 
     41  insertBefore(target) {
     42    const xBefore = this.dragTarget.offsetLeft;
     43    this.toolboxTabsElement.insertBefore(this.dragTarget, target);
     44    const xAfter = this.dragTarget.offsetLeft;
     45    this.dragStartX += xAfter - xBefore;
     46    this.isOrderUpdated = true;
     47  }
     48 
     49  isFirstTab(tabElement) {
     50    return !tabElement.previousSibling;
     51  }
     52 
     53  isLastTab(tabElement) {
     54    return (
     55      !tabElement.nextSibling ||
     56      tabElement.nextSibling.id === "tools-chevron-menu-button"
     57    );
     58  }
     59 
     60  isRTL() {
     61    return this.toolbox.direction === "rtl";
     62  }
     63 
     64  async saveOrderPreference() {
     65    const tabs = [...this.toolboxTabsElement.querySelectorAll(".devtools-tab")];
     66    const tabIds = tabs.map(tab => tab.dataset.extensionId || tab.dataset.id);
     67    // Concat the overflowed tabs id since they are not contained in visible tabs.
     68    // The overflowed tabs cannot be reordered so we just append the id from current
     69    // panel definitions on their order.
     70    const overflowedTabIds = this.currentPanelDefinitions
     71      .filter(definition => !tabs.some(tab => tab.dataset.id === definition.id))
     72      .map(definition => definition.extensionId || definition.id);
     73    const currentTabIds = tabIds.concat(overflowedTabIds);
     74    const dragTargetId =
     75      this.dragTarget.dataset.extensionId || this.dragTarget.dataset.id;
     76    const prefIds = getTabsOrderFromPreference();
     77    const absoluteIds = toAbsoluteOrder(prefIds, currentTabIds, dragTargetId);
     78 
     79    // Remove panel id which is not in panel definitions and addons list.
     80    const extensions = await AddonManager.getAllAddons();
     81    const definitions = gDevTools.getToolDefinitionArray();
     82    const result = absoluteIds.filter(
     83      id =>
     84        definitions.find(d => id === (d.extensionId || d.id)) ||
     85        extensions.find(e => id === e.id)
     86    );
     87 
     88    Services.prefs.setCharPref(PREFERENCE_NAME, result.join(","));
     89  }
     90 
     91  setCurrentPanelDefinitions(currentPanelDefinitions) {
     92    this.currentPanelDefinitions = currentPanelDefinitions;
     93  }
     94 
     95  onMouseDown(e) {
     96    if (!e.target.classList.contains("devtools-tab")) {
     97      return;
     98    }
     99 
    100    this.dragStartX = e.pageX;
    101    this.dragTarget = e.target;
    102    this.previousPageX = e.pageX;
    103    this.toolboxContainerElement =
    104      this.dragTarget.closest("#toolbox-container");
    105    this.toolboxTabsElement = this.dragTarget.closest(".toolbox-tabs");
    106    this.isOrderUpdated = false;
    107    this.eventTarget = this.dragTarget.ownerGlobal.top;
    108 
    109    this.eventTarget.addEventListener("mousemove", this.onMouseMove);
    110    this.eventTarget.addEventListener("mouseup", this.onMouseUp);
    111 
    112    this.toolboxContainerElement.classList.add("tabs-reordering");
    113  }
    114 
    115  onMouseMove(e) {
    116    const diffPageX = e.pageX - this.previousPageX;
    117    let dragTargetCenterX =
    118      this.dragTarget.offsetLeft + diffPageX + this.dragTarget.clientWidth / 2;
    119    let isDragTargetPreviousSibling = false;
    120 
    121    const tabElements =
    122      this.toolboxTabsElement.querySelectorAll(".devtools-tab");
    123 
    124    // Calculate the minimum and maximum X-offset that can be valid for the drag target.
    125    const firstElement = tabElements[0];
    126    const firstElementCenterX =
    127      firstElement.offsetLeft + firstElement.clientWidth / 2;
    128    const lastElement = tabElements[tabElements.length - 1];
    129    const lastElementCenterX =
    130      lastElement.offsetLeft + lastElement.clientWidth / 2;
    131    const max = Math.max(firstElementCenterX, lastElementCenterX);
    132    const min = Math.min(firstElementCenterX, lastElementCenterX);
    133 
    134    // Normalize the target center X so to remain between the first and last tab.
    135    dragTargetCenterX = Math.min(max, dragTargetCenterX);
    136    dragTargetCenterX = Math.max(min, dragTargetCenterX);
    137 
    138    for (const tabElement of tabElements) {
    139      if (tabElement === this.dragTarget) {
    140        isDragTargetPreviousSibling = true;
    141        continue;
    142      }
    143 
    144      // Is the dragTarget near the center of the other tab?
    145      const anotherCenterX = tabElement.offsetLeft + tabElement.clientWidth / 2;
    146      const distanceWithDragTarget = Math.abs(
    147        dragTargetCenterX - anotherCenterX
    148      );
    149      const isReplaceable = distanceWithDragTarget < tabElement.clientWidth / 3;
    150 
    151      if (isReplaceable) {
    152        const replaceableElement = isDragTargetPreviousSibling
    153          ? tabElement.nextSibling
    154          : tabElement;
    155        this.insertBefore(replaceableElement);
    156        break;
    157      }
    158    }
    159 
    160    let distance = e.pageX - this.dragStartX;
    161 
    162    // To accomodate for RTL locales, we cannot rely on the first/last element of the
    163    // NodeList. We cannot have negative distances for the leftmost tab, and we cannot
    164    // have positive distances for the rightmost tab.
    165    const isFirstTab = this.isFirstTab(this.dragTarget);
    166    const isLastTab = this.isLastTab(this.dragTarget);
    167    const isLeftmostTab = this.isRTL() ? isLastTab : isFirstTab;
    168    const isRightmostTab = this.isRTL() ? isFirstTab : isLastTab;
    169 
    170    if ((isLeftmostTab && distance < 0) || (isRightmostTab && distance > 0)) {
    171      // If the drag target is already edge of the tabs and the mouse will make the
    172      // element to move to same direction more, keep the position.
    173      distance = 0;
    174    }
    175 
    176    this.dragTarget.style.left = `${distance}px`;
    177    this.previousPageX = e.pageX;
    178  }
    179 
    180  async onMouseUp() {
    181    if (!this.dragTarget) {
    182      // The case in here has two type:
    183      // 1. Although destroy method was called, it was not during reordering.
    184      // 2. Although mouse event occur, destroy method was called during reordering.
    185      return;
    186    }
    187 
    188    if (this.isOrderUpdated) {
    189      await this.saveOrderPreference();
    190 
    191      // Log which tabs reordered. The question we want to answer is:
    192      // "How frequently are the tabs re-ordered, also which tabs get re-ordered?"
    193      const toolId =
    194        this.dragTarget.dataset.extensionId || this.dragTarget.dataset.id;
    195      Glean.devtoolsToolbox.tabsReordered[toolId].add(1);
    196    }
    197 
    198    this.eventTarget.removeEventListener("mousemove", this.onMouseMove);
    199    this.eventTarget.removeEventListener("mouseup", this.onMouseUp);
    200 
    201    this.toolboxContainerElement.classList.remove("tabs-reordering");
    202    this.dragTarget.style.left = null;
    203    this.dragTarget = null;
    204    this.toolboxContainerElement = null;
    205    this.toolboxTabsElement = null;
    206    this.eventTarget = null;
    207  }
    208 }
    209 
    210 function getTabsOrderFromPreference() {
    211  const pref = Services.prefs.getCharPref(PREFERENCE_NAME, "");
    212  return pref ? pref.split(",") : [];
    213 }
    214 
    215 function sortPanelDefinitions(definitions) {
    216  const toolIds = getTabsOrderFromPreference();
    217 
    218  return definitions.sort((a, b) => {
    219    let orderA = toolIds.indexOf(a.extensionId || a.id);
    220    let orderB = toolIds.indexOf(b.extensionId || b.id);
    221    orderA = orderA < 0 ? Number.MAX_VALUE : orderA;
    222    orderB = orderB < 0 ? Number.MAX_VALUE : orderB;
    223    return orderA - orderB;
    224  });
    225 }
    226 
    227 /**
    228 * This function returns absolute tab ids that were merged the both ids that are in
    229 * preference and tabs.
    230 * Some tabs added with add-ons etc show/hide depending on conditions.
    231 * However, all of tabs that include hidden tab always keep the relationship with
    232 * left side tab, except in case the left tab was target of dragging. If the left
    233 * tab has been moved, it keeps its relationship with the tab next to it.
    234 *
    235 * Case 1: Drag a tab to left
    236 *   currentTabIds: [T1, T2, T3, T4, T5]
    237 *   prefIds      : [T1, T2, T3, E1(hidden), T4, T5]
    238 *   drag T4      : [T1, T2, T4, T3, T5]
    239 *   result       : [T1, T2, T4, T3, E1, T5]
    240 *
    241 * Case 2: Drag a tab to right
    242 *   currentTabIds: [T1, T2, T3, T4, T5]
    243 *   prefIds      : [T1, T2, T3, E1(hidden), T4, T5]
    244 *   drag T2      : [T1, T3, T4, T2, T5]
    245 *   result       : [T1, T3, E1, T4, T2, T5]
    246 *
    247 * Case 3: Hidden tab was left end and drag a tab to left end
    248 *   currentTabIds: [T1, T2, T3, T4, T5]
    249 *   prefIds      : [E1(hidden), T1, T2, T3, T4, T5]
    250 *   drag T4      : [T4, T1, T2, T3, T5]
    251 *   result       : [E1, T4, T1, T2, T3, T5]
    252 *
    253 * Case 4: Hidden tab was right end and drag a tab to right end
    254 *   currentTabIds: [T1, T2, T3, T4, T5]
    255 *   prefIds      : [T1, T2, T3, T4, T5, E1(hidden)]
    256 *   drag T1      : [T2, T3, T4, T5, T1]
    257 *   result       : [T2, T3, T4, T5, E1, T1]
    258 *
    259 * @param Array - prefIds: id array of preference
    260 * @param Array - currentTabIds: id array of appearanced tabs
    261 * @param String - dragTargetId: id of dragged target
    262 * @return Array
    263 */
    264 function toAbsoluteOrder(prefIds, currentTabIds, dragTargetId) {
    265  currentTabIds = [...currentTabIds];
    266  let indexAtCurrentTabs = 0;
    267 
    268  for (const prefId of prefIds) {
    269    if (prefId === dragTargetId) {
    270      // do nothing
    271    } else if (currentTabIds.includes(prefId)) {
    272      indexAtCurrentTabs = currentTabIds.indexOf(prefId) + 1;
    273    } else {
    274      currentTabIds.splice(indexAtCurrentTabs, 0, prefId);
    275      indexAtCurrentTabs += 1;
    276    }
    277  }
    278 
    279  return currentTabIds;
    280 }
    281 
    282 module.exports.ToolboxTabsOrderManager = ToolboxTabsOrderManager;
    283 module.exports.sortPanelDefinitions = sortPanelDefinitions;
    284 module.exports.toAbsoluteOrder = toAbsoluteOrder;