tor-browser

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

markup.js (90294B)


      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 flags = require("resource://devtools/shared/flags.js");
      8 const nodeConstants = require("resource://devtools/shared/dom-node-constants.js");
      9 const nodeFilterConstants = require("resource://devtools/shared/dom-node-filter-constants.js");
     10 const EventEmitter = require("resource://devtools/shared/event-emitter.js");
     11 const { LocalizationHelper } = require("resource://devtools/shared/l10n.js");
     12 const { PluralForm } = require("resource://devtools/shared/plural-form.js");
     13 const AutocompletePopup = require("resource://devtools/client/shared/autocomplete-popup.js");
     14 const KeyShortcuts = require("resource://devtools/client/shared/key-shortcuts.js");
     15 const { scrollIntoViewIfNeeded } = ChromeUtils.importESModule(
     16  "resource://devtools/client/shared/scroll.mjs"
     17 );
     18 const { PrefObserver } = require("resource://devtools/client/shared/prefs.js");
     19 const MarkupElementContainer = require("resource://devtools/client/inspector/markup/views/element-container.js");
     20 const MarkupReadOnlyContainer = require("resource://devtools/client/inspector/markup/views/read-only-container.js");
     21 const MarkupTextContainer = require("resource://devtools/client/inspector/markup/views/text-container.js");
     22 const RootContainer = require("resource://devtools/client/inspector/markup/views/root-container.js");
     23 const WalkerEventListener = require("resource://devtools/client/inspector/shared/walker-event-listener.js");
     24 
     25 loader.lazyRequireGetter(
     26  this,
     27  ["createDOMMutationBreakpoint", "deleteDOMMutationBreakpoint"],
     28  "resource://devtools/client/framework/actions/index.js",
     29  true
     30 );
     31 loader.lazyRequireGetter(
     32  this,
     33  "MarkupContextMenu",
     34  "resource://devtools/client/inspector/markup/markup-context-menu.js"
     35 );
     36 loader.lazyRequireGetter(
     37  this,
     38  "SlottedNodeContainer",
     39  "resource://devtools/client/inspector/markup/views/slotted-node-container.js"
     40 );
     41 loader.lazyRequireGetter(
     42  this,
     43  "getLongString",
     44  "resource://devtools/client/inspector/shared/utils.js",
     45  true
     46 );
     47 loader.lazyRequireGetter(
     48  this,
     49  "openContentLink",
     50  "resource://devtools/client/shared/link.js",
     51  true
     52 );
     53 loader.lazyRequireGetter(
     54  this,
     55  "HTMLTooltip",
     56  "resource://devtools/client/shared/widgets/tooltip/HTMLTooltip.js",
     57  true
     58 );
     59 loader.lazyRequireGetter(
     60  this,
     61  "UndoStack",
     62  "resource://devtools/client/shared/undo.js",
     63  true
     64 );
     65 loader.lazyRequireGetter(
     66  this,
     67  "clipboardHelper",
     68  "resource://devtools/shared/platform/clipboard.js"
     69 );
     70 loader.lazyRequireGetter(
     71  this,
     72  "beautify",
     73  "resource://devtools/shared/jsbeautify/beautify.js"
     74 );
     75 loader.lazyRequireGetter(
     76  this,
     77  "getTabPrefs",
     78  "resource://devtools/shared/indentation.js",
     79  true
     80 );
     81 
     82 const INSPECTOR_L10N = new LocalizationHelper(
     83  "devtools/client/locales/inspector.properties"
     84 );
     85 
     86 // Page size for pageup/pagedown
     87 const PAGE_SIZE = 10;
     88 const DEFAULT_MAX_CHILDREN = 100;
     89 const DRAG_DROP_AUTOSCROLL_EDGE_MAX_DISTANCE = 50;
     90 const DRAG_DROP_AUTOSCROLL_EDGE_RATIO = 0.1;
     91 const DRAG_DROP_MIN_AUTOSCROLL_SPEED = 2;
     92 const DRAG_DROP_MAX_AUTOSCROLL_SPEED = 8;
     93 const DRAG_DROP_HEIGHT_TO_SPEED = 500;
     94 const DRAG_DROP_HEIGHT_TO_SPEED_MIN = 0.5;
     95 const DRAG_DROP_HEIGHT_TO_SPEED_MAX = 1;
     96 const ATTR_COLLAPSE_ENABLED_PREF = "devtools.markup.collapseAttributes";
     97 const ATTR_COLLAPSE_LENGTH_PREF = "devtools.markup.collapseAttributeLength";
     98 const BEAUTIFY_HTML_ON_COPY_PREF = "devtools.markup.beautifyOnCopy";
     99 
    100 /**
    101 * These functions are called when a shortcut (as defined in `_initShortcuts`) occurs.
    102 * Each property in the following object corresponds to one of the shortcut that is
    103 * handled by the markup-view.
    104 * Each property value is a function that takes the markup-view instance as only
    105 * argument, and returns a boolean that signifies whether the event should be consumed.
    106 * By default, the event gets consumed after the shortcut handler returns,
    107 * this means its propagation is stopped. If you do want the shortcut event
    108 * to continue propagating through DevTools, then return true from the handler.
    109 */
    110 const shortcutHandlers = {
    111  // Localizable keys
    112  "markupView.hide.key": markupView => {
    113    const node = markupView._selectedContainer.node;
    114    const walkerFront = node.walkerFront;
    115 
    116    if (node.hidden) {
    117      walkerFront.unhideNode(node);
    118    } else {
    119      walkerFront.hideNode(node);
    120    }
    121  },
    122  "markupView.edit.key": markupView => {
    123    markupView.beginEditingHTML(markupView._selectedContainer.node);
    124  },
    125  "markupView.scrollInto.key": markupView => {
    126    markupView.scrollNodeIntoView();
    127  },
    128  // Generic keys
    129  Delete: markupView => {
    130    markupView.deleteNodeOrAttribute();
    131  },
    132  Backspace: markupView => {
    133    markupView.deleteNodeOrAttribute(true);
    134  },
    135  Home: markupView => {
    136    const rootContainer = markupView.getContainer(markupView._rootNode);
    137    markupView.navigate(rootContainer.children.firstChild.container);
    138  },
    139  Left: markupView => {
    140    if (markupView._selectedContainer.expanded) {
    141      markupView.collapseNode(markupView._selectedContainer.node);
    142    } else {
    143      const parent = markupView._selectionWalker().parentNode();
    144      if (parent) {
    145        markupView.navigate(parent.container);
    146      }
    147    }
    148  },
    149  Right: markupView => {
    150    if (
    151      !markupView._selectedContainer.expanded &&
    152      markupView._selectedContainer.hasChildren
    153    ) {
    154      markupView._expandContainer(markupView._selectedContainer);
    155    } else {
    156      const next = markupView._selectionWalker().nextNode();
    157      if (next) {
    158        markupView.navigate(next.container);
    159      }
    160    }
    161  },
    162  Up: markupView => {
    163    const previousNode = markupView._selectionWalker().previousNode();
    164    if (previousNode) {
    165      markupView.navigate(previousNode.container);
    166    }
    167  },
    168  Down: markupView => {
    169    const nextNode = markupView._selectionWalker().nextNode();
    170    if (nextNode) {
    171      markupView.navigate(nextNode.container);
    172    }
    173  },
    174  PageUp: markupView => {
    175    const walker = markupView._selectionWalker();
    176    let selection = markupView._selectedContainer;
    177    for (let i = 0; i < PAGE_SIZE; i++) {
    178      const previousNode = walker.previousNode();
    179      if (!previousNode) {
    180        break;
    181      }
    182      selection = previousNode.container;
    183    }
    184    markupView.navigate(selection);
    185  },
    186  PageDown: markupView => {
    187    const walker = markupView._selectionWalker();
    188    let selection = markupView._selectedContainer;
    189    for (let i = 0; i < PAGE_SIZE; i++) {
    190      const nextNode = walker.nextNode();
    191      if (!nextNode) {
    192        break;
    193      }
    194      selection = nextNode.container;
    195    }
    196    markupView.navigate(selection);
    197  },
    198  Enter: markupView => {
    199    if (!markupView._selectedContainer.canFocus) {
    200      markupView._selectedContainer.canFocus = true;
    201      markupView._selectedContainer.focus();
    202      return false;
    203    }
    204    return true;
    205  },
    206  Space: markupView => {
    207    if (!markupView._selectedContainer.canFocus) {
    208      markupView._selectedContainer.canFocus = true;
    209      markupView._selectedContainer.focus();
    210      return false;
    211    }
    212    return true;
    213  },
    214  Esc: markupView => {
    215    if (markupView.isDragging) {
    216      markupView.cancelDragging();
    217      return false;
    218    }
    219    // Prevent cancelling the event when not
    220    // dragging, to allow the split console to be toggled.
    221    return true;
    222  },
    223 };
    224 
    225 /**
    226 * Vocabulary for the purposes of this file:
    227 *
    228 * MarkupContainer - the structure that holds an editor and its
    229 *  immediate children in the markup panel.
    230 *  - MarkupElementContainer: markup container for element nodes
    231 *  - MarkupTextContainer: markup container for text / comment nodes
    232 *  - MarkupReadonlyContainer: markup container for other nodes
    233 * Node - A content node.
    234 * object.elt - A UI element in the markup panel.
    235 */
    236 
    237 /**
    238 * The markup tree.  Manages the mapping of nodes to MarkupContainers,
    239 * updating based on mutations, and the undo/redo bindings.
    240 */
    241 class MarkupView extends EventEmitter {
    242  /**
    243   * @param  {Inspector} inspector
    244   *         The inspector we're watching.
    245   * @param  {iframe} frame
    246   *         An iframe in which the caller has kindly loaded markup.xhtml.
    247   * @param  {XULWindow} controllerWindow
    248   *         Will enable the undo/redo feature from devtools/client/shared/undo.
    249   *         Should be a XUL window, will typically point to the toolbox window.
    250   */
    251  constructor(inspector, frame, controllerWindow) {
    252    super();
    253 
    254    this.controllerWindow = controllerWindow;
    255    this.inspector = inspector;
    256    this.highlighters = inspector.highlighters;
    257    this.walker = this.inspector.walker;
    258    this._frame = frame;
    259    this.win = this._frame.contentWindow;
    260    this.doc = this._frame.contentDocument;
    261    this._elt = this.doc.getElementById("root");
    262    this.telemetry = this.inspector.telemetry;
    263    this._breakpointIDsInLocalState = new Map();
    264    this._containersToUpdate = new Map();
    265 
    266    this.maxChildren = Services.prefs.getIntPref(
    267      "devtools.markup.pagesize",
    268      DEFAULT_MAX_CHILDREN
    269    );
    270 
    271    this.collapseAttributes = Services.prefs.getBoolPref(
    272      ATTR_COLLAPSE_ENABLED_PREF
    273    );
    274    this.collapseAttributeLength = Services.prefs.getIntPref(
    275      ATTR_COLLAPSE_LENGTH_PREF
    276    );
    277 
    278    // Creating the popup to be used to show CSS suggestions.
    279    // The popup will be attached to the toolbox document.
    280    this.popup = new AutocompletePopup(inspector.toolbox.doc, {
    281      autoSelect: true,
    282    });
    283 
    284    this._containers = new Map();
    285    // This weakmap will hold keys used with the _containers map, in order to retrieve the
    286    // slotted container for a given node front.
    287    this._slottedContainerKeys = new WeakMap();
    288 
    289    // Binding functions that need to be called in scope.
    290    this._handleRejectionIfNotDestroyed =
    291      this._handleRejectionIfNotDestroyed.bind(this);
    292    this._isImagePreviewTarget = this._isImagePreviewTarget.bind(this);
    293    this._onWalkerMutations = this._onWalkerMutations.bind(this);
    294    this._onBlur = this._onBlur.bind(this);
    295    this._onContextMenu = this._onContextMenu.bind(this);
    296    this._onCopy = this._onCopy.bind(this);
    297    this._onCollapseAttributesPrefChange =
    298      this._onCollapseAttributesPrefChange.bind(this);
    299    this._onWalkerNodeStatesChanged =
    300      this._onWalkerNodeStatesChanged.bind(this);
    301    this._onFocus = this._onFocus.bind(this);
    302    this._onResourceAvailable = this._onResourceAvailable.bind(this);
    303    this._onTargetAvailable = this._onTargetAvailable.bind(this);
    304    this._onTargetDestroyed = this._onTargetDestroyed.bind(this);
    305    this._onMouseClick = this._onMouseClick.bind(this);
    306    this._onMouseMove = this._onMouseMove.bind(this);
    307    this._onMouseOut = this._onMouseOut.bind(this);
    308    this._onMouseUp = this._onMouseUp.bind(this);
    309    this._onNewSelection = this._onNewSelection.bind(this);
    310    this._onToolboxPickerCanceled = this._onToolboxPickerCanceled.bind(this);
    311    this._onToolboxPickerHover = this._onToolboxPickerHover.bind(this);
    312    this._onDomMutation = this._onDomMutation.bind(this);
    313    this._updateSearchResultsHighlightingInSelectedNode =
    314      this._updateSearchResultsHighlightingInSelectedNode.bind(this);
    315    this._onToolboxSelect = this._onToolboxSelect.bind(this);
    316 
    317    // Listening to various events.
    318    this._elt.addEventListener("blur", this._onBlur, true);
    319    this._elt.addEventListener("click", this._onMouseClick);
    320    this._elt.addEventListener("contextmenu", this._onContextMenu);
    321    this._elt.addEventListener("mousemove", this._onMouseMove);
    322    this._elt.addEventListener("mouseout", this._onMouseOut);
    323    this._frame.addEventListener("focus", this._onFocus);
    324    this.inspector.selection.on("new-node-front", this._onNewSelection);
    325    this.inspector.on(
    326      "search-cleared",
    327      this._updateSearchResultsHighlightingInSelectedNode
    328    );
    329    this._unsubscribeFromToolboxStore = this.inspector.toolbox.store.subscribe(
    330      this._onDomMutation
    331    );
    332 
    333    if (flags.testing) {
    334      // In tests, we start listening immediately to avoid having to simulate a mousemove.
    335      this._initTooltips();
    336    }
    337 
    338    this.win.addEventListener("copy", this._onCopy);
    339    this.win.addEventListener("mouseup", this._onMouseUp);
    340    this.inspector.toolbox.nodePicker.on(
    341      "picker-node-canceled",
    342      this._onToolboxPickerCanceled
    343    );
    344    this.inspector.toolbox.nodePicker.on(
    345      "picker-node-hovered",
    346      this._onToolboxPickerHover
    347    );
    348 
    349    // Event listeners for highlighter events
    350    this.onHighlighterShown = data =>
    351      this.handleHighlighterEvent("highlighter-shown", data);
    352    this.onHighlighterHidden = data =>
    353      this.handleHighlighterEvent("highlighter-hidden", data);
    354    this.inspector.highlighters.on(
    355      "highlighter-shown",
    356      this.onHighlighterShown
    357    );
    358    this.inspector.highlighters.on(
    359      "highlighter-hidden",
    360      this.onHighlighterHidden
    361    );
    362    this.inspector.toolbox.once("select", this._onToolboxSelect);
    363 
    364    this._onNewSelection(this.inspector.selection.nodeFront);
    365    if (this.inspector.selection.nodeFront) {
    366      this.expandNode(this.inspector.selection.nodeFront);
    367    }
    368 
    369    this._prefObserver = new PrefObserver("devtools.markup");
    370    this._prefObserver.on(
    371      ATTR_COLLAPSE_ENABLED_PREF,
    372      this._onCollapseAttributesPrefChange
    373    );
    374    this._prefObserver.on(
    375      ATTR_COLLAPSE_LENGTH_PREF,
    376      this._onCollapseAttributesPrefChange
    377    );
    378 
    379    this._initShortcuts();
    380 
    381    this._walkerEventListener = new WalkerEventListener(this.inspector, {
    382      "anchor-name-change": this._onWalkerNodeStatesChanged,
    383      "container-type-change": this._onWalkerNodeStatesChanged,
    384      "display-change": this._onWalkerNodeStatesChanged,
    385      "scrollable-change": this._onWalkerNodeStatesChanged,
    386      "overflow-change": this._onWalkerNodeStatesChanged,
    387      mutations: this._onWalkerMutations,
    388    });
    389 
    390    this.resourceCommand = this.inspector.commands.resourceCommand;
    391    this.resourceCommand.watchResources(
    392      [this.resourceCommand.TYPES.ROOT_NODE],
    393      {
    394        onAvailable: this._onResourceAvailable,
    395      }
    396    );
    397 
    398    this.targetCommand = this.inspector.commands.targetCommand;
    399    this.targetCommand.watchTargets({
    400      types: [this.targetCommand.TYPES.FRAME],
    401      onAvailable: this._onTargetAvailable,
    402      onDestroyed: this._onTargetDestroyed,
    403    });
    404  }
    405 
    406  /**
    407   * How long does a node flash when it mutates (in ms).
    408   */
    409  CONTAINER_FLASHING_DURATION = 500;
    410 
    411  _selectedContainer = null;
    412 
    413  get contextMenu() {
    414    if (!this._contextMenu) {
    415      this._contextMenu = new MarkupContextMenu(this);
    416    }
    417 
    418    return this._contextMenu;
    419  }
    420 
    421  hasEventDetailsTooltip() {
    422    return !!this._eventDetailsTooltip;
    423  }
    424 
    425  get eventDetailsTooltip() {
    426    if (!this._eventDetailsTooltip) {
    427      // This tooltip will be attached to the toolbox document.
    428      this._eventDetailsTooltip = new HTMLTooltip(this.toolbox.doc, {
    429        type: "arrow",
    430        consumeOutsideClicks: false,
    431      });
    432    }
    433 
    434    return this._eventDetailsTooltip;
    435  }
    436 
    437  get toolbox() {
    438    return this.inspector.toolbox;
    439  }
    440 
    441  get undo() {
    442    if (!this._undo) {
    443      this._undo = new UndoStack();
    444      this._undo.installController(this.controllerWindow);
    445    }
    446 
    447    return this._undo;
    448  }
    449 
    450  _onDomMutation() {
    451    const domMutationBreakpoints =
    452      this.inspector.toolbox.store.getState().domMutationBreakpoints
    453        .breakpoints;
    454    const breakpointIDsInCurrentState = [];
    455    for (const breakpoint of domMutationBreakpoints) {
    456      const nodeFront = breakpoint.nodeFront;
    457      const mutationType = breakpoint.mutationType;
    458      const enabledStatus = breakpoint.enabled;
    459      breakpointIDsInCurrentState.push(breakpoint.id);
    460      // If breakpoint is not in local state
    461      if (!this._breakpointIDsInLocalState.has(breakpoint.id)) {
    462        this._breakpointIDsInLocalState.set(breakpoint.id, breakpoint);
    463        if (!this._containersToUpdate.has(nodeFront)) {
    464          this._containersToUpdate.set(nodeFront, new Map());
    465        }
    466      }
    467      this._containersToUpdate.get(nodeFront).set(mutationType, enabledStatus);
    468    }
    469    // If a breakpoint is in local state but not current state, it has been
    470    // removed by the user.
    471    for (const id of this._breakpointIDsInLocalState.keys()) {
    472      if (breakpointIDsInCurrentState.includes(id) === false) {
    473        const nodeFront = this._breakpointIDsInLocalState.get(id).nodeFront;
    474        const mutationType =
    475          this._breakpointIDsInLocalState.get(id).mutationType;
    476        this._containersToUpdate.get(nodeFront).delete(mutationType);
    477        this._breakpointIDsInLocalState.delete(id);
    478      }
    479    }
    480    // Update each container
    481    for (const nodeFront of this._containersToUpdate.keys()) {
    482      const mutationBreakpoints = this._containersToUpdate.get(nodeFront);
    483      const container = this.getContainer(nodeFront);
    484      container.update(mutationBreakpoints);
    485      if (this._containersToUpdate.get(nodeFront).size === 0) {
    486        this._containersToUpdate.delete(nodeFront);
    487      }
    488    }
    489  }
    490 
    491  /**
    492   * Handle promise rejections for various asynchronous actions, and only log errors if
    493   * the markup view still exists.
    494   * This is useful to silence useless errors that happen when the markup view is
    495   * destroyed while still initializing (and making protocol requests).
    496   */
    497  _handleRejectionIfNotDestroyed(e) {
    498    if (!this._destroyed) {
    499      console.error(e);
    500    }
    501  }
    502 
    503  _initTooltips() {
    504    if (this.imagePreviewTooltip) {
    505      return;
    506    }
    507    // The tooltips will be attached to the toolbox document.
    508    this.imagePreviewTooltip = new HTMLTooltip(this.toolbox.doc, {
    509      type: "arrow",
    510      useXulWrapper: true,
    511    });
    512    this._enableImagePreviewTooltip();
    513  }
    514 
    515  _enableImagePreviewTooltip() {
    516    if (!this.imagePreviewTooltip) {
    517      return;
    518    }
    519    this.imagePreviewTooltip.startTogglingOnHover(
    520      this._elt,
    521      this._isImagePreviewTarget
    522    );
    523  }
    524 
    525  _disableImagePreviewTooltip() {
    526    if (!this.imagePreviewTooltip) {
    527      return;
    528    }
    529    this.imagePreviewTooltip.stopTogglingOnHover();
    530  }
    531 
    532  _onToolboxPickerHover(nodeFront) {
    533    this.showNode(nodeFront).then(() => {
    534      this._showNodeAsHovered(nodeFront);
    535    }, console.error);
    536  }
    537 
    538  /**
    539   * If the element picker gets canceled, make sure and re-center the view on the
    540   * current selected element.
    541   */
    542  _onToolboxPickerCanceled() {
    543    if (this._selectedContainer) {
    544      scrollIntoViewIfNeeded(this._selectedContainer.editor.elt);
    545    }
    546  }
    547 
    548  _onToolboxSelect(id) {
    549    if (id !== "inspector") {
    550      return;
    551    }
    552 
    553    // If the inspector was opened from the "Inspect" context menu, the node gets selected
    554    // in the MarkupView constructor, but the Toolbox focuses the Inspector iframe once
    555    // the tool is loaded (and the iframe is actually visible), so we need to focus
    556    // the selected node after the inspector was properly selected and focused (See Bug 1979591).
    557    if (this.inspector.selection?.reason === "browser-context-menu") {
    558      this.maybeNavigateToNewSelection();
    559    }
    560  }
    561 
    562  isDragging = false;
    563  _draggedContainer = null;
    564 
    565  _onMouseMove(event) {
    566    // Note that in tests, we start listening immediately from the constructor to avoid having to simulate a mousemove.
    567    // Also note that initTooltips bails out if it is called many times, so it isn't an issue to call it a second
    568    // time from here in case tests are doing a mousemove.
    569    this._initTooltips();
    570 
    571    let target = event.target;
    572 
    573    if (this._draggedContainer) {
    574      this._draggedContainer.onMouseMove(event);
    575    }
    576    // Auto-scroll if we're dragging.
    577    if (this.isDragging) {
    578      event.preventDefault();
    579      this._autoScroll(event);
    580      return;
    581    }
    582 
    583    // Show the current container as hovered and highlight it.
    584    // This requires finding the current MarkupContainer (walking up the DOM).
    585    while (!target.container) {
    586      if (target.tagName.toLowerCase() === "body") {
    587        return;
    588      }
    589      target = target.parentNode;
    590    }
    591 
    592    const container = target.container;
    593    if (this._hoveredContainer !== container) {
    594      this._showBoxModel(container.node);
    595    }
    596    this._showContainerAsHovered(container);
    597 
    598    this.emit("node-hover");
    599  }
    600 
    601  /**
    602   * If focus is moved outside of the markup view document and there is a
    603   * selected container, make its contents not focusable by a keyboard.
    604   */
    605  _onBlur(event) {
    606    if (!this._selectedContainer) {
    607      return;
    608    }
    609 
    610    const { relatedTarget } = event;
    611    if (relatedTarget && relatedTarget.ownerDocument === this.doc) {
    612      return;
    613    }
    614 
    615    if (this._selectedContainer) {
    616      this._selectedContainer.clearFocus();
    617    }
    618  }
    619 
    620  _onContextMenu(event) {
    621    this.contextMenu.show(event);
    622  }
    623 
    624  /**
    625   * Executed on each mouse-move while a node is being dragged in the view.
    626   * Auto-scrolls the view to reveal nodes below the fold to drop the dragged
    627   * node in.
    628   */
    629  _autoScroll(event) {
    630    const docEl = this.doc.documentElement;
    631 
    632    if (this._autoScrollAnimationFrame) {
    633      this.win.cancelAnimationFrame(this._autoScrollAnimationFrame);
    634    }
    635 
    636    // Auto-scroll when the mouse approaches top/bottom edge.
    637    const fromBottom = docEl.clientHeight - event.pageY + this.win.scrollY;
    638    const fromTop = event.pageY - this.win.scrollY;
    639    const edgeDistance = Math.min(
    640      DRAG_DROP_AUTOSCROLL_EDGE_MAX_DISTANCE,
    641      docEl.clientHeight * DRAG_DROP_AUTOSCROLL_EDGE_RATIO
    642    );
    643 
    644    // The smaller the screen, the slower the movement.
    645    const heightToSpeedRatio = Math.max(
    646      DRAG_DROP_HEIGHT_TO_SPEED_MIN,
    647      Math.min(
    648        DRAG_DROP_HEIGHT_TO_SPEED_MAX,
    649        docEl.clientHeight / DRAG_DROP_HEIGHT_TO_SPEED
    650      )
    651    );
    652 
    653    if (fromBottom <= edgeDistance) {
    654      // Map our distance range to a speed range so that the speed is not too
    655      // fast or too slow.
    656      const speed = map(
    657        fromBottom,
    658        0,
    659        edgeDistance,
    660        DRAG_DROP_MIN_AUTOSCROLL_SPEED,
    661        DRAG_DROP_MAX_AUTOSCROLL_SPEED
    662      );
    663 
    664      this._runUpdateLoop(() => {
    665        docEl.scrollTop -=
    666          heightToSpeedRatio * (speed - DRAG_DROP_MAX_AUTOSCROLL_SPEED);
    667      });
    668    }
    669 
    670    if (fromTop <= edgeDistance) {
    671      const speed = map(
    672        fromTop,
    673        0,
    674        edgeDistance,
    675        DRAG_DROP_MIN_AUTOSCROLL_SPEED,
    676        DRAG_DROP_MAX_AUTOSCROLL_SPEED
    677      );
    678 
    679      this._runUpdateLoop(() => {
    680        docEl.scrollTop +=
    681          heightToSpeedRatio * (speed - DRAG_DROP_MAX_AUTOSCROLL_SPEED);
    682      });
    683    }
    684  }
    685 
    686  /**
    687   * Run a loop on the requestAnimationFrame.
    688   */
    689  _runUpdateLoop(update) {
    690    const loop = () => {
    691      update();
    692      this._autoScrollAnimationFrame = this.win.requestAnimationFrame(loop);
    693    };
    694    loop();
    695  }
    696 
    697  _onMouseClick(event) {
    698    // From the target passed here, let's find the parent MarkupContainer
    699    // and forward the event if needed.
    700    let parentNode = event.target;
    701    let container;
    702    while (parentNode !== this.doc.body) {
    703      if (parentNode.container) {
    704        container = parentNode.container;
    705        break;
    706      }
    707      parentNode = parentNode.parentNode;
    708    }
    709 
    710    if (typeof container.onContainerClick === "function") {
    711      // Forward the event to the container if it implements onContainerClick.
    712      container.onContainerClick(event);
    713    }
    714  }
    715 
    716  _onMouseUp(event) {
    717    if (this._draggedContainer) {
    718      this._draggedContainer.onMouseUp(event);
    719    }
    720 
    721    this.indicateDropTarget(null);
    722    this.indicateDragTarget(null);
    723    if (this._autoScrollAnimationFrame) {
    724      this.win.cancelAnimationFrame(this._autoScrollAnimationFrame);
    725    }
    726  }
    727 
    728  _onCollapseAttributesPrefChange() {
    729    this.collapseAttributes = Services.prefs.getBoolPref(
    730      ATTR_COLLAPSE_ENABLED_PREF
    731    );
    732    this.collapseAttributeLength = Services.prefs.getIntPref(
    733      ATTR_COLLAPSE_LENGTH_PREF
    734    );
    735    this.update();
    736  }
    737 
    738  cancelDragging() {
    739    if (!this.isDragging) {
    740      return;
    741    }
    742 
    743    for (const [, container] of this._containers) {
    744      if (container.isDragging) {
    745        container.cancelDragging();
    746        break;
    747      }
    748    }
    749 
    750    this.indicateDropTarget(null);
    751    this.indicateDragTarget(null);
    752    if (this._autoScrollAnimationFrame) {
    753      this.win.cancelAnimationFrame(this._autoScrollAnimationFrame);
    754    }
    755  }
    756 
    757  _hoveredContainer = null;
    758 
    759  /**
    760   * Show a NodeFront's container as being hovered
    761   *
    762   * @param  {NodeFront} nodeFront
    763   *         The node to show as hovered
    764   */
    765  _showNodeAsHovered(nodeFront) {
    766    const container = this.getContainer(nodeFront);
    767    this._showContainerAsHovered(container);
    768  }
    769 
    770  _showContainerAsHovered(container) {
    771    if (this._hoveredContainer === container) {
    772      return;
    773    }
    774 
    775    if (this._hoveredContainer) {
    776      this._hoveredContainer.hovered = false;
    777    }
    778 
    779    container.hovered = true;
    780    this._hoveredContainer = container;
    781  }
    782 
    783  async _onMouseOut(event) {
    784    // Emulate mouseleave by skipping any relatedTarget inside the markup-view.
    785    if (this._elt.contains(event.relatedTarget)) {
    786      return;
    787    }
    788 
    789    if (this._autoScrollAnimationFrame) {
    790      this.win.cancelAnimationFrame(this._autoScrollAnimationFrame);
    791    }
    792    if (this.isDragging) {
    793      return;
    794    }
    795 
    796    await this._hideBoxModel();
    797    if (this._hoveredContainer) {
    798      this._hoveredContainer.hovered = false;
    799    }
    800    this._hoveredContainer = null;
    801 
    802    this.emit("leave");
    803  }
    804 
    805  /**
    806   * Show the Box Model Highlighter on a given node front
    807   *
    808   * @param  {NodeFront} nodeFront
    809   *         The node for which to show the highlighter.
    810   * @param  {object} options
    811   *         Configuration object with options for the Box Model Highlighter.
    812   * @return {Promise} Resolves after the highlighter for this nodeFront is shown.
    813   */
    814  _showBoxModel(nodeFront, options) {
    815    return this.inspector.highlighters.showHighlighterTypeForNode(
    816      this.inspector.highlighters.TYPES.BOXMODEL,
    817      nodeFront,
    818      options
    819    );
    820  }
    821 
    822  /**
    823   * Hide the Box Model Highlighter for any node that may be highlighted.
    824   *
    825   * @return {Promise} Resolves when the highlighter is hidden.
    826   */
    827  _hideBoxModel() {
    828    return this.inspector.highlighters.hideHighlighterType(
    829      this.inspector.highlighters.TYPES.BOXMODEL
    830    );
    831  }
    832 
    833  /**
    834   * Delegate handler for highlighter events.
    835   *
    836   * This is the place to observe for highlighter events, check the highlighter type and
    837   * event name, then react for example by modifying the DOM.
    838   *
    839   * @param {string} eventName
    840   *        Highlighter event name. One of: "highlighter-hidden", "highlighter-shown"
    841   * @param {object} data
    842   *        Object with data associated with the highlighter event.
    843   *        {String} data.type
    844   *        Highlighter type
    845   *        {NodeFront} data.nodeFront
    846   *        NodeFront of the node associated with the highlighter event
    847   *        {Object} data.options
    848   *        Optional configuration passed to the highlighter when shown
    849   *        {CustomHighlighterFront} data.highlighter
    850   *        Highlighter instance
    851   */
    852  handleHighlighterEvent(eventName, data) {
    853    switch (data.type) {
    854      // Toggle the "active" CSS class name on flex and grid display badges next to
    855      // elements in the Markup view when a coresponding flex or grid highlighter is
    856      // shown or hidden for a node.
    857      case this.inspector.highlighters.TYPES.FLEXBOX:
    858      case this.inspector.highlighters.TYPES.GRID: {
    859        const { nodeFront } = data;
    860        if (!nodeFront) {
    861          return;
    862        }
    863 
    864        // Find the badge corresponding to the node from the highlighter event payload.
    865        const container = this.getContainer(nodeFront);
    866        const badge = container?.editor?.displayBadge;
    867        if (badge) {
    868          const isActive = eventName == "highlighter-shown";
    869          badge.classList.toggle("active", isActive);
    870          badge.setAttribute("aria-pressed", isActive);
    871        }
    872 
    873        // There is a limit to how many grid highlighters can be active at the same time.
    874        // If the limit was reached, disable all non-active grid badges.
    875        if (data.type === this.inspector.highlighters.TYPES.GRID) {
    876          // Matches badges for "grid", "inline-grid" and "subgrid"
    877          const selector = "[data-display*='grid']:not(.active)";
    878          const isLimited =
    879            this.inspector.highlighters.isGridHighlighterLimitReached();
    880          Array.from(this._elt.querySelectorAll(selector)).map(el => {
    881            el.classList.toggle("interactive", !isLimited);
    882          });
    883        }
    884        break;
    885      }
    886    }
    887  }
    888 
    889  /**
    890   * Used by tests
    891   */
    892  getSelectedContainer() {
    893    return this._selectedContainer;
    894  }
    895 
    896  /**
    897   * Get the MarkupContainer object for a given node, or undefined if
    898   * none exists.
    899   *
    900   * @param  {NodeFront} nodeFront
    901   *         The node to get the container for.
    902   * @param  {boolean} slotted
    903   *         true to get the slotted version of the container.
    904   * @return {MarkupContainer} The container for the provided node.
    905   */
    906  getContainer(node, slotted) {
    907    const key = this._getContainerKey(node, slotted);
    908    return this._containers.get(key);
    909  }
    910 
    911  /**
    912   * Register a given container for a given node/slotted node.
    913   *
    914   * @param  {NodeFront} nodeFront
    915   *         The node to set the container for.
    916   * @param  {boolean} slotted
    917   *         true if the container represents the slotted version of the node.
    918   */
    919  setContainer(node, container, slotted) {
    920    const key = this._getContainerKey(node, slotted);
    921    return this._containers.set(key, container);
    922  }
    923 
    924  /**
    925   * Check if a MarkupContainer object exists for a given node/slotted node
    926   *
    927   * @param  {NodeFront} nodeFront
    928   *         The node to check.
    929   * @param  {boolean} slotted
    930   *         true to check for a container matching the slotted version of the node.
    931   * @return {boolean} True if a container exists, false otherwise.
    932   */
    933  hasContainer(node, slotted) {
    934    const key = this._getContainerKey(node, slotted);
    935    return this._containers.has(key);
    936  }
    937 
    938  _getContainerKey(node, slotted) {
    939    if (!slotted) {
    940      return node;
    941    }
    942 
    943    if (!this._slottedContainerKeys.has(node)) {
    944      this._slottedContainerKeys.set(node, { node });
    945    }
    946    return this._slottedContainerKeys.get(node);
    947  }
    948 
    949  _isContainerSelected(container) {
    950    if (!container) {
    951      return false;
    952    }
    953 
    954    const selection = this.inspector.selection;
    955    return (
    956      container.node == selection.nodeFront &&
    957      container.isSlotted() == selection.isSlotted()
    958    );
    959  }
    960 
    961  update() {
    962    const updateChildren = node => {
    963      this.getContainer(node).update();
    964      for (const child of node.treeChildren()) {
    965        updateChildren(child);
    966      }
    967    };
    968 
    969    // Start with the documentElement
    970    let documentElement;
    971    for (const node of this._rootNode.treeChildren()) {
    972      if (node.isDocumentElement === true) {
    973        documentElement = node;
    974        break;
    975      }
    976    }
    977 
    978    // Recursively update each node starting with documentElement.
    979    updateChildren(documentElement);
    980  }
    981 
    982  /**
    983   * Executed when the mouse hovers over a target in the markup-view and is used
    984   * to decide whether this target should be used to display an image preview
    985   * tooltip.
    986   * Delegates the actual decision to the corresponding MarkupContainer instance
    987   * if one is found.
    988   *
    989   * @return {Promise} the promise returned by
    990   *         MarkupElementContainer._isImagePreviewTarget
    991   */
    992  async _isImagePreviewTarget(target) {
    993    // From the target passed here, let's find the parent MarkupContainer
    994    // and ask it if the tooltip should be shown
    995    if (this.isDragging) {
    996      return false;
    997    }
    998 
    999    let parent = target,
   1000      container;
   1001    while (parent) {
   1002      if (parent.container) {
   1003        container = parent.container;
   1004        break;
   1005      }
   1006      parent = parent.parentNode;
   1007    }
   1008 
   1009    if (container instanceof MarkupElementContainer) {
   1010      return container.isImagePreviewTarget(target, this.imagePreviewTooltip);
   1011    }
   1012 
   1013    return false;
   1014  }
   1015 
   1016  /**
   1017   * Given the known reason, should the current selection be briefly highlighted
   1018   * In a few cases, we don't want to highlight the node:
   1019   * - If the reason is null (used to reset the selection),
   1020   * - if it's "inspector-default-selection" (initial node selected, either when
   1021   *   opening the inspector or after a navigation/reload)
   1022   * - if it's "picker-node-picked" or "picker-node-previewed" (node selected with the
   1023   *   node picker. Note that this does not include the "Inspect element" context menu,
   1024   *   which has a dedicated reason, "browser-context-menu").
   1025   * - if it's "test" (this is a special case for mochitest. In tests, we often
   1026   * need to select elements but don't necessarily want the highlighter to come
   1027   * and go after a delay as this might break test scenarios)
   1028   * We also do not want to start a brief highlight timeout if the node is
   1029   * already being hovered over, since in that case it will already be
   1030   * highlighted.
   1031   */
   1032  _shouldNewSelectionBeHighlighted() {
   1033    const reason = this.inspector.selection.reason;
   1034    const unwantedReasons = [
   1035      "inspector-default-selection",
   1036      "nodeselected",
   1037      "picker-node-picked",
   1038      "picker-node-previewed",
   1039      "test",
   1040    ];
   1041 
   1042    const isHighlight = this._isContainerSelected(this._hoveredContainer);
   1043    return !isHighlight && reason && !unwantedReasons.includes(reason);
   1044  }
   1045 
   1046  /**
   1047   * React to new-node-front selection events.
   1048   * Highlights the node if needed, and make sure it is shown and selected in
   1049   * the view.
   1050   * Note that this might be called when the panel is initialized to properly setup
   1051   * all the listeners.
   1052   *
   1053   * @param {NodeFront|undefined} nodeFront
   1054   * @param {string | undefined} reason
   1055   */
   1056  _onNewSelection(nodeFront, reason) {
   1057    const selection = this.inspector.selection;
   1058    // this will probably leak.
   1059    // TODO: use resource api listeners?
   1060    if (nodeFront) {
   1061      nodeFront.walkerFront.on(
   1062        "anchor-name-change",
   1063        this._onWalkerNodeStatesChanged
   1064      );
   1065      nodeFront.walkerFront.on(
   1066        "container-type-change",
   1067        this._onWalkerNodeStatesChanged
   1068      );
   1069      nodeFront.walkerFront.on(
   1070        "display-change",
   1071        this._onWalkerNodeStatesChanged
   1072      );
   1073      nodeFront.walkerFront.on(
   1074        "scrollable-change",
   1075        this._onWalkerNodeStatesChanged
   1076      );
   1077      nodeFront.walkerFront.on(
   1078        "overflow-change",
   1079        this._onWalkerNodeStatesChanged
   1080      );
   1081      nodeFront.walkerFront.on("mutations", this._onWalkerMutations);
   1082    }
   1083 
   1084    if (this.htmlEditor) {
   1085      this.htmlEditor.hide();
   1086    }
   1087    if (this._isContainerSelected(this._hoveredContainer)) {
   1088      this._hoveredContainer.hovered = false;
   1089      this._hoveredContainer = null;
   1090    }
   1091 
   1092    if (!selection.isNode()) {
   1093      this.unmarkSelectedNode();
   1094      return;
   1095    }
   1096 
   1097    const done = this.inspector.updating("markup-view");
   1098    let onShowBoxModel;
   1099 
   1100    // Highlight the element briefly if needed.
   1101    if (this._shouldNewSelectionBeHighlighted()) {
   1102      onShowBoxModel = this._showBoxModel(nodeFront, {
   1103        duration: this.inspector.HIGHLIGHTER_AUTOHIDE_TIMER,
   1104      });
   1105    }
   1106 
   1107    const slotted = selection.isSlotted();
   1108    const smoothScroll = reason === "reveal-from-slot";
   1109    const selectionSearchQuery = selection.getSearchQuery();
   1110 
   1111    const onShow = this.showNode(selection.nodeFront, {
   1112      slotted,
   1113      smoothScroll,
   1114      // Don't scroll if we selected the node from the search, we'll scroll to the first
   1115      // matching Range done in _updateSearchResultsHighlightingInSelectedNode.
   1116      // This need to be done there because the matching Range might be out of screen,
   1117      // for example if the node is very tall, or if the markup view overflows horizontally
   1118      // and the Range is located near the right end of the node container.
   1119      scroll: !selectionSearchQuery,
   1120    })
   1121      .then(() => {
   1122        // We could be destroyed by now.
   1123        if (this._destroyed) {
   1124          return Promise.reject("markupview destroyed");
   1125        }
   1126 
   1127        // Mark the node as selected.
   1128        const container = this.getContainer(selection.nodeFront, slotted);
   1129        this._markContainerAsSelected(container);
   1130        this._updateSearchResultsHighlightingInSelectedNode(
   1131          selectionSearchQuery
   1132        );
   1133 
   1134        // Make sure the new selection is navigated to.
   1135        this.maybeNavigateToNewSelection();
   1136        return undefined;
   1137      })
   1138      .catch(this._handleRejectionIfNotDestroyed);
   1139 
   1140    Promise.all([onShowBoxModel, onShow]).then(done);
   1141  }
   1142 
   1143  _getSearchResultsHighlight() {
   1144    const highlightName = "devtools-search";
   1145    const highlights = this.win.CSS.highlights;
   1146 
   1147    if (!highlights.has(highlightName)) {
   1148      highlights.set(highlightName, new this.win.Highlight());
   1149    }
   1150 
   1151    return highlights.get(highlightName);
   1152  }
   1153 
   1154  /**
   1155   * @returns {nsISelectionController}
   1156   */
   1157  _getSelectionController() {
   1158    if (!this._selectionController) {
   1159      // QueryInterface can be expensive, so cache the controller.
   1160      this._selectionController = this.win.docShell
   1161        .QueryInterface(Ci.nsIInterfaceRequestor)
   1162        .getInterface(Ci.nsISelectionDisplay)
   1163        .QueryInterface(Ci.nsISelectionController);
   1164    }
   1165    return this._selectionController;
   1166  }
   1167 
   1168  /**
   1169   * Highlight search results in the markup view.
   1170   *
   1171   * @param {string | null} searchQuery: The search string we want to highlight. Pass null
   1172   *                                   to clear existing highlighting.
   1173   */
   1174  _updateSearchResultsHighlightingInSelectedNode(searchQuery) {
   1175    // Clear any existing search highlights
   1176    const searchHighlight = this._getSearchResultsHighlight();
   1177    searchHighlight.clear();
   1178 
   1179    // If there's no selected container, or if the search is empty, we don't have anything
   1180    // to highlight.
   1181    if (!this._selectedContainer || !searchQuery) {
   1182      this.emitForTests("search-results-highlighting-updated");
   1183      return;
   1184    }
   1185 
   1186    // Look for search string occurences in the tag
   1187    const treeWalker = this.doc.createTreeWalker(
   1188      this._selectedContainer.tagLine,
   1189      NodeFilter.SHOW_TEXT
   1190    );
   1191    searchQuery = searchQuery.toLowerCase();
   1192    const searchQueryLength = searchQuery.length;
   1193    let currentNode = treeWalker.nextNode();
   1194    let scrolled = false;
   1195 
   1196    while (currentNode) {
   1197      const text = currentNode.textContent.toLowerCase();
   1198      let startPos = 0;
   1199      while (startPos < text.length) {
   1200        const index = text.indexOf(searchQuery, startPos);
   1201        if (index === -1) {
   1202          break;
   1203        }
   1204 
   1205        const range = new this.win.Range();
   1206        range.setStart(currentNode, index);
   1207        range.setEnd(currentNode, index + searchQueryLength);
   1208 
   1209        searchHighlight.add(range);
   1210 
   1211        startPos = index + searchQuery.length;
   1212 
   1213        // We want to scroll the first matching range into view
   1214        if (!scrolled) {
   1215          // We want to take advantage of nsISelectionController.scrollSelectionIntoView,
   1216          // so we need to put the range in the selection. That's fine to do here because
   1217          // in this situation the user shouldn't have any text selected
   1218          const selection = this.win.getSelection();
   1219          selection.removeAllRanges();
   1220          selection.addRange(range);
   1221 
   1222          const selectionController = this._getSelectionController();
   1223          selectionController.scrollSelectionIntoView(
   1224            selectionController.SELECTION_NORMAL,
   1225            selectionController.SELECTION_ON,
   1226            selectionController.SCROLL_SYNCHRONOUS |
   1227              selectionController.SCROLL_VERTICAL_CENTER
   1228          );
   1229          selection.removeAllRanges();
   1230          scrolled = true;
   1231        }
   1232      }
   1233 
   1234      currentNode = treeWalker.nextNode();
   1235    }
   1236 
   1237    // It can happen that we didn't find a Range for a search result (e.g. if the matching
   1238    // string is in a cropped attribute). In such case, go back to scroll the container
   1239    // into view.
   1240    if (!scrolled) {
   1241      const container = this.getContainer(
   1242        this.inspector.selection.nodeFront,
   1243        this.inspector.selection.isSlotted()
   1244      );
   1245      scrollIntoViewIfNeeded(
   1246        container.editor.elt,
   1247        // centered
   1248        true,
   1249        // smoothScroll
   1250        false
   1251      );
   1252    }
   1253    this.emitForTests("search-results-highlighting-updated");
   1254  }
   1255 
   1256  /**
   1257   * Maybe make selected the current node selection's MarkupContainer depending
   1258   * on why the current node got selected.
   1259   */
   1260  async maybeNavigateToNewSelection() {
   1261    const { reason, nodeFront } = this.inspector.selection;
   1262 
   1263    // The list of reasons that should lead to navigating to the node.
   1264    const reasonsToNavigate = [
   1265      // If the user picked an element with the element picker.
   1266      "picker-node-picked",
   1267      // If the user shift-clicked (previewed) an element.
   1268      "picker-node-previewed",
   1269      // If the user selected an element with the browser context menu.
   1270      "browser-context-menu",
   1271      // If the user added a new node by clicking in the inspector toolbar.
   1272      "node-inserted",
   1273    ];
   1274 
   1275    // If the user performed an action with a keyboard, move keyboard focus to
   1276    // the markup tree container.
   1277    if (reason && reason.endsWith("-keyboard")) {
   1278      this.getContainer(this._rootNode).elt.focus();
   1279    }
   1280 
   1281    if (reasonsToNavigate.includes(reason)) {
   1282      // not sure this is necessary
   1283      const root = await nodeFront.walkerFront.getRootNode();
   1284      this.getContainer(root).elt.focus();
   1285      this.navigate(this.getContainer(nodeFront));
   1286    }
   1287  }
   1288 
   1289  /**
   1290   * Create a TreeWalker to find the next/previous
   1291   * node for selection.
   1292   */
   1293  _selectionWalker(start) {
   1294    const walker = this.doc.createTreeWalker(
   1295      start || this._elt,
   1296      nodeFilterConstants.SHOW_ELEMENT,
   1297      function (element) {
   1298        if (
   1299          element.container &&
   1300          element.container.elt === element &&
   1301          element.container.visible
   1302        ) {
   1303          return nodeFilterConstants.FILTER_ACCEPT;
   1304        }
   1305        return nodeFilterConstants.FILTER_SKIP;
   1306      }
   1307    );
   1308    walker.currentNode = this._selectedContainer.elt;
   1309    return walker;
   1310  }
   1311 
   1312  _onCopy(evt) {
   1313    // Ignore copy events from editors
   1314    if (this.isInputOrTextareaOrInCodeMirrorEditor(evt.target)) {
   1315      return;
   1316    }
   1317 
   1318    const selection = this.inspector.selection;
   1319    if (selection.isNode()) {
   1320      this.copyOuterHTML();
   1321    }
   1322    evt.stopPropagation();
   1323    evt.preventDefault();
   1324  }
   1325 
   1326  /**
   1327   * Copy the outerHTML of the selected Node to the clipboard.
   1328   */
   1329  copyOuterHTML() {
   1330    if (!this.inspector.selection.isNode()) {
   1331      return;
   1332    }
   1333    const node = this.inspector.selection.nodeFront;
   1334 
   1335    switch (node.nodeType) {
   1336      case nodeConstants.ELEMENT_NODE:
   1337        copyLongHTMLString(node.walkerFront.outerHTML(node));
   1338        break;
   1339      case nodeConstants.COMMENT_NODE:
   1340        getLongString(node.getNodeValue()).then(comment => {
   1341          clipboardHelper.copyString("<!--" + comment + "-->");
   1342        });
   1343        break;
   1344      case nodeConstants.DOCUMENT_TYPE_NODE:
   1345        clipboardHelper.copyString(node.doctypeString);
   1346        break;
   1347    }
   1348  }
   1349 
   1350  /**
   1351   * Copy the innerHTML of the selected Node to the clipboard.
   1352   */
   1353  copyInnerHTML() {
   1354    const nodeFront = this.inspector.selection.nodeFront;
   1355    if (!this.inspector.selection.isNode()) {
   1356      return;
   1357    }
   1358 
   1359    copyLongHTMLString(nodeFront.walkerFront.innerHTML(nodeFront));
   1360  }
   1361 
   1362  /**
   1363   * Given a type and link found in a node's attribute in the markup-view,
   1364   * attempt to follow that link (which may result in opening a new tab, the
   1365   * style editor or debugger).
   1366   */
   1367  followAttributeLink(type, link) {
   1368    if (!type || !link) {
   1369      return;
   1370    }
   1371 
   1372    const nodeFront = this.inspector.selection.nodeFront;
   1373    if (type === "uri" || type === "cssresource" || type === "jsresource") {
   1374      // Open link in a new tab.
   1375      nodeFront.inspectorFront
   1376        .resolveRelativeURL(link, this.inspector.selection.nodeFront)
   1377        .then(url => {
   1378          if (type === "uri") {
   1379            openContentLink(url);
   1380          } else if (type === "cssresource") {
   1381            return this.toolbox.viewGeneratedSourceInStyleEditor(url);
   1382          } else if (type === "jsresource") {
   1383            return this.toolbox.viewGeneratedSourceInDebugger(url);
   1384          }
   1385          return null;
   1386        })
   1387        .catch(console.error);
   1388    } else if (type == "idref") {
   1389      // Select the node in the same document.
   1390      nodeFront.walkerFront
   1391        .getIdrefNode(
   1392          nodeFront,
   1393          // No need to escape the id, the server getIdrefNode uses getElementById
   1394          link
   1395        )
   1396        .then(node => {
   1397          if (!node) {
   1398            this.emitForTests("idref-attribute-link-failed");
   1399            return;
   1400          }
   1401          this.inspector.selection.setNodeFront(node, {
   1402            reason: "markup-attribute-link",
   1403          });
   1404        })
   1405        .catch(console.error);
   1406    }
   1407  }
   1408 
   1409  /**
   1410   * Register all key shortcuts.
   1411   */
   1412  _initShortcuts() {
   1413    const shortcuts = new KeyShortcuts({
   1414      window: this.win,
   1415    });
   1416 
   1417    // Keep a pointer on shortcuts to destroy them when destroying the markup
   1418    // view.
   1419    this._shortcuts = shortcuts;
   1420 
   1421    this._onShortcut = this._onShortcut.bind(this);
   1422 
   1423    // Process localizable keys
   1424    [
   1425      "markupView.hide.key",
   1426      "markupView.edit.key",
   1427      "markupView.scrollInto.key",
   1428    ].forEach(name => {
   1429      const key = INSPECTOR_L10N.getStr(name);
   1430      shortcuts.on(key, event => this._onShortcut(name, event));
   1431    });
   1432 
   1433    // Process generic keys:
   1434    [
   1435      "Delete",
   1436      "Backspace",
   1437      "Home",
   1438      "Left",
   1439      "Right",
   1440      "Up",
   1441      "Down",
   1442      "PageUp",
   1443      "PageDown",
   1444      "Esc",
   1445      "Enter",
   1446      "Space",
   1447    ].forEach(key => {
   1448      shortcuts.on(key, event => this._onShortcut(key, event));
   1449    });
   1450  }
   1451 
   1452  /**
   1453   * Key shortcut listener.
   1454   */
   1455  _onShortcut(name, event) {
   1456    if (this.isInputOrTextareaOrInCodeMirrorEditor(event.target)) {
   1457      return;
   1458    }
   1459 
   1460    // If the selected element is a button (e.g. `flex` badge), we don't want to highjack
   1461    // keyboard activation.
   1462    if (
   1463      event.target.closest(":is(button, [role=button])") &&
   1464      (name === "Enter" || name === "Space")
   1465    ) {
   1466      return;
   1467    }
   1468 
   1469    const handler = shortcutHandlers[name];
   1470    const shouldPropagate = handler(this);
   1471    if (shouldPropagate) {
   1472      return;
   1473    }
   1474 
   1475    event.stopPropagation();
   1476    event.preventDefault();
   1477  }
   1478 
   1479  /**
   1480   * Check if a node is used to type text (i.e. an input or textarea, or in a CodeMirror editor)
   1481   */
   1482  isInputOrTextareaOrInCodeMirrorEditor(element) {
   1483    const name = element.tagName.toLowerCase();
   1484    if (name === "input" || name === "textarea") {
   1485      return true;
   1486    }
   1487 
   1488    if (element.closest(".cm-editor")) {
   1489      return true;
   1490    }
   1491 
   1492    return false;
   1493  }
   1494 
   1495  /**
   1496   * If there's an attribute on the current node that's currently focused, then
   1497   * delete this attribute, otherwise delete the node itself.
   1498   *
   1499   * @param  {boolean} moveBackward
   1500   *         If set to true and if we're deleting the node, focus the previous
   1501   *         sibling after deletion, otherwise the next one.
   1502   */
   1503  deleteNodeOrAttribute(moveBackward) {
   1504    const focusedAttribute = this.doc.activeElement
   1505      ? this.doc.activeElement.closest(".attreditor")
   1506      : null;
   1507    if (focusedAttribute) {
   1508      // The focused attribute might not be in the current selected container.
   1509      const container = focusedAttribute.closest("li.child").container;
   1510      container.removeAttribute(focusedAttribute.dataset.attr);
   1511    } else {
   1512      this.deleteNode(this._selectedContainer.node, moveBackward);
   1513    }
   1514  }
   1515 
   1516  /**
   1517   * Returns a value indicating whether a node can be deleted.
   1518   *
   1519   * @param {NodeFront} nodeFront
   1520   *        The node to test for deletion
   1521   */
   1522  isDeletable(nodeFront) {
   1523    return !(
   1524      nodeFront.isDocumentElement ||
   1525      nodeFront.nodeType == nodeConstants.DOCUMENT_NODE ||
   1526      nodeFront.nodeType == nodeConstants.DOCUMENT_TYPE_NODE ||
   1527      nodeFront.nodeType == nodeConstants.DOCUMENT_FRAGMENT_NODE ||
   1528      nodeFront.isNativeAnonymous
   1529    );
   1530  }
   1531 
   1532  /**
   1533   * Delete a node from the DOM.
   1534   * This is an undoable action.
   1535   *
   1536   * @param  {NodeFront} node
   1537   *         The node to remove.
   1538   * @param  {boolean} moveBackward
   1539   *         If set to true, focus the previous sibling, otherwise the next one.
   1540   */
   1541  deleteNode(node, moveBackward) {
   1542    if (!this.isDeletable(node)) {
   1543      return;
   1544    }
   1545 
   1546    const container = this.getContainer(node);
   1547 
   1548    // Retain the node so we can undo this...
   1549    node.walkerFront
   1550      .retainNode(node)
   1551      .then(() => {
   1552        const parent = node.parentNode();
   1553        let nextSibling = null;
   1554        this.undo.do(
   1555          () => {
   1556            node.walkerFront.removeNode(node).then(siblings => {
   1557              nextSibling = siblings.nextSibling;
   1558              const prevSibling = siblings.previousSibling;
   1559              let focusNode = moveBackward ? prevSibling : nextSibling;
   1560 
   1561              // If we can't move as the user wants, we move to the other direction.
   1562              // If there is no sibling elements anymore, move to the parent node.
   1563              if (!focusNode) {
   1564                focusNode = nextSibling || prevSibling || parent;
   1565              }
   1566 
   1567              const isNextSiblingText = nextSibling
   1568                ? nextSibling.nodeType === nodeConstants.TEXT_NODE
   1569                : false;
   1570              const isPrevSiblingText = prevSibling
   1571                ? prevSibling.nodeType === nodeConstants.TEXT_NODE
   1572                : false;
   1573 
   1574              // If the parent had two children and the next or previous sibling
   1575              // is a text node, then it now has only a single text node, is about
   1576              // to be in-lined; and focus should move to the parent.
   1577              if (
   1578                parent.numChildren === 2 &&
   1579                (isNextSiblingText || isPrevSiblingText)
   1580              ) {
   1581                focusNode = parent;
   1582              }
   1583 
   1584              if (container.selected) {
   1585                this.navigate(this.getContainer(focusNode));
   1586              }
   1587            });
   1588          },
   1589          () => {
   1590            const isValidSibling = nextSibling && !nextSibling.isPseudoElement;
   1591            nextSibling = isValidSibling ? nextSibling : null;
   1592            node.walkerFront.insertBefore(node, parent, nextSibling);
   1593          }
   1594        );
   1595      })
   1596      .catch(console.error);
   1597  }
   1598 
   1599  /**
   1600   * Scroll the node into view.
   1601   */
   1602  scrollNodeIntoView() {
   1603    if (!this.inspector.selection.supportsScrollIntoView()) {
   1604      return;
   1605    }
   1606 
   1607    this.inspector.selection.nodeFront
   1608      .scrollIntoView()
   1609      .then(() => this.emitForTests("node-scrolled-into-view"));
   1610  }
   1611 
   1612  async toggleMutationBreakpoint(name) {
   1613    if (!this.inspector.selection.isElementNode()) {
   1614      return;
   1615    }
   1616 
   1617    const toolboxStore = this.inspector.toolbox.store;
   1618    const nodeFront = this.inspector.selection.nodeFront;
   1619 
   1620    if (nodeFront.mutationBreakpoints[name]) {
   1621      toolboxStore.dispatch(deleteDOMMutationBreakpoint(nodeFront, name));
   1622    } else {
   1623      toolboxStore.dispatch(createDOMMutationBreakpoint(nodeFront, name));
   1624    }
   1625  }
   1626 
   1627  /**
   1628   * If an editable item is focused, select its container.
   1629   */
   1630  _onFocus(event) {
   1631    let parent = event.target;
   1632    while (!parent.container) {
   1633      parent = parent.parentNode;
   1634    }
   1635    if (parent) {
   1636      this.navigate(parent.container);
   1637    }
   1638  }
   1639 
   1640  /**
   1641   * Handle a user-requested navigation to a given MarkupContainer,
   1642   * updating the inspector's currently-selected node.
   1643   *
   1644   * @param  {MarkupContainer} container
   1645   *         The container we're navigating to.
   1646   */
   1647  navigate(container) {
   1648    if (!container) {
   1649      return;
   1650    }
   1651 
   1652    this._markContainerAsSelected(container, "treepanel");
   1653  }
   1654 
   1655  /**
   1656   * Make sure a node is included in the markup tool.
   1657   *
   1658   * @param  {NodeFront} node
   1659   *         The node in the content document.
   1660   * @param  {boolean} flashNode
   1661   *         Whether the newly imported node should be flashed
   1662   * @param  {boolean} slotted
   1663   *         Whether we are importing the slotted version of the node.
   1664   * @return {MarkupContainer} The MarkupContainer object for this element.
   1665   */
   1666  importNode(node, flashNode, slotted) {
   1667    if (!node) {
   1668      return null;
   1669    }
   1670 
   1671    if (this.hasContainer(node, slotted)) {
   1672      return this.getContainer(node, slotted);
   1673    }
   1674 
   1675    let container;
   1676    const { nodeType, isPseudoElement } = node;
   1677    if (node === node.walkerFront.rootNode) {
   1678      container = new RootContainer(this, node);
   1679      this._elt.appendChild(container.elt);
   1680    }
   1681    if (node === this.walker.rootNode) {
   1682      this._rootNode = node;
   1683    } else if (slotted) {
   1684      container = new SlottedNodeContainer(this, node, this.inspector);
   1685    } else if (nodeType == nodeConstants.ELEMENT_NODE && !isPseudoElement) {
   1686      container = new MarkupElementContainer(this, node, this.inspector);
   1687    } else if (
   1688      nodeType == nodeConstants.COMMENT_NODE ||
   1689      nodeType == nodeConstants.TEXT_NODE
   1690    ) {
   1691      container = new MarkupTextContainer(this, node, this.inspector);
   1692    } else {
   1693      container = new MarkupReadOnlyContainer(this, node, this.inspector);
   1694    }
   1695 
   1696    if (flashNode) {
   1697      container.flashMutation();
   1698    }
   1699 
   1700    this.setContainer(node, container, slotted);
   1701    this._forceUpdateChildren(container);
   1702 
   1703    this.inspector.emit("container-created", container);
   1704 
   1705    return container;
   1706  }
   1707 
   1708  async _onResourceAvailable(resources) {
   1709    for (const resource of resources) {
   1710      if (
   1711        !this.resourceCommand ||
   1712        resource.resourceType !== this.resourceCommand.TYPES.ROOT_NODE ||
   1713        resource.isDestroyed()
   1714      ) {
   1715        // Only handle alive root-node resources
   1716        continue;
   1717      }
   1718 
   1719      if (resource.targetFront.isTopLevel && resource.isTopLevelDocument) {
   1720        // The topmost root node will lead to the destruction and recreation of
   1721        // the MarkupView. This is handled by the inspector.
   1722        continue;
   1723      }
   1724 
   1725      const parentNodeFront = resource.parentNode();
   1726      const container = this.getContainer(parentNodeFront);
   1727      if (container) {
   1728        // If there is no container for the parentNodeFront, the markup view is
   1729        // currently not watching this part of the tree.
   1730        this._forceUpdateChildren(container, {
   1731          flash: true,
   1732          updateLevel: true,
   1733        });
   1734      }
   1735    }
   1736  }
   1737 
   1738  _onTargetAvailable() {}
   1739 
   1740  _onTargetDestroyed({ targetFront, isModeSwitching }) {
   1741    // Bug 1776250: We only watch targets in order to update containers which
   1742    // might no longer be able to display children hosted in remote processes,
   1743    // which corresponds to a Browser Toolbox mode switch.
   1744    if (isModeSwitching) {
   1745      const container = this.getContainer(targetFront.getParentNodeFront());
   1746      if (container) {
   1747        this._forceUpdateChildren(container, {
   1748          updateLevel: true,
   1749        });
   1750      }
   1751    }
   1752  }
   1753 
   1754  /**
   1755   * Mutation observer used for included nodes.
   1756   */
   1757  _onWalkerMutations(mutations) {
   1758    for (const mutation of mutations) {
   1759      const type = mutation.type;
   1760      const target = mutation.target;
   1761 
   1762      const container = this.getContainer(target);
   1763      if (!container) {
   1764        // Container might not exist if this came from a load event for a node
   1765        // we're not viewing.
   1766        continue;
   1767      }
   1768 
   1769      if (
   1770        type === "attributes" ||
   1771        type === "characterData" ||
   1772        type === "customElementDefined" ||
   1773        type === "events" ||
   1774        type === "pseudoClassLock"
   1775      ) {
   1776        container.update();
   1777      } else if (
   1778        type === "childList" ||
   1779        type === "slotchange" ||
   1780        type === "shadowRootAttached"
   1781      ) {
   1782        this._forceUpdateChildren(container, {
   1783          flash: true,
   1784          updateLevel: true,
   1785        });
   1786      } else if (type === "inlineTextChild") {
   1787        this._forceUpdateChildren(container, { flash: true });
   1788        container.update();
   1789      }
   1790    }
   1791 
   1792    this._waitForChildren().then(() => {
   1793      if (this._destroyed) {
   1794        // Could not fully update after markup mutations, the markup-view was destroyed
   1795        // while waiting for children. Bail out silently.
   1796        return;
   1797      }
   1798      this._flashMutatedNodes(mutations);
   1799      this.inspector.emit("markupmutation", mutations);
   1800 
   1801      // Since the htmlEditor is absolutely positioned, a mutation may change
   1802      // the location in which it should be shown.
   1803      if (this.htmlEditor) {
   1804        this.htmlEditor.refresh();
   1805      }
   1806    });
   1807  }
   1808 
   1809  /**
   1810   * React to display-change and scrollable-change events from the walker. These are
   1811   * events that tell us when something of interest changed on a collection of nodes:
   1812   * whether their display type changed, or whether they became scrollable.
   1813   *
   1814   * @param  {Array} nodes
   1815   *         An array of nodeFronts
   1816   */
   1817  _onWalkerNodeStatesChanged(nodes) {
   1818    for (const node of nodes) {
   1819      const container = this.getContainer(node);
   1820      if (container) {
   1821        container.update();
   1822      }
   1823    }
   1824  }
   1825 
   1826  /**
   1827   * Given a list of mutations returned by the mutation observer, flash the
   1828   * corresponding containers to attract attention.
   1829   */
   1830  _flashMutatedNodes(mutations) {
   1831    const addedOrEditedContainers = new Set();
   1832    const removedContainers = new Set();
   1833 
   1834    for (const { type, target, added, removed, newValue } of mutations) {
   1835      const container = this.getContainer(target);
   1836 
   1837      if (container) {
   1838        if (type === "characterData") {
   1839          addedOrEditedContainers.add(container);
   1840        } else if (type === "attributes" && newValue === null) {
   1841          // Removed attributes should flash the entire node.
   1842          // New or changed attributes will flash the attribute itself
   1843          // in ElementEditor.flashAttribute.
   1844          addedOrEditedContainers.add(container);
   1845        } else if (type === "childList") {
   1846          // If there has been removals, flash the parent
   1847          if (removed.length) {
   1848            removedContainers.add(container);
   1849          }
   1850 
   1851          // If there has been additions, flash the nodes if their associated
   1852          // container exist (so if their parent is expanded in the inspector).
   1853          added.forEach(node => {
   1854            const addedContainer = this.getContainer(node);
   1855            if (addedContainer) {
   1856              addedOrEditedContainers.add(addedContainer);
   1857 
   1858              // The node may be added as a result of an append, in which case
   1859              // it will have been removed from another container first, but in
   1860              // these cases we don't want to flash both the removal and the
   1861              // addition
   1862              removedContainers.delete(container);
   1863            }
   1864          });
   1865        }
   1866      }
   1867    }
   1868 
   1869    for (const container of removedContainers) {
   1870      container.flashMutation();
   1871    }
   1872    for (const container of addedOrEditedContainers) {
   1873      container.flashMutation();
   1874    }
   1875  }
   1876 
   1877  /**
   1878   * Make sure the given node's parents are expanded and the
   1879   * node is scrolled on to screen.
   1880   *
   1881   * @param {NodeFront} nodeFront
   1882   * @param {object} options
   1883   * @param {boolean} options.centered
   1884   * @param {boolean} options.scroll
   1885   * @param {boolean} options.slotted
   1886   * @param {boolean} options.smoothScroll
   1887   * @returns
   1888   */
   1889  showNode(
   1890    nodeFront,
   1891    { centered = true, scroll = true, slotted, smoothScroll = false } = {}
   1892  ) {
   1893    if (slotted && !this.hasContainer(nodeFront, slotted)) {
   1894      throw new Error("Tried to show a slotted node not previously imported");
   1895    } else {
   1896      this._ensureNodeImported(nodeFront);
   1897    }
   1898 
   1899    return this._waitForChildren()
   1900      .then(() => {
   1901        if (this._destroyed) {
   1902          return Promise.reject("markupview destroyed");
   1903        }
   1904        return this._ensureVisible(nodeFront);
   1905      })
   1906      .then(() => {
   1907        if (!scroll) {
   1908          return;
   1909        }
   1910 
   1911        const container = this.getContainer(nodeFront, slotted);
   1912        scrollIntoViewIfNeeded(container.editor.elt, centered, smoothScroll);
   1913      }, this._handleRejectionIfNotDestroyed);
   1914  }
   1915 
   1916  _ensureNodeImported(node) {
   1917    let parent = node;
   1918 
   1919    this.importNode(node);
   1920 
   1921    while ((parent = this._getParentInTree(parent))) {
   1922      this.importNode(parent);
   1923      this.expandNode(parent);
   1924    }
   1925  }
   1926 
   1927  /**
   1928   * Expand the container's children.
   1929   */
   1930  _expandContainer(container) {
   1931    return this._updateChildren(container, { expand: true }).then(() => {
   1932      if (this._destroyed) {
   1933        // Could not expand the node, the markup-view was destroyed in the meantime. Just
   1934        // silently give up.
   1935        return;
   1936      }
   1937      container.setExpanded(true);
   1938    });
   1939  }
   1940 
   1941  /**
   1942   * Expand the node's children.
   1943   */
   1944  expandNode(node) {
   1945    const container = this.getContainer(node);
   1946    return this._expandContainer(container);
   1947  }
   1948 
   1949  /**
   1950   * Expand the entire tree beneath a container.
   1951   *
   1952   * @param  {MarkupContainer} container
   1953   *         The container to expand.
   1954   */
   1955  _expandAll(container) {
   1956    return this._expandContainer(container)
   1957      .then(() => {
   1958        let child = container.children.firstChild;
   1959        const promises = [];
   1960        while (child) {
   1961          promises.push(this._expandAll(child.container));
   1962          child = child.nextSibling;
   1963        }
   1964        return Promise.all(promises);
   1965      })
   1966      .catch(console.error);
   1967  }
   1968 
   1969  /**
   1970   * Expand the entire tree beneath a node.
   1971   *
   1972   * @param  {DOMNode} node
   1973   *         The node to expand, or null to start from the top.
   1974   * @return {Promise} promise that resolves once all children are expanded.
   1975   */
   1976  expandAll(node) {
   1977    node = node || this._rootNode;
   1978    return this._expandAll(this.getContainer(node));
   1979  }
   1980 
   1981  /**
   1982   * Collapse the node's children.
   1983   */
   1984  collapseNode(node) {
   1985    const container = this.getContainer(node);
   1986    container.setExpanded(false);
   1987  }
   1988 
   1989  _collapseAll(container) {
   1990    container.setExpanded(false);
   1991    const children = container.getChildContainers() || [];
   1992    children.forEach(child => this._collapseAll(child));
   1993  }
   1994 
   1995  /**
   1996   * Collapse the entire tree beneath a node.
   1997   *
   1998   * @param  {DOMNode} node
   1999   *         The node to collapse.
   2000   * @return {Promise} promise that resolves once all children are collapsed.
   2001   */
   2002  collapseAll(node) {
   2003    this._collapseAll(this.getContainer(node));
   2004 
   2005    // collapseAll is synchronous, return a promise for consistency with expandAll.
   2006    return Promise.resolve();
   2007  }
   2008 
   2009  /**
   2010   * Returns either the innerHTML or the outerHTML for a remote node.
   2011   *
   2012   * @param  {NodeFront} node
   2013   *         The NodeFront to get the outerHTML / innerHTML for.
   2014   * @param  {boolean} isOuter
   2015   *         If true, makes the function return the outerHTML,
   2016   *         otherwise the innerHTML.
   2017   * @return {Promise} that will be resolved with the outerHTML / innerHTML.
   2018   */
   2019  _getNodeHTML(node, isOuter) {
   2020    let walkerPromise = null;
   2021 
   2022    if (isOuter) {
   2023      walkerPromise = node.walkerFront.outerHTML(node);
   2024    } else {
   2025      walkerPromise = node.walkerFront.innerHTML(node);
   2026    }
   2027 
   2028    return getLongString(walkerPromise);
   2029  }
   2030 
   2031  /**
   2032   * Retrieve the outerHTML for a remote node.
   2033   *
   2034   * @param  {NodeFront} node
   2035   *         The NodeFront to get the outerHTML for.
   2036   * @return {Promise} that will be resolved with the outerHTML.
   2037   */
   2038  getNodeOuterHTML(node) {
   2039    return this._getNodeHTML(node, true);
   2040  }
   2041 
   2042  /**
   2043   * Retrieve the innerHTML for a remote node.
   2044   *
   2045   * @param  {NodeFront} node
   2046   *         The NodeFront to get the innerHTML for.
   2047   * @return {Promise} that will be resolved with the innerHTML.
   2048   */
   2049  getNodeInnerHTML(node) {
   2050    return this._getNodeHTML(node);
   2051  }
   2052 
   2053  /**
   2054   * Listen to mutations, expect a given node to be removed and try and select
   2055   * the node that sits at the same place instead.
   2056   * This is useful when changing the outerHTML or the tag name so that the
   2057   * newly inserted node gets selected instead of the one that just got removed.
   2058   */
   2059  reselectOnRemoved(removedNode, reason) {
   2060    // Only allow one removed node reselection at a time, so that when there are
   2061    // more than 1 request in parallel, the last one wins.
   2062    this.cancelReselectOnRemoved();
   2063 
   2064    // Get the removedNode index in its parent node to reselect the right node.
   2065    const isRootElement = ["html", "svg"].includes(
   2066      removedNode.tagName.toLowerCase()
   2067    );
   2068    const oldContainer = this.getContainer(removedNode);
   2069    const parentContainer = this.getContainer(removedNode.parentNode());
   2070    const childIndex = parentContainer
   2071      .getChildContainers()
   2072      .indexOf(oldContainer);
   2073 
   2074    const onMutations = (this._removedNodeObserver = mutations => {
   2075      let isNodeRemovalMutation = false;
   2076      for (const mutation of mutations) {
   2077        const containsRemovedNode =
   2078          mutation.removed && mutation.removed.some(n => n === removedNode);
   2079        if (
   2080          mutation.type === "childList" &&
   2081          (containsRemovedNode || isRootElement)
   2082        ) {
   2083          isNodeRemovalMutation = true;
   2084          break;
   2085        }
   2086      }
   2087      if (!isNodeRemovalMutation) {
   2088        return;
   2089      }
   2090 
   2091      this.inspector.off("markupmutation", onMutations);
   2092      this._removedNodeObserver = null;
   2093 
   2094      // Don't select the new node if the user has already changed the current
   2095      // selection.
   2096      if (
   2097        this.inspector.selection.nodeFront === parentContainer.node ||
   2098        (this.inspector.selection.nodeFront === removedNode && isRootElement)
   2099      ) {
   2100        const childContainers = parentContainer.getChildContainers();
   2101        if (childContainers?.[childIndex]) {
   2102          const childContainer = childContainers[childIndex];
   2103          this._markContainerAsSelected(childContainer, reason);
   2104          if (childContainer.hasChildren) {
   2105            this.expandNode(childContainer.node);
   2106          }
   2107          this.emit("reselectedonremoved");
   2108        }
   2109      }
   2110    });
   2111 
   2112    // Start listening for mutations until we find a childList change that has
   2113    // removedNode removed.
   2114    this.inspector.on("markupmutation", onMutations);
   2115  }
   2116 
   2117  /**
   2118   * Make sure to stop listening for node removal markupmutations and not
   2119   * reselect the corresponding node when that happens.
   2120   * Useful when the outerHTML/tagname edition failed.
   2121   */
   2122  cancelReselectOnRemoved() {
   2123    if (this._removedNodeObserver) {
   2124      this.inspector.off("markupmutation", this._removedNodeObserver);
   2125      this._removedNodeObserver = null;
   2126      this.emit("canceledreselectonremoved");
   2127    }
   2128  }
   2129 
   2130  /**
   2131   * Replace the outerHTML of any node displayed in the inspector with
   2132   * some other HTML code
   2133   *
   2134   * @param  {NodeFront} node
   2135   *         Node which outerHTML will be replaced.
   2136   * @param  {string} newValue
   2137   *         The new outerHTML to set on the node.
   2138   * @param  {string} oldValue
   2139   *         The old outerHTML that will be used if the user undoes the update.
   2140   * @return {Promise} that will resolve when the outer HTML has been updated.
   2141   */
   2142  updateNodeOuterHTML(node, newValue) {
   2143    const container = this.getContainer(node);
   2144    if (!container) {
   2145      return Promise.reject();
   2146    }
   2147 
   2148    // Changing the outerHTML removes the node which outerHTML was changed.
   2149    // Listen to this removal to reselect the right node afterwards.
   2150    this.reselectOnRemoved(node, "outerhtml");
   2151    return node.walkerFront.setOuterHTML(node, newValue).catch(() => {
   2152      this.cancelReselectOnRemoved();
   2153    });
   2154  }
   2155 
   2156  /**
   2157   * Replace the innerHTML of any node displayed in the inspector with
   2158   * some other HTML code
   2159   *
   2160   * @param  {Node} node
   2161   *         node which innerHTML will be replaced.
   2162   * @param  {string} newValue
   2163   *         The new innerHTML to set on the node.
   2164   * @param  {string} oldValue
   2165   *         The old innerHTML that will be used if the user undoes the update.
   2166   * @return {Promise} that will resolve when the inner HTML has been updated.
   2167   */
   2168  updateNodeInnerHTML(node, newValue, oldValue) {
   2169    const container = this.getContainer(node);
   2170    if (!container) {
   2171      return Promise.reject();
   2172    }
   2173 
   2174    return new Promise((resolve, reject) => {
   2175      container.undo.do(
   2176        () => {
   2177          node.walkerFront.setInnerHTML(node, newValue).then(resolve, reject);
   2178        },
   2179        () => {
   2180          node.walkerFront.setInnerHTML(node, oldValue);
   2181        }
   2182      );
   2183    });
   2184  }
   2185 
   2186  /**
   2187   * Insert adjacent HTML to any node displayed in the inspector.
   2188   *
   2189   * @param  {NodeFront} node
   2190   *         The reference node.
   2191   * @param  {string} position
   2192   *         The position as specified for Element.insertAdjacentHTML
   2193   *         (i.e. "beforeBegin", "afterBegin", "beforeEnd", "afterEnd").
   2194   * @param  {string} newValue
   2195   *         The adjacent HTML.
   2196   * @return {Promise} that will resolve when the adjacent HTML has
   2197   *         been inserted.
   2198   */
   2199  insertAdjacentHTMLToNode(node, position, value) {
   2200    const container = this.getContainer(node);
   2201    if (!container) {
   2202      return Promise.reject();
   2203    }
   2204 
   2205    let injectedNodes = [];
   2206 
   2207    return new Promise((resolve, reject) => {
   2208      container.undo.do(
   2209        () => {
   2210          // eslint-disable-next-line no-unsanitized/method
   2211          node.walkerFront
   2212            .insertAdjacentHTML(node, position, value)
   2213            .then(nodeArray => {
   2214              injectedNodes = nodeArray.nodes;
   2215              return nodeArray;
   2216            })
   2217            .then(resolve, reject);
   2218        },
   2219        () => {
   2220          node.walkerFront.removeNodes(injectedNodes);
   2221        }
   2222      );
   2223    });
   2224  }
   2225 
   2226  /**
   2227   * Open an editor in the UI to allow editing of a node's html.
   2228   *
   2229   * @param  {NodeFront} node
   2230   *         The NodeFront to edit.
   2231   */
   2232  beginEditingHTML(node) {
   2233    // We use outer html for elements, but inner html for fragments.
   2234    const isOuter = node.nodeType == nodeConstants.ELEMENT_NODE;
   2235    const html = isOuter
   2236      ? this.getNodeOuterHTML(node)
   2237      : this.getNodeInnerHTML(node);
   2238    html.then(oldValue => {
   2239      const container = this.getContainer(node);
   2240      if (!container) {
   2241        return;
   2242      }
   2243      // Load load and create HTML Editor as it is rarely used and fetch complex deps
   2244      if (!this.htmlEditor) {
   2245        const HTMLEditor = require("resource://devtools/client/inspector/markup/views/html-editor.js");
   2246        this.htmlEditor = new HTMLEditor(this.doc);
   2247      }
   2248      this.htmlEditor.show(container.tagLine, oldValue);
   2249      const start = this.telemetry.msSystemNow();
   2250      this.htmlEditor.once("popuphidden", (commit, value) => {
   2251        // Need to focus the <html> element instead of the frame / window
   2252        // in order to give keyboard focus back to doc (from editor).
   2253        this.doc.documentElement.focus();
   2254 
   2255        if (commit) {
   2256          if (isOuter) {
   2257            this.updateNodeOuterHTML(node, value, oldValue);
   2258          } else {
   2259            this.updateNodeInnerHTML(node, value, oldValue);
   2260          }
   2261        }
   2262 
   2263        const end = this.telemetry.msSystemNow();
   2264        this.telemetry.recordEvent("edit_html", "inspector", null, {
   2265          made_changes: commit,
   2266          time_open: end - start,
   2267        });
   2268      });
   2269 
   2270      this.emit("begin-editing");
   2271    });
   2272  }
   2273 
   2274  /**
   2275   * Expand or collapse the given node.
   2276   *
   2277   * @param  {NodeFront} node
   2278   *         The NodeFront to update.
   2279   * @param  {boolean} expanded
   2280   *         Whether the node should be expanded/collapsed.
   2281   * @param  {boolean} applyToDescendants
   2282   *         Whether all descendants should also be expanded/collapsed
   2283   */
   2284  setNodeExpanded(node, expanded, applyToDescendants) {
   2285    if (expanded) {
   2286      if (applyToDescendants) {
   2287        this.expandAll(node);
   2288      } else {
   2289        this.expandNode(node);
   2290      }
   2291    } else if (applyToDescendants) {
   2292      this.collapseAll(node);
   2293    } else {
   2294      this.collapseNode(node);
   2295    }
   2296  }
   2297 
   2298  /**
   2299   * Mark the given node selected, and update the inspector.selection
   2300   * object's NodeFront to keep consistent state between UI and selection.
   2301   *
   2302   * @param  {NodeFront} node
   2303   *         The NodeFront to mark as selected.
   2304   * @return {boolean} False if the node is already marked as selected, true
   2305   *         otherwise.
   2306   */
   2307  markNodeAsSelected(node) {
   2308    const container = this.getContainer(node);
   2309    return this._markContainerAsSelected(container);
   2310  }
   2311 
   2312  _markContainerAsSelected(container, reason) {
   2313    if (!container || this._selectedContainer === container) {
   2314      return false;
   2315    }
   2316 
   2317    const { node } = container;
   2318 
   2319    // Un-select and remove focus from the previous container.
   2320    if (this._selectedContainer) {
   2321      this._selectedContainer.selected = false;
   2322      this._selectedContainer.clearFocus();
   2323    }
   2324 
   2325    // Select the new container.
   2326    this._selectedContainer = container;
   2327    if (node) {
   2328      this._selectedContainer.selected = true;
   2329    }
   2330 
   2331    // Change the current selection if needed.
   2332    if (!this._isContainerSelected(this._selectedContainer)) {
   2333      const isSlotted = container.isSlotted();
   2334      this.inspector.selection.setNodeFront(node, { reason, isSlotted });
   2335    }
   2336 
   2337    return true;
   2338  }
   2339 
   2340  /**
   2341   * Make sure that every ancestor of the selection are updated
   2342   * and included in the list of visible children.
   2343   */
   2344  _ensureVisible(node) {
   2345    while (node) {
   2346      const container = this.getContainer(node);
   2347      const parent = this._getParentInTree(node);
   2348      if (!container.elt.parentNode) {
   2349        const parentContainer = this.getContainer(parent);
   2350        if (parentContainer) {
   2351          this._forceUpdateChildren(parentContainer, { expand: true });
   2352        }
   2353      }
   2354 
   2355      node = parent;
   2356    }
   2357    return this._waitForChildren();
   2358  }
   2359 
   2360  /**
   2361   * Unmark selected node (no node selected).
   2362   */
   2363  unmarkSelectedNode() {
   2364    if (this._selectedContainer) {
   2365      this._selectedContainer.selected = false;
   2366      this._selectedContainer = null;
   2367    }
   2368  }
   2369 
   2370  /**
   2371   * Check if the current selection is a descendent of the container.
   2372   * if so, make sure it's among the visible set for the container,
   2373   * and set the dirty flag if needed.
   2374   *
   2375   * @return The node that should be made visible, if any.
   2376   */
   2377  _checkSelectionVisible(container) {
   2378    let centered = null;
   2379    let node = this.inspector.selection.nodeFront;
   2380    while (node) {
   2381      if (this._getParentInTree(node) === container.node) {
   2382        centered = node;
   2383        break;
   2384      }
   2385      node = this._getParentInTree(node);
   2386    }
   2387 
   2388    return centered;
   2389  }
   2390 
   2391  async _forceUpdateChildren(container, options = {}) {
   2392    const { flash, updateLevel, expand } = options;
   2393 
   2394    // Set childrenDirty to true to force fetching new children.
   2395    container.childrenDirty = true;
   2396 
   2397    // Update the children to take care of changes in the markup view DOM
   2398    await this._updateChildren(container, { expand, flash });
   2399 
   2400    // The markup view may have been destroyed in the meantime
   2401    if (this._destroyed) {
   2402      return;
   2403    }
   2404 
   2405    if (updateLevel) {
   2406      // Update container (and its subtree) DOM tree depth level for
   2407      // accessibility where necessary.
   2408      container.updateLevel();
   2409    }
   2410  }
   2411 
   2412  /**
   2413   * Make sure all children of the given container's node are
   2414   * imported and attached to the container in the right order.
   2415   *
   2416   * Children need to be updated only in the following circumstances:
   2417   * a) We just imported this node and have never seen its children.
   2418   *    container.childrenDirty will be set by importNode in this case.
   2419   * b) We received a childList mutation on the node.
   2420   *    container.childrenDirty will be set in that case too.
   2421   * c) We have changed the selection, and the path to that selection
   2422   *    wasn't loaded in a previous children request (because we only
   2423   *    grab a subset).
   2424   *    container.childrenDirty should be set in that case too!
   2425   *
   2426   * @param  {MarkupContainer} container
   2427   *         The markup container whose children need updating
   2428   * @param  {object} options
   2429   *         Options are {expand:boolean,flash:boolean}
   2430   * @return {Promise} that will be resolved when the children are ready
   2431   *         (which may be immediately).
   2432   */
   2433  _updateChildren(container, options) {
   2434    // Slotted containers do not display any children.
   2435    if (container.isSlotted()) {
   2436      return Promise.resolve(container);
   2437    }
   2438 
   2439    const expand = options?.expand;
   2440    const flash = options?.flash;
   2441 
   2442    container.hasChildren = container.node.hasChildren;
   2443    // Accessibility should either ignore empty children or semantically
   2444    // consider them a group.
   2445    container.setChildrenRole();
   2446 
   2447    if (!this._queuedChildUpdates) {
   2448      this._queuedChildUpdates = new Map();
   2449    }
   2450 
   2451    if (this._queuedChildUpdates.has(container)) {
   2452      return this._queuedChildUpdates.get(container);
   2453    }
   2454 
   2455    if (!container.childrenDirty) {
   2456      return Promise.resolve(container);
   2457    }
   2458 
   2459    // Before bailing out for other conditions, check if the unavailable
   2460    // children badge needs updating (Bug 1776250).
   2461    if (
   2462      typeof container?.editor?.hasUnavailableChildren == "function" &&
   2463      container.editor.hasUnavailableChildren() !=
   2464        container.node.childrenUnavailable
   2465    ) {
   2466      container.update();
   2467    }
   2468 
   2469    if (
   2470      container.inlineTextChild &&
   2471      container.inlineTextChild != container.node.inlineTextChild
   2472    ) {
   2473      // This container was doing double duty as a container for a single
   2474      // text child, back that out.
   2475      this._containers.delete(container.inlineTextChild);
   2476      container.clearInlineTextChild();
   2477 
   2478      if (container.hasChildren && container.selected) {
   2479        container.setExpanded(true);
   2480      }
   2481    }
   2482 
   2483    if (container.node.inlineTextChild) {
   2484      container.setExpanded(false);
   2485      // this container will do double duty as the container for the single text child.
   2486      container.children.replaceChildren();
   2487 
   2488      container.setInlineTextChild(container.node.inlineTextChild);
   2489 
   2490      this.setContainer(container.node.inlineTextChild, container);
   2491      container.childrenDirty = false;
   2492      return Promise.resolve(container);
   2493    }
   2494 
   2495    if (!container.hasChildren) {
   2496      container.children.replaceChildren();
   2497      container.childrenDirty = false;
   2498      container.setExpanded(false);
   2499      return Promise.resolve(container);
   2500    }
   2501 
   2502    // If we're not expanded (or asked to update anyway), we're done for
   2503    // now.  Note that this will leave the childrenDirty flag set, so when
   2504    // expanded we'll refresh the child list.
   2505    if (!(container.expanded || expand)) {
   2506      return Promise.resolve(container);
   2507    }
   2508 
   2509    // We're going to issue a children request, make sure it includes the
   2510    // centered node.
   2511    const centered = this._checkSelectionVisible(container);
   2512 
   2513    // Children aren't updated yet, but clear the childrenDirty flag anyway.
   2514    // If the dirty flag is re-set while we're fetching we'll need to fetch
   2515    // again.
   2516    container.childrenDirty = false;
   2517 
   2518    const isShadowHost = container.node.isShadowHost;
   2519    const updatePromise = this._getVisibleChildren(container, centered)
   2520      .then(children => {
   2521        if (!this._containers) {
   2522          return Promise.reject("markup view destroyed");
   2523        }
   2524        this._queuedChildUpdates.delete(container);
   2525 
   2526        // If children are dirty, we got a change notification for this node
   2527        // while the request was in progress, we need to do it again.
   2528        if (container.childrenDirty) {
   2529          return this._updateChildren(container, {
   2530            expand: centered || expand,
   2531          });
   2532        }
   2533 
   2534        const fragment = this.doc.createDocumentFragment();
   2535 
   2536        // Store the focused element before moving elements to the document fragment
   2537        const previouslyActiveElement = this.doc.activeElement;
   2538        for (const child of children.nodes) {
   2539          const slotted = !isShadowHost && child.isDirectShadowHostChild;
   2540          const childContainer = this.importNode(child, flash, slotted);
   2541          fragment.appendChild(childContainer.elt);
   2542        }
   2543 
   2544        container.children.replaceChildren();
   2545 
   2546        if (!children.hasFirst) {
   2547          const topItem = this.buildMoreNodesButtonMarkup(container);
   2548          fragment.insertBefore(topItem, fragment.firstChild);
   2549        }
   2550        if (!children.hasLast) {
   2551          const bottomItem = this.buildMoreNodesButtonMarkup(container);
   2552          fragment.appendChild(bottomItem);
   2553        }
   2554 
   2555        container.children.appendChild(fragment);
   2556        // If previouslyActiveElement was moved to `fragment`, the focus was moved elsewhere,
   2557        // so here we set it back (see Bug 1955040)
   2558        if (container.children.contains(previouslyActiveElement)) {
   2559          previouslyActiveElement.focus({
   2560            // don't scroll the item into view, the user might have scrolled away and we
   2561            // don't want to disturb them.
   2562            preventScroll: true,
   2563          });
   2564        }
   2565        return container;
   2566      })
   2567      .catch(this._handleRejectionIfNotDestroyed);
   2568    this._queuedChildUpdates.set(container, updatePromise);
   2569    return updatePromise;
   2570  }
   2571 
   2572  buildMoreNodesButtonMarkup(container) {
   2573    const elt = this.doc.createElement("li");
   2574    elt.classList.add("more-nodes", "devtools-class-comment");
   2575 
   2576    const label = this.doc.createElement("span");
   2577    label.textContent = INSPECTOR_L10N.getStr("markupView.more.showing");
   2578    elt.appendChild(label);
   2579 
   2580    const button = this.doc.createElement("button");
   2581    button.setAttribute("href", "#");
   2582    const showAllString = PluralForm.get(
   2583      container.node.numChildren,
   2584      INSPECTOR_L10N.getStr("markupView.more.showAll2")
   2585    );
   2586    button.textContent = showAllString.replace(
   2587      "#1",
   2588      container.node.numChildren
   2589    );
   2590    elt.appendChild(button);
   2591 
   2592    button.addEventListener("click", () => {
   2593      container.maxChildren = -1;
   2594      this._forceUpdateChildren(container);
   2595    });
   2596 
   2597    return elt;
   2598  }
   2599 
   2600  _waitForChildren() {
   2601    if (!this._queuedChildUpdates) {
   2602      return Promise.resolve(undefined);
   2603    }
   2604 
   2605    return Promise.all([...this._queuedChildUpdates.values()]);
   2606  }
   2607 
   2608  /**
   2609   * Return a list of the children to display for this container.
   2610   */
   2611  async _getVisibleChildren(container, centered) {
   2612    let maxChildren = container.maxChildren || this.maxChildren;
   2613    if (maxChildren == -1) {
   2614      maxChildren = undefined;
   2615    }
   2616 
   2617    // We have to use node's walker and not a top level walker
   2618    // as for fission frames, we are going to have multiple walkers
   2619    const inspectorFront =
   2620      await container.node.targetFront.getFront("inspector");
   2621    return inspectorFront.walker.children(container.node, {
   2622      maxNodes: maxChildren,
   2623      center: centered,
   2624    });
   2625  }
   2626 
   2627  /**
   2628   * The parent of a given node as rendered in the markup view is not necessarily
   2629   * node.parentNode(). For instance, shadow roots don't have a parentNode, but a host
   2630   * element. However they are represented as parent and children in the markup view.
   2631   *
   2632   * Use this method when you are interested in the parent of a node from the perspective
   2633   * of the markup-view tree, and not from the perspective of the actual DOM.
   2634   */
   2635  _getParentInTree(node) {
   2636    const parent = node.parentOrHost();
   2637    if (!parent) {
   2638      return null;
   2639    }
   2640 
   2641    // If the parent node belongs to a different target while the node's target is the
   2642    // one selected by the user in the iframe picker, we don't want to go further up.
   2643    if (
   2644      node.targetFront !== parent.targetFront &&
   2645      node.targetFront ==
   2646        this.inspector.commands.targetCommand.selectedTargetFront
   2647    ) {
   2648      return null;
   2649    }
   2650 
   2651    return parent;
   2652  }
   2653 
   2654  /**
   2655   * Tear down the markup panel.
   2656   */
   2657  destroy() {
   2658    if (this._destroyed) {
   2659      return;
   2660    }
   2661 
   2662    this._destroyed = true;
   2663 
   2664    this._hoveredContainer = null;
   2665 
   2666    if (this._contextMenu) {
   2667      this._contextMenu.destroy();
   2668      this._contextMenu = null;
   2669    }
   2670 
   2671    if (this._eventDetailsTooltip) {
   2672      this._eventDetailsTooltip.destroy();
   2673      this._eventDetailsTooltip = null;
   2674    }
   2675 
   2676    if (this.htmlEditor) {
   2677      this.htmlEditor.destroy();
   2678      this.htmlEditor = null;
   2679    }
   2680 
   2681    if (this.imagePreviewTooltip) {
   2682      this.imagePreviewTooltip.destroy();
   2683      this.imagePreviewTooltip = null;
   2684    }
   2685 
   2686    if (this._undo) {
   2687      this._undo.destroy();
   2688      this._undo = null;
   2689    }
   2690 
   2691    if (this._shortcuts) {
   2692      this._shortcuts.destroy();
   2693      this._shortcuts = null;
   2694    }
   2695 
   2696    this.popup.destroy();
   2697    this.popup = null;
   2698    this._selectedContainer = null;
   2699 
   2700    this._elt.removeEventListener("blur", this._onBlur, true);
   2701    this._elt.removeEventListener("click", this._onMouseClick);
   2702    this._elt.removeEventListener("contextmenu", this._onContextMenu);
   2703    this._elt.removeEventListener("mousemove", this._onMouseMove);
   2704    this._elt.removeEventListener("mouseout", this._onMouseOut);
   2705    this._frame.removeEventListener("focus", this._onFocus);
   2706    this._unsubscribeFromToolboxStore();
   2707    this.inspector.selection.off("new-node-front", this._onNewSelection);
   2708    this.inspector.off(
   2709      "search-cleared",
   2710      this._updateSearchResultsHighlightingInSelectedNode
   2711    );
   2712    this.resourceCommand.unwatchResources(
   2713      [this.resourceCommand.TYPES.ROOT_NODE],
   2714      { onAvailable: this._onResourceAvailable }
   2715    );
   2716    this.targetCommand.unwatchTargets({
   2717      types: [this.targetCommand.TYPES.FRAME],
   2718      onAvailable: this._onTargetAvailable,
   2719      onDestroyed: this._onTargetDestroyed,
   2720    });
   2721    this.inspector.toolbox.nodePicker.off(
   2722      "picker-node-hovered",
   2723      this._onToolboxPickerHover
   2724    );
   2725    this.inspector.toolbox.nodePicker.off(
   2726      "picker-node-canceled",
   2727      this._onToolboxPickerCanceled
   2728    );
   2729    this.inspector.highlighters.off(
   2730      "highlighter-shown",
   2731      this.onHighlighterShown
   2732    );
   2733    this.inspector.highlighters.off(
   2734      "highlighter-hidden",
   2735      this.onHighlighterHidden
   2736    );
   2737    this.inspector.toolbox.off("select", this._onToolboxSelect);
   2738    this.win.removeEventListener("copy", this._onCopy);
   2739    this.win.removeEventListener("mouseup", this._onMouseUp);
   2740 
   2741    this._walkerEventListener.destroy();
   2742    this._walkerEventListener = null;
   2743 
   2744    this._prefObserver.off(
   2745      ATTR_COLLAPSE_ENABLED_PREF,
   2746      this._onCollapseAttributesPrefChange
   2747    );
   2748    this._prefObserver.off(
   2749      ATTR_COLLAPSE_LENGTH_PREF,
   2750      this._onCollapseAttributesPrefChange
   2751    );
   2752    this._prefObserver.destroy();
   2753 
   2754    for (const [, container] of this._containers) {
   2755      container.destroy();
   2756    }
   2757    this._containers = null;
   2758 
   2759    this._elt.innerHTML = "";
   2760    this._elt = null;
   2761 
   2762    this._selectionController = null;
   2763    this.controllerWindow = null;
   2764    this.doc = null;
   2765    this.highlighters = null;
   2766    this.walker = null;
   2767    this.resourceCommand = null;
   2768    this.win = null;
   2769 
   2770    this._lastDropTarget = null;
   2771    this._lastDragTarget = null;
   2772  }
   2773 
   2774  /**
   2775   * Find the closest element with class tag-line. These are used to indicate
   2776   * drag and drop targets.
   2777   *
   2778   * @param  {DOMNode} el
   2779   * @return {DOMNode}
   2780   */
   2781  findClosestDragDropTarget(el) {
   2782    return el.classList.contains("tag-line")
   2783      ? el
   2784      : el.querySelector(".tag-line") || el.closest(".tag-line");
   2785  }
   2786 
   2787  /**
   2788   * Takes an element as it's only argument and marks the element
   2789   * as the drop target
   2790   */
   2791  indicateDropTarget(el) {
   2792    if (this._lastDropTarget) {
   2793      this._lastDropTarget.classList.remove("drop-target");
   2794    }
   2795 
   2796    if (!el) {
   2797      return;
   2798    }
   2799 
   2800    const target = this.findClosestDragDropTarget(el);
   2801    if (target) {
   2802      target.classList.add("drop-target");
   2803      this._lastDropTarget = target;
   2804    }
   2805  }
   2806 
   2807  /**
   2808   * Takes an element to mark it as indicator of dragging target's initial place
   2809   */
   2810  indicateDragTarget(el) {
   2811    if (this._lastDragTarget) {
   2812      this._lastDragTarget.classList.remove("drag-target");
   2813    }
   2814 
   2815    if (!el) {
   2816      return;
   2817    }
   2818 
   2819    const target = this.findClosestDragDropTarget(el);
   2820    if (target) {
   2821      target.classList.add("drag-target");
   2822      this._lastDragTarget = target;
   2823    }
   2824  }
   2825 
   2826  /**
   2827   * Used to get the nodes required to modify the markup after dragging the
   2828   * element (parent/nextSibling).
   2829   */
   2830  get dropTargetNodes() {
   2831    const target = this._lastDropTarget;
   2832 
   2833    if (!target) {
   2834      return null;
   2835    }
   2836 
   2837    let parent, nextSibling;
   2838 
   2839    if (
   2840      target.previousElementSibling &&
   2841      target.previousElementSibling.nodeName.toLowerCase() === "ul"
   2842    ) {
   2843      parent = target.parentNode.container.node;
   2844      nextSibling = null;
   2845    } else {
   2846      parent = target.parentNode.container.node.parentNode();
   2847      nextSibling = target.parentNode.container.node;
   2848    }
   2849 
   2850    if (nextSibling) {
   2851      while (
   2852        nextSibling.displayName === "::marker" ||
   2853        nextSibling.displayName === "::before"
   2854      ) {
   2855        nextSibling =
   2856          this.getContainer(nextSibling).elt.nextSibling.container.node;
   2857      }
   2858      if (nextSibling.displayName === "::after") {
   2859        parent = target.parentNode.container.node.parentNode();
   2860        nextSibling = null;
   2861      }
   2862    }
   2863 
   2864    if (parent.nodeType !== nodeConstants.ELEMENT_NODE) {
   2865      return null;
   2866    }
   2867 
   2868    return { parent, nextSibling };
   2869  }
   2870 }
   2871 
   2872 /**
   2873 * Copy the content of a longString containing HTML code to the clipboard.
   2874 * The string is retrieved, and possibly beautified if the user has the right pref set and
   2875 * then placed in the clipboard.
   2876 *
   2877 * @param  {Promise} longStringActorPromise
   2878 *         The promise expected to resolve a LongStringActor instance
   2879 */
   2880 async function copyLongHTMLString(longStringActorPromise) {
   2881  let string = await getLongString(longStringActorPromise);
   2882 
   2883  if (Services.prefs.getBoolPref(BEAUTIFY_HTML_ON_COPY_PREF)) {
   2884    const { indentUnit, indentWithTabs } = getTabPrefs();
   2885    string = beautify.html(string, {
   2886      // eslint-disable-next-line camelcase
   2887      preserve_newlines: false,
   2888      // eslint-disable-next-line camelcase
   2889      indent_size: indentWithTabs ? 1 : indentUnit,
   2890      // eslint-disable-next-line camelcase
   2891      indent_char: indentWithTabs ? "\t" : " ",
   2892      unformatted: [],
   2893    });
   2894  }
   2895 
   2896  clipboardHelper.copyString(string);
   2897 }
   2898 
   2899 /**
   2900 * Map a number from one range to another.
   2901 */
   2902 function map(value, oldMin, oldMax, newMin, newMax) {
   2903  const ratio = oldMax - oldMin;
   2904  if (ratio == 0) {
   2905    return value;
   2906  }
   2907  return newMin + (newMax - newMin) * ((value - oldMin) / ratio);
   2908 }
   2909 
   2910 module.exports = MarkupView;