Quellcodebibliothek Statistik Leitseite products/Sources/formale Sprachen/C/Firefox/devtools/client/inspector/markup/   (Browser von der Mozilla Stiftung Version 136.0.1©)  Datei vom 10.2.2025 mit Größe 80 kB image not shown  

Quelle  markup.js   Sprache: JAVA

 
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */


"use strict";

const flags = require("resource://devtools/shared/flags.js");
const nodeConstants = require("resource://devtools/shared/dom-node-constants.js");
const nodeFilterConstants = require("resource://devtools/shared/dom-node-filter-constants.js");
const EventEmitter = require("resource://devtools/shared/event-emitter.js");
const { LocalizationHelper } = require("resource://devtools/shared/l10n.js");
const { PluralForm } = require("resource://devtools/shared/plural-form.js");
const AutocompletePopup = require("resource://devtools/client/shared/autocomplete-popup.js");
const KeyShortcuts = require("resource://devtools/client/shared/key-shortcuts.js");
const {
  scrollIntoViewIfNeeded,
} = require("resource://devtools/client/shared/scroll.js");
const { PrefObserver } = require("resource://devtools/client/shared/prefs.js");
const MarkupElementContainer = require("resource://devtools/client/inspector/markup/views/element-container.js");
const MarkupReadOnlyContainer = require("resource://devtools/client/inspector/markup/views/read-only-container.js");
const MarkupTextContainer = require("resource://devtools/client/inspector/markup/views/text-container.js");
const RootContainer = require("resource://devtools/client/inspector/markup/views/root-container.js");
const WalkerEventListener = require("resource://devtools/client/inspector/shared/walker-event-listener.js");

loader.lazyRequireGetter(
  this,
  ["createDOMMutationBreakpoint""deleteDOMMutationBreakpoint"],
  "resource://devtools/client/framework/actions/index.js",
  true
);
loader.lazyRequireGetter(
  this,
  "MarkupContextMenu",
  "resource://devtools/client/inspector/markup/markup-context-menu.js"
);
loader.lazyRequireGetter(
  this,
  "SlottedNodeContainer",
  "resource://devtools/client/inspector/markup/views/slotted-node-container.js"
);
loader.lazyRequireGetter(
  this,
  "getLongString",
  "resource://devtools/client/inspector/shared/utils.js",
  true
);
loader.lazyRequireGetter(
  this,
  "openContentLink",
  "resource://devtools/client/shared/link.js",
  true
);
loader.lazyRequireGetter(
  this,
  "HTMLTooltip",
  "resource://devtools/client/shared/widgets/tooltip/HTMLTooltip.js",
  true
);
loader.lazyRequireGetter(
  this,
  "UndoStack",
  "resource://devtools/client/shared/undo.js",
  true
);
loader.lazyRequireGetter(
  this,
  "clipboardHelper",
  "resource://devtools/shared/platform/clipboard.js"
);
loader.lazyRequireGetter(
  this,
  "beautify",
  "resource://devtools/shared/jsbeautify/beautify.js"
);
loader.lazyRequireGetter(
  this,
  "getTabPrefs",
  "resource://devtools/shared/indentation.js",
  true
);

const INSPECTOR_L10N = new LocalizationHelper(
  "devtools/client/locales/inspector.properties"
);

// Page size for pageup/pagedown
const PAGE_SIZE = 10;
const DEFAULT_MAX_CHILDREN = 100;
const DRAG_DROP_AUTOSCROLL_EDGE_MAX_DISTANCE = 50;
const DRAG_DROP_AUTOSCROLL_EDGE_RATIO = 0.1;
const DRAG_DROP_MIN_AUTOSCROLL_SPEED = 2;
const DRAG_DROP_MAX_AUTOSCROLL_SPEED = 8;
const DRAG_DROP_HEIGHT_TO_SPEED = 500;
const DRAG_DROP_HEIGHT_TO_SPEED_MIN = 0.5;
const DRAG_DROP_HEIGHT_TO_SPEED_MAX = 1;
const ATTR_COLLAPSE_ENABLED_PREF = "devtools.markup.collapseAttributes";
const ATTR_COLLAPSE_LENGTH_PREF = "devtools.markup.collapseAttributeLength";
const BEAUTIFY_HTML_ON_COPY_PREF = "devtools.markup.beautifyOnCopy";

/**
 * These functions are called when a shortcut (as defined in `_initShortcuts`) occurs.
 * Each property in the following object corresponds to one of the shortcut that is
 * handled by the markup-view.
 * Each property value is a function that takes the markup-view instance as only
 * argument, and returns a boolean that signifies whether the event should be consumed.
 * By default, the event gets consumed after the shortcut handler returns,
 * this means its propagation is stopped. If you do want the shortcut event
 * to continue propagating through DevTools, then return true from the handler.
 */

const shortcutHandlers = {
  // Localizable keys
  "markupView.hide.key": markupView => {
    const node = markupView._selectedContainer.node;
    const walkerFront = node.walkerFront;

    if (node.hidden) {
      walkerFront.unhideNode(node);
    } else {
      walkerFront.hideNode(node);
    }
  },
  "markupView.edit.key": markupView => {
    markupView.beginEditingHTML(markupView._selectedContainer.node);
  },
  "markupView.scrollInto.key": markupView => {
    markupView.scrollNodeIntoView();
  },
  // Generic keys
  Delete: markupView => {
    markupView.deleteNodeOrAttribute();
  },
  Backspace: markupView => {
    markupView.deleteNodeOrAttribute(true);
  },
  Home: markupView => {
    const rootContainer = markupView.getContainer(markupView._rootNode);
    markupView.navigate(rootContainer.children.firstChild.container);
  },
  Left: markupView => {
    if (markupView._selectedContainer.expanded) {
      markupView.collapseNode(markupView._selectedContainer.node);
    } else {
      const parent = markupView._selectionWalker().parentNode();
      if (parent) {
        markupView.navigate(parent.container);
      }
    }
  },
  Right: markupView => {
    if (
      !markupView._selectedContainer.expanded &&
      markupView._selectedContainer.hasChildren
    ) {
      markupView._expandContainer(markupView._selectedContainer);
    } else {
      const next = markupView._selectionWalker().nextNode();
      if (next) {
        markupView.navigate(next.container);
      }
    }
  },
  Up: markupView => {
    const previousNode = markupView._selectionWalker().previousNode();
    if (previousNode) {
      markupView.navigate(previousNode.container);
    }
  },
  Down: markupView => {
    const nextNode = markupView._selectionWalker().nextNode();
    if (nextNode) {
      markupView.navigate(nextNode.container);
    }
  },
  PageUp: markupView => {
    const walker = markupView._selectionWalker();
    let selection = markupView._selectedContainer;
    for (let i = 0; i < PAGE_SIZE; i++) {
      const previousNode = walker.previousNode();
      if (!previousNode) {
        break;
      }
      selection = previousNode.container;
    }
    markupView.navigate(selection);
  },
  PageDown: markupView => {
    const walker = markupView._selectionWalker();
    let selection = markupView._selectedContainer;
    for (let i = 0; i < PAGE_SIZE; i++) {
      const nextNode = walker.nextNode();
      if (!nextNode) {
        break;
      }
      selection = nextNode.container;
    }
    markupView.navigate(selection);
  },
  Enter: markupView => {
    if (!markupView._selectedContainer.canFocus) {
      markupView._selectedContainer.canFocus = true;
      markupView._selectedContainer.focus();
      return false;
    }
    return true;
  },
  Space: markupView => {
    if (!markupView._selectedContainer.canFocus) {
      markupView._selectedContainer.canFocus = true;
      markupView._selectedContainer.focus();
      return false;
    }
    return true;
  },
  Esc: markupView => {
    if (markupView.isDragging) {
      markupView.cancelDragging();
      return false;
    }
    // Prevent cancelling the event when not
    // dragging, to allow the split console to be toggled.
    return true;
  },
};

/**
 * Vocabulary for the purposes of this file:
 *
 * MarkupContainer - the structure that holds an editor and its
 *  immediate children in the markup panel.
 *  - MarkupElementContainer: markup container for element nodes
 *  - MarkupTextContainer: markup container for text / comment nodes
 *  - MarkupReadonlyContainer: markup container for other nodes
 * Node - A content node.
 * object.elt - A UI element in the markup panel.
 */


/**
 * The markup tree.  Manages the mapping of nodes to MarkupContainers,
 * updating based on mutations, and the undo/redo bindings.
 *
 * @param  {Inspector} inspector
 *         The inspector we're watching.
 * @param  {iframe} frame
 *         An iframe in which the caller has kindly loaded markup.xhtml.
 * @param  {XULWindow} controllerWindow
 *         Will enable the undo/redo feature from devtools/client/shared/undo.
 *         Should be a XUL window, will typically point to the toolbox window.
 */

function MarkupView(inspector, frame, controllerWindow) {
  EventEmitter.decorate(this);

  this.controllerWindow = controllerWindow;
  this.inspector = inspector;
  this.highlighters = inspector.highlighters;
  this.walker = this.inspector.walker;
  this._frame = frame;
  this.win = this._frame.contentWindow;
  this.doc = this._frame.contentDocument;
  this._elt = this.doc.getElementById("root");
  this.telemetry = this.inspector.telemetry;
  this._breakpointIDsInLocalState = new Map();
  this._containersToUpdate = new Map();

  this.maxChildren = Services.prefs.getIntPref(
    "devtools.markup.pagesize",
    DEFAULT_MAX_CHILDREN
  );

  this.collapseAttributes = Services.prefs.getBoolPref(
    ATTR_COLLAPSE_ENABLED_PREF
  );
  this.collapseAttributeLength = Services.prefs.getIntPref(
    ATTR_COLLAPSE_LENGTH_PREF
  );

  // Creating the popup to be used to show CSS suggestions.
  // The popup will be attached to the toolbox document.
  this.popup = new AutocompletePopup(inspector.toolbox.doc, {
    autoSelect: true,
  });

  this._containers = new Map();
  // This weakmap will hold keys used with the _containers map, in order to retrieve the
  // slotted container for a given node front.
  this._slottedContainerKeys = new WeakMap();

  // Binding functions that need to be called in scope.
  this._handleRejectionIfNotDestroyed =
    this._handleRejectionIfNotDestroyed.bind(this);
  this._isImagePreviewTarget = this._isImagePreviewTarget.bind(this);
  this._onWalkerMutations = this._onWalkerMutations.bind(this);
  this._onBlur = this._onBlur.bind(this);
  this._onContextMenu = this._onContextMenu.bind(this);
  this._onCopy = this._onCopy.bind(this);
  this._onCollapseAttributesPrefChange =
    this._onCollapseAttributesPrefChange.bind(this);
  this._onWalkerNodeStatesChanged = this._onWalkerNodeStatesChanged.bind(this);
  this._onFocus = this._onFocus.bind(this);
  this._onResourceAvailable = this._onResourceAvailable.bind(this);
  this._onTargetAvailable = this._onTargetAvailable.bind(this);
  this._onTargetDestroyed = this._onTargetDestroyed.bind(this);
  this._onMouseClick = this._onMouseClick.bind(this);
  this._onMouseMove = this._onMouseMove.bind(this);
  this._onMouseOut = this._onMouseOut.bind(this);
  this._onMouseUp = this._onMouseUp.bind(this);
  this._onNewSelection = this._onNewSelection.bind(this);
  this._onToolboxPickerCanceled = this._onToolboxPickerCanceled.bind(this);
  this._onToolboxPickerHover = this._onToolboxPickerHover.bind(this);
  this._onDomMutation = this._onDomMutation.bind(this);

  // Listening to various events.
  this._elt.addEventListener("blur"this._onBlur, true);
  this._elt.addEventListener("click"this._onMouseClick);
  this._elt.addEventListener("contextmenu"this._onContextMenu);
  this._elt.addEventListener("mousemove"this._onMouseMove);
  this._elt.addEventListener("mouseout"this._onMouseOut);
  this._frame.addEventListener("focus"this._onFocus);
  this.inspector.selection.on("new-node-front"this._onNewSelection);
  this._unsubscribeFromToolboxStore = this.inspector.toolbox.store.subscribe(
    this._onDomMutation
  );

  if (flags.testing) {
    // In tests, we start listening immediately to avoid having to simulate a mousemove.
    this._initTooltips();
  }

  this.win.addEventListener("copy"this._onCopy);
  this.win.addEventListener("mouseup"this._onMouseUp);
  this.inspector.toolbox.nodePicker.on(
    "picker-node-canceled",
    this._onToolboxPickerCanceled
  );
  this.inspector.toolbox.nodePicker.on(
    "picker-node-hovered",
    this._onToolboxPickerHover
  );

  // Event listeners for highlighter events
  this.onHighlighterShown = data =>
    this.handleHighlighterEvent("highlighter-shown", data);
  this.onHighlighterHidden = data =>
    this.handleHighlighterEvent("highlighter-hidden", data);
  this.inspector.highlighters.on("highlighter-shown"this.onHighlighterShown);
  this.inspector.highlighters.on(
    "highlighter-hidden",
    this.onHighlighterHidden
  );

  this._onNewSelection();
  if (this.inspector.selection.nodeFront) {
    this.expandNode(this.inspector.selection.nodeFront);
  }

  this._prefObserver = new PrefObserver("devtools.markup");
  this._prefObserver.on(
    ATTR_COLLAPSE_ENABLED_PREF,
    this._onCollapseAttributesPrefChange
  );
  this._prefObserver.on(
    ATTR_COLLAPSE_LENGTH_PREF,
    this._onCollapseAttributesPrefChange
  );

  this._initShortcuts();

  this._walkerEventListener = new WalkerEventListener(this.inspector, {
    "container-type-change"this._onWalkerNodeStatesChanged,
    "display-change"this._onWalkerNodeStatesChanged,
    "scrollable-change"this._onWalkerNodeStatesChanged,
    "overflow-change"this._onWalkerNodeStatesChanged,
    mutations: this._onWalkerMutations,
  });

  this.resourceCommand = this.inspector.toolbox.resourceCommand;
  this.resourceCommand.watchResources([this.resourceCommand.TYPES.ROOT_NODE], {
    onAvailable: this._onResourceAvailable,
  });

  this.targetCommand = this.inspector.commands.targetCommand;
  this.targetCommand.watchTargets({
    types: [this.targetCommand.TYPES.FRAME],
    onAvailable: this._onTargetAvailable,
    onDestroyed: this._onTargetDestroyed,
  });
}

MarkupView.prototype = {
  /**
   * How long does a node flash when it mutates (in ms).
   */

  CONTAINER_FLASHING_DURATION: 500,

  _selectedContainer: null,

  get contextMenu() {
    if (!this._contextMenu) {
      this._contextMenu = new MarkupContextMenu(this);
    }

    return this._contextMenu;
  },

  hasEventDetailsTooltip() {
    return !!this._eventDetailsTooltip;
  },

  get eventDetailsTooltip() {
    if (!this._eventDetailsTooltip) {
      // This tooltip will be attached to the toolbox document.
      this._eventDetailsTooltip = new HTMLTooltip(this.toolbox.doc, {
        type: "arrow",
        consumeOutsideClicks: false,
      });
    }

    return this._eventDetailsTooltip;
  },

  get toolbox() {
    return this.inspector.toolbox;
  },

  get undo() {
    if (!this._undo) {
      this._undo = new UndoStack();
      this._undo.installController(this.controllerWindow);
    }

    return this._undo;
  },

  _onDomMutation() {
    const domMutationBreakpoints =
      this.inspector.toolbox.store.getState().domMutationBreakpoints
        .breakpoints;
    const breakpointIDsInCurrentState = [];
    for (const breakpoint of domMutationBreakpoints) {
      const nodeFront = breakpoint.nodeFront;
      const mutationType = breakpoint.mutationType;
      const enabledStatus = breakpoint.enabled;
      breakpointIDsInCurrentState.push(breakpoint.id);
      // If breakpoint is not in local state
      if (!this._breakpointIDsInLocalState.has(breakpoint.id)) {
        this._breakpointIDsInLocalState.set(breakpoint.id, breakpoint);
        if (!this._containersToUpdate.has(nodeFront)) {
          this._containersToUpdate.set(nodeFront, new Map());
        }
      }
      this._containersToUpdate.get(nodeFront).set(mutationType, enabledStatus);
    }
    // If a breakpoint is in local state but not current state, it has been
    // removed by the user.
    for (const id of this._breakpointIDsInLocalState.keys()) {
      if (breakpointIDsInCurrentState.includes(id) === false) {
        const nodeFront = this._breakpointIDsInLocalState.get(id).nodeFront;
        const mutationType =
          this._breakpointIDsInLocalState.get(id).mutationType;
        this._containersToUpdate.get(nodeFront).delete(mutationType);
        this._breakpointIDsInLocalState.delete(id);
      }
    }
    // Update each container
    for (const nodeFront of this._containersToUpdate.keys()) {
      const mutationBreakpoints = this._containersToUpdate.get(nodeFront);
      const container = this.getContainer(nodeFront);
      container.update(mutationBreakpoints);
      if (this._containersToUpdate.get(nodeFront).size === 0) {
        this._containersToUpdate.delete(nodeFront);
      }
    }
  },

  /**
   * Handle promise rejections for various asynchronous actions, and only log errors if
   * the markup view still exists.
   * This is useful to silence useless errors that happen when the markup view is
   * destroyed while still initializing (and making protocol requests).
   */

  _handleRejectionIfNotDestroyed(e) {
    if (!this._destroyed) {
      console.error(e);
    }
  },

  _initTooltips() {
    if (this.imagePreviewTooltip) {
      return;
    }
    // The tooltips will be attached to the toolbox document.
    this.imagePreviewTooltip = new HTMLTooltip(this.toolbox.doc, {
      type: "arrow",
      useXulWrapper: true,
    });
    this._enableImagePreviewTooltip();
  },

  _enableImagePreviewTooltip() {
    if (!this.imagePreviewTooltip) {
      return;
    }
    this.imagePreviewTooltip.startTogglingOnHover(
      this._elt,
      this._isImagePreviewTarget
    );
  },

  _disableImagePreviewTooltip() {
    if (!this.imagePreviewTooltip) {
      return;
    }
    this.imagePreviewTooltip.stopTogglingOnHover();
  },

  _onToolboxPickerHover(nodeFront) {
    this.showNode(nodeFront).then(() => {
      this._showNodeAsHovered(nodeFront);
    }, console.error);
  },

  /**
   * If the element picker gets canceled, make sure and re-center the view on the
   * current selected element.
   */

  _onToolboxPickerCanceled() {
    if (this._selectedContainer) {
      scrollIntoViewIfNeeded(this._selectedContainer.editor.elt);
    }
  },

  isDragging: false,
  _draggedContainer: null,

  _onMouseMove(event) {
    // Note that in tests, we start listening immediately from the constructor to avoid having to simulate a mousemove.
    // Also note that initTooltips bails out if it is called many times, so it isn't an issue to call it a second
    // time from here in case tests are doing a mousemove.
    this._initTooltips();

    let target = event.target;

    if (this._draggedContainer) {
      this._draggedContainer.onMouseMove(event);
    }
    // Auto-scroll if we're dragging.
    if (this.isDragging) {
      event.preventDefault();
      this._autoScroll(event);
      return;
    }

    // Show the current container as hovered and highlight it.
    // This requires finding the current MarkupContainer (walking up the DOM).
    while (!target.container) {
      if (target.tagName.toLowerCase() === "body") {
        return;
      }
      target = target.parentNode;
    }

    const container = target.container;
    if (this._hoveredContainer !== container) {
      this._showBoxModel(container.node);
    }
    this._showContainerAsHovered(container);

    this.emit("node-hover");
  },

  /**
   * If focus is moved outside of the markup view document and there is a
   * selected container, make its contents not focusable by a keyboard.
   */

  _onBlur(event) {
    if (!this._selectedContainer) {
      return;
    }

    const { relatedTarget } = event;
    if (relatedTarget && relatedTarget.ownerDocument === this.doc) {
      return;
    }

    if (this._selectedContainer) {
      this._selectedContainer.clearFocus();
    }
  },

  _onContextMenu(event) {
    this.contextMenu.show(event);
  },

  /**
   * Executed on each mouse-move while a node is being dragged in the view.
   * Auto-scrolls the view to reveal nodes below the fold to drop the dragged
   * node in.
   */

  _autoScroll(event) {
    const docEl = this.doc.documentElement;

    if (this._autoScrollAnimationFrame) {
      this.win.cancelAnimationFrame(this._autoScrollAnimationFrame);
    }

    // Auto-scroll when the mouse approaches top/bottom edge.
    const fromBottom = docEl.clientHeight - event.pageY + this.win.scrollY;
    const fromTop = event.pageY - this.win.scrollY;
    const edgeDistance = Math.min(
      DRAG_DROP_AUTOSCROLL_EDGE_MAX_DISTANCE,
      docEl.clientHeight * DRAG_DROP_AUTOSCROLL_EDGE_RATIO
    );

    // The smaller the screen, the slower the movement.
    const heightToSpeedRatio = Math.max(
      DRAG_DROP_HEIGHT_TO_SPEED_MIN,
      Math.min(
        DRAG_DROP_HEIGHT_TO_SPEED_MAX,
        docEl.clientHeight / DRAG_DROP_HEIGHT_TO_SPEED
      )
    );

    if (fromBottom <= edgeDistance) {
      // Map our distance range to a speed range so that the speed is not too
      // fast or too slow.
      const speed = map(
        fromBottom,
        0,
        edgeDistance,
        DRAG_DROP_MIN_AUTOSCROLL_SPEED,
        DRAG_DROP_MAX_AUTOSCROLL_SPEED
      );

      this._runUpdateLoop(() => {
        docEl.scrollTop -=
          heightToSpeedRatio * (speed - DRAG_DROP_MAX_AUTOSCROLL_SPEED);
      });
    }

    if (fromTop <= edgeDistance) {
      const speed = map(
        fromTop,
        0,
        edgeDistance,
        DRAG_DROP_MIN_AUTOSCROLL_SPEED,
        DRAG_DROP_MAX_AUTOSCROLL_SPEED
      );

      this._runUpdateLoop(() => {
        docEl.scrollTop +=
          heightToSpeedRatio * (speed - DRAG_DROP_MAX_AUTOSCROLL_SPEED);
      });
    }
  },

  /**
   * Run a loop on the requestAnimationFrame.
   */

  _runUpdateLoop(update) {
    const loop = () => {
      update();
      this._autoScrollAnimationFrame = this.win.requestAnimationFrame(loop);
    };
    loop();
  },

  _onMouseClick(event) {
    // From the target passed here, let's find the parent MarkupContainer
    // and forward the event if needed.
    let parentNode = event.target;
    let container;
    while (parentNode !== this.doc.body) {
      if (parentNode.container) {
        container = parentNode.container;
        break;
      }
      parentNode = parentNode.parentNode;
    }

    if (typeof container.onContainerClick === "function") {
      // Forward the event to the container if it implements onContainerClick.
      container.onContainerClick(event);
    }
  },

  _onMouseUp(event) {
    if (this._draggedContainer) {
      this._draggedContainer.onMouseUp(event);
    }

    this.indicateDropTarget(null);
    this.indicateDragTarget(null);
    if (this._autoScrollAnimationFrame) {
      this.win.cancelAnimationFrame(this._autoScrollAnimationFrame);
    }
  },

  _onCollapseAttributesPrefChange() {
    this.collapseAttributes = Services.prefs.getBoolPref(
      ATTR_COLLAPSE_ENABLED_PREF
    );
    this.collapseAttributeLength = Services.prefs.getIntPref(
      ATTR_COLLAPSE_LENGTH_PREF
    );
    this.update();
  },

  cancelDragging() {
    if (!this.isDragging) {
      return;
    }

    for (const [, container] of this._containers) {
      if (container.isDragging) {
        container.cancelDragging();
        break;
      }
    }

    this.indicateDropTarget(null);
    this.indicateDragTarget(null);
    if (this._autoScrollAnimationFrame) {
      this.win.cancelAnimationFrame(this._autoScrollAnimationFrame);
    }
  },

  _hoveredContainer: null,

  /**
   * Show a NodeFront's container as being hovered
   *
   * @param  {NodeFront} nodeFront
   *         The node to show as hovered
   */

  _showNodeAsHovered(nodeFront) {
    const container = this.getContainer(nodeFront);
    this._showContainerAsHovered(container);
  },

  _showContainerAsHovered(container) {
    if (this._hoveredContainer === container) {
      return;
    }

    if (this._hoveredContainer) {
      this._hoveredContainer.hovered = false;
    }

    container.hovered = true;
    this._hoveredContainer = container;
  },

  async _onMouseOut(event) {
    // Emulate mouseleave by skipping any relatedTarget inside the markup-view.
    if (this._elt.contains(event.relatedTarget)) {
      return;
    }

    if (this._autoScrollAnimationFrame) {
      this.win.cancelAnimationFrame(this._autoScrollAnimationFrame);
    }
    if (this.isDragging) {
      return;
    }

    await this._hideBoxModel();
    if (this._hoveredContainer) {
      this._hoveredContainer.hovered = false;
    }
    this._hoveredContainer = null;

    this.emit("leave");
  },

  /**
   * Show the Box Model Highlighter on a given node front
   *
   * @param  {NodeFront} nodeFront
   *         The node for which to show the highlighter.
   * @param  {Object} options
   *         Configuration object with options for the Box Model Highlighter.
   * @return {Promise} Resolves after the highlighter for this nodeFront is shown.
   */

  _showBoxModel(nodeFront, options) {
    return this.inspector.highlighters.showHighlighterTypeForNode(
      this.inspector.highlighters.TYPES.BOXMODEL,
      nodeFront,
      options
    );
  },

  /**
   * Hide the Box Model Highlighter for any node that may be highlighted.
   *
   * @return {Promise} Resolves when the highlighter is hidden.
   */

  _hideBoxModel() {
    return this.inspector.highlighters.hideHighlighterType(
      this.inspector.highlighters.TYPES.BOXMODEL
    );
  },

  /**
   * Delegate handler for highlighter events.
   *
   * This is the place to observe for highlighter events, check the highlighter type and
   * event name, then react for example by modifying the DOM.
   *
   * @param {String} eventName
   *        Highlighter event name. One of: "highlighter-hidden", "highlighter-shown"
   * @param {Object} data
   *        Object with data associated with the highlighter event.
   *        {String} data.type
   *        Highlighter type
   *        {NodeFront} data.nodeFront
   *        NodeFront of the node associated with the highlighter event
   *        {Object} data.options
   *        Optional configuration passed to the highlighter when shown
   *        {CustomHighlighterFront} data.highlighter
   *        Highlighter instance
   *
   */

  handleHighlighterEvent(eventName, data) {
    switch (data.type) {
      // Toggle the "active" CSS class name on flex and grid display badges next to
      // elements in the Markup view when a coresponding flex or grid highlighter is
      // shown or hidden for a node.
      case this.inspector.highlighters.TYPES.FLEXBOX:
      case this.inspector.highlighters.TYPES.GRID:
        const { nodeFront } = data;
        if (!nodeFront) {
          return;
        }

        // Find the badge corresponding to the node from the highlighter event payload.
        const container = this.getContainer(nodeFront);
        const badge = container?.editor?.displayBadge;
        if (badge) {
          const isActive = eventName == "highlighter-shown";
          badge.classList.toggle("active", isActive);
          badge.setAttribute("aria-pressed", isActive);
        }

        // There is a limit to how many grid highlighters can be active at the same time.
        // If the limit was reached, disable all non-active grid badges.
        if (data.type === this.inspector.highlighters.TYPES.GRID) {
          // Matches badges for "grid", "inline-grid" and "subgrid"
          const selector = "[data-display*='grid']:not(.active)";
          const isLimited =
            this.inspector.highlighters.isGridHighlighterLimitReached();
          Array.from(this._elt.querySelectorAll(selector)).map(el => {
            el.classList.toggle("interactive", !isLimited);
          });
        }
        break;
    }
  },

  /**
   * Used by tests
   */

  getSelectedContainer() {
    return this._selectedContainer;
  },

  /**
   * Get the MarkupContainer object for a given node, or undefined if
   * none exists.
   *
   * @param  {NodeFront} nodeFront
   *         The node to get the container for.
   * @param  {Boolean} slotted
   *         true to get the slotted version of the container.
   * @return {MarkupContainer} The container for the provided node.
   */

  getContainer(node, slotted) {
    const key = this._getContainerKey(node, slotted);
    return this._containers.get(key);
  },

  /**
   * Register a given container for a given node/slotted node.
   *
   * @param  {NodeFront} nodeFront
   *         The node to set the container for.
   * @param  {Boolean} slotted
   *         true if the container represents the slotted version of the node.
   */

  setContainer(node, container, slotted) {
    const key = this._getContainerKey(node, slotted);
    return this._containers.set(key, container);
  },

  /**
   * Check if a MarkupContainer object exists for a given node/slotted node
   *
   * @param  {NodeFront} nodeFront
   *         The node to check.
   * @param  {Boolean} slotted
   *         true to check for a container matching the slotted version of the node.
   * @return {Boolean} True if a container exists, false otherwise.
   */

  hasContainer(node, slotted) {
    const key = this._getContainerKey(node, slotted);
    return this._containers.has(key);
  },

  _getContainerKey(node, slotted) {
    if (!slotted) {
      return node;
    }

    if (!this._slottedContainerKeys.has(node)) {
      this._slottedContainerKeys.set(node, { node });
    }
    return this._slottedContainerKeys.get(node);
  },

  _isContainerSelected(container) {
    if (!container) {
      return false;
    }

    const selection = this.inspector.selection;
    return (
      container.node == selection.nodeFront &&
      container.isSlotted() == selection.isSlotted()
    );
  },

  update() {
    const updateChildren = node => {
      this.getContainer(node).update();
      for (const child of node.treeChildren()) {
        updateChildren(child);
      }
    };

    // Start with the documentElement
    let documentElement;
    for (const node of this._rootNode.treeChildren()) {
      if (node.isDocumentElement === true) {
        documentElement = node;
        break;
      }
    }

    // Recursively update each node starting with documentElement.
    updateChildren(documentElement);
  },

  /**
   * Executed when the mouse hovers over a target in the markup-view and is used
   * to decide whether this target should be used to display an image preview
   * tooltip.
   * Delegates the actual decision to the corresponding MarkupContainer instance
   * if one is found.
   *
   * @return {Promise} the promise returned by
   *         MarkupElementContainer._isImagePreviewTarget
   */

  async _isImagePreviewTarget(target) {
    // From the target passed here, let's find the parent MarkupContainer
    // and ask it if the tooltip should be shown
    if (this.isDragging) {
      return false;
    }

    let parent = target,
      container;
    while (parent) {
      if (parent.container) {
        container = parent.container;
        break;
      }
      parent = parent.parentNode;
    }

    if (container instanceof MarkupElementContainer) {
      return container.isImagePreviewTarget(target, this.imagePreviewTooltip);
    }

    return false;
  },

  /**
   * Given the known reason, should the current selection be briefly highlighted
   * In a few cases, we don't want to highlight the node:
   * - If the reason is null (used to reset the selection),
   * - if it's "inspector-default-selection" (initial node selected, either when
   *   opening the inspector or after a navigation/reload)
   * - if it's "picker-node-picked" or "picker-node-previewed" (node selected with the
   *   node picker. Note that this does not include the "Inspect element" context menu,
   *   which has a dedicated reason, "browser-context-menu").
   * - if it's "test" (this is a special case for mochitest. In tests, we often
   * need to select elements but don't necessarily want the highlighter to come
   * and go after a delay as this might break test scenarios)
   * We also do not want to start a brief highlight timeout if the node is
   * already being hovered over, since in that case it will already be
   * highlighted.
   */

  _shouldNewSelectionBeHighlighted() {
    const reason = this.inspector.selection.reason;
    const unwantedReasons = [
      "inspector-default-selection",
      "nodeselected",
      "picker-node-picked",
      "picker-node-previewed",
      "test",
    ];

    const isHighlight = this._isContainerSelected(this._hoveredContainer);
    return !isHighlight && reason && !unwantedReasons.includes(reason);
  },

  /**
   * React to new-node-front selection events.
   * Highlights the node if needed, and make sure it is shown and selected in
   * the view.
   */

  _onNewSelection(nodeFront, reason) {
    const selection = this.inspector.selection;
    // this will probably leak.
    // TODO: use resource api listeners?
    if (nodeFront) {
      nodeFront.walkerFront.on(
        "container-type-change",
        this._onWalkerNodeStatesChanged
      );
      nodeFront.walkerFront.on(
        "display-change",
        this._onWalkerNodeStatesChanged
      );
      nodeFront.walkerFront.on(
        "scrollable-change",
        this._onWalkerNodeStatesChanged
      );
      nodeFront.walkerFront.on(
        "overflow-change",
        this._onWalkerNodeStatesChanged
      );
      nodeFront.walkerFront.on("mutations"this._onWalkerMutations);
    }

    if (this.htmlEditor) {
      this.htmlEditor.hide();
    }
    if (this._isContainerSelected(this._hoveredContainer)) {
      this._hoveredContainer.hovered = false;
      this._hoveredContainer = null;
    }

    if (!selection.isNode()) {
      this.unmarkSelectedNode();
      return;
    }

    const done = this.inspector.updating("markup-view");
    let onShowBoxModel;

    // Highlight the element briefly if needed.
    if (this._shouldNewSelectionBeHighlighted()) {
      onShowBoxModel = this._showBoxModel(nodeFront, {
        duration: this.inspector.HIGHLIGHTER_AUTOHIDE_TIMER,
      });
    }

    const slotted = selection.isSlotted();
    const smoothScroll = reason === "reveal-from-slot";
    const onShow = this.showNode(selection.nodeFront, { slotted, smoothScroll })
      .then(() => {
        // We could be destroyed by now.
        if (this._destroyed) {
          return Promise.reject("markupview destroyed");
        }

        // Mark the node as selected.
        const container = this.getContainer(selection.nodeFront, slotted);
        this._markContainerAsSelected(container);

        // Make sure the new selection is navigated to.
        this.maybeNavigateToNewSelection();
        return undefined;
      })
      .catch(this._handleRejectionIfNotDestroyed);

    Promise.all([onShowBoxModel, onShow]).then(done);
  },

  /**
   * Maybe make selected the current node selection's MarkupContainer depending
   * on why the current node got selected.
   */

  async maybeNavigateToNewSelection() {
    const { reason, nodeFront } = this.inspector.selection;

    // The list of reasons that should lead to navigating to the node.
    const reasonsToNavigate = [
      // If the user picked an element with the element picker.
      "picker-node-picked",
      // If the user shift-clicked (previewed) an element.
      "picker-node-previewed",
      // If the user selected an element with the browser context menu.
      "browser-context-menu",
      // If the user added a new node by clicking in the inspector toolbar.
      "node-inserted",
    ];

    // If the user performed an action with a keyboard, move keyboard focus to
    // the markup tree container.
    if (reason && reason.endsWith("-keyboard")) {
      this.getContainer(this._rootNode).elt.focus();
    }

    if (reasonsToNavigate.includes(reason)) {
      // not sure this is necessary
      const root = await nodeFront.walkerFront.getRootNode();
      this.getContainer(root).elt.focus();
      this.navigate(this.getContainer(nodeFront));
    }
  },

  /**
   * Create a TreeWalker to find the next/previous
   * node for selection.
   */

  _selectionWalker(start) {
    const walker = this.doc.createTreeWalker(
      start || this._elt,
      nodeFilterConstants.SHOW_ELEMENT,
      function (element) {
        if (
          element.container &&
          element.container.elt === element &&
          element.container.visible
        ) {
          return nodeFilterConstants.FILTER_ACCEPT;
        }
        return nodeFilterConstants.FILTER_SKIP;
      }
    );
    walker.currentNode = this._selectedContainer.elt;
    return walker;
  },

  _onCopy(evt) {
    // Ignore copy events from editors
    if (this._isInputOrTextarea(evt.target)) {
      return;
    }

    const selection = this.inspector.selection;
    if (selection.isNode()) {
      this.copyOuterHTML();
    }
    evt.stopPropagation();
    evt.preventDefault();
  },

  /**
   * Copy the outerHTML of the selected Node to the clipboard.
   */

  copyOuterHTML() {
    if (!this.inspector.selection.isNode()) {
      return;
    }
    const node = this.inspector.selection.nodeFront;

    switch (node.nodeType) {
      case nodeConstants.ELEMENT_NODE:
        copyLongHTMLString(node.walkerFront.outerHTML(node));
        break;
      case nodeConstants.COMMENT_NODE:
        getLongString(node.getNodeValue()).then(comment => {
          clipboardHelper.copyString("");
        });
        break;
      case nodeConstants.DOCUMENT_TYPE_NODE:
        clipboardHelper.copyString(node.doctypeString);
        break;
    }
  },

  /**
   * Copy the innerHTML of the selected Node to the clipboard.
   */

  copyInnerHTML() {
    const nodeFront = this.inspector.selection.nodeFront;
    if (!this.inspector.selection.isNode()) {
      return;
    }

    copyLongHTMLString(nodeFront.walkerFront.innerHTML(nodeFront));
  },

  /**
   * Given a type and link found in a node's attribute in the markup-view,
   * attempt to follow that link (which may result in opening a new tab, the
   * style editor or debugger).
   */

  followAttributeLink(type, link) {
    if (!type || !link) {
      return;
    }

    const nodeFront = this.inspector.selection.nodeFront;
    if (type === "uri" || type === "cssresource" || type === "jsresource") {
      // Open link in a new tab.
      nodeFront.inspectorFront
        .resolveRelativeURL(link, this.inspector.selection.nodeFront)
        .then(url => {
          if (type === "uri") {
            openContentLink(url);
          } else if (type === "cssresource") {
            return this.toolbox.viewGeneratedSourceInStyleEditor(url);
          } else if (type === "jsresource") {
            return this.toolbox.viewGeneratedSourceInDebugger(url);
          }
          return null;
        })
        .catch(console.error);
    } else if (type == "idref") {
      // Select the node in the same document.
      nodeFront.walkerFront
        .getIdrefNode(nodeFront, CSS.escape(link))
        .then(node => {
          if (!node) {
            this.emitForTests("idref-attribute-link-failed");
            return;
          }
          this.inspector.selection.setNodeFront(node, {
            reason: "markup-attribute-link",
          });
        })
        .catch(console.error);
    }
  },

  /**
   * Register all key shortcuts.
   */

  _initShortcuts() {
    const shortcuts = new KeyShortcuts({
      window: this.win,
    });

    // Keep a pointer on shortcuts to destroy them when destroying the markup
    // view.
    this._shortcuts = shortcuts;

    this._onShortcut = this._onShortcut.bind(this);

    // Process localizable keys
    [
      "markupView.hide.key",
      "markupView.edit.key",
      "markupView.scrollInto.key",
    ].forEach(name => {
      const key = INSPECTOR_L10N.getStr(name);
      shortcuts.on(key, event => this._onShortcut(name, event));
    });

    // Process generic keys:
    [
      "Delete",
      "Backspace",
      "Home",
      "Left",
      "Right",
      "Up",
      "Down",
      "PageUp",
      "PageDown",
      "Esc",
      "Enter",
      "Space",
    ].forEach(key => {
      shortcuts.on(key, event => this._onShortcut(key, event));
    });
  },

  /**
   * Key shortcut listener.
   */

  _onShortcut(name, event) {
    if (this._isInputOrTextarea(event.target)) {
      return;
    }

    // If the selected element is a button (e.g. `flex` badge), we don't want to highjack
    // keyboard activation.
    if (
      event.target.closest(":is(button, [role=button])") &&
      (name === "Enter" || name === "Space")
    ) {
      return;
    }

    const handler = shortcutHandlers[name];
    const shouldPropagate = handler(this);
    if (shouldPropagate) {
      return;
    }

    event.stopPropagation();
    event.preventDefault();
  },

  /**
   * Check if a node is an input or textarea
   */

  _isInputOrTextarea(element) {
    const name = element.tagName.toLowerCase();
    return name === "input" || name === "textarea";
  },

  /**
   * If there's an attribute on the current node that's currently focused, then
   * delete this attribute, otherwise delete the node itself.
   *
   * @param  {Boolean} moveBackward
   *         If set to true and if we're deleting the node, focus the previous
   *         sibling after deletion, otherwise the next one.
   */

  deleteNodeOrAttribute(moveBackward) {
    const focusedAttribute = this.doc.activeElement
      ? this.doc.activeElement.closest(".attreditor")
      : null;
    if (focusedAttribute) {
      // The focused attribute might not be in the current selected container.
      const container = focusedAttribute.closest("li.child").container;
      container.removeAttribute(focusedAttribute.dataset.attr);
    } else {
      this.deleteNode(this._selectedContainer.node, moveBackward);
    }
  },

  /**
   * Returns a value indicating whether a node can be deleted.
   *
   * @param {NodeFront} nodeFront
   *        The node to test for deletion
   */

  isDeletable(nodeFront) {
    return !(
      nodeFront.isDocumentElement ||
      nodeFront.nodeType == nodeConstants.DOCUMENT_NODE ||
      nodeFront.nodeType == nodeConstants.DOCUMENT_TYPE_NODE ||
      nodeFront.nodeType == nodeConstants.DOCUMENT_FRAGMENT_NODE ||
      nodeFront.isAnonymous
    );
  },

  /**
   * Delete a node from the DOM.
   * This is an undoable action.
   *
   * @param  {NodeFront} node
   *         The node to remove.
   * @param  {Boolean} moveBackward
   *         If set to true, focus the previous sibling, otherwise the next one.
   */

  deleteNode(node, moveBackward) {
    if (!this.isDeletable(node)) {
      return;
    }

    const container = this.getContainer(node);

    // Retain the node so we can undo this...
    node.walkerFront
      .retainNode(node)
      .then(() => {
        const parent = node.parentNode();
        let nextSibling = null;
        this.undo.do(
          () => {
            node.walkerFront.removeNode(node).then(siblings => {
              nextSibling = siblings.nextSibling;
              const prevSibling = siblings.previousSibling;
              let focusNode = moveBackward ? prevSibling : nextSibling;

              // If we can't move as the user wants, we move to the other direction.
              // If there is no sibling elements anymore, move to the parent node.
              if (!focusNode) {
                focusNode = nextSibling || prevSibling || parent;
              }

              const isNextSiblingText = nextSibling
                ? nextSibling.nodeType === nodeConstants.TEXT_NODE
                : false;
              const isPrevSiblingText = prevSibling
                ? prevSibling.nodeType === nodeConstants.TEXT_NODE
                : false;

              // If the parent had two children and the next or previous sibling
              // is a text node, then it now has only a single text node, is about
              // to be in-lined; and focus should move to the parent.
              if (
                parent.numChildren === 2 &&
                (isNextSiblingText || isPrevSiblingText)
              ) {
                focusNode = parent;
              }

              if (container.selected) {
                this.navigate(this.getContainer(focusNode));
              }
            });
          },
          () => {
            const isValidSibling = nextSibling && !nextSibling.isPseudoElement;
            nextSibling = isValidSibling ? nextSibling : null;
            node.walkerFront.insertBefore(node, parent, nextSibling);
          }
        );
      })
      .catch(console.error);
  },

  /**
   * Scroll the node into view.
   */

  scrollNodeIntoView() {
    if (!this.inspector.selection.isNode()) {
      return;
    }

    this.inspector.selection.nodeFront.scrollIntoView();
  },

  async toggleMutationBreakpoint(name) {
    if (!this.inspector.selection.isElementNode()) {
      return;
    }

    const toolboxStore = this.inspector.toolbox.store;
    const nodeFront = this.inspector.selection.nodeFront;

    if (nodeFront.mutationBreakpoints[name]) {
      toolboxStore.dispatch(deleteDOMMutationBreakpoint(nodeFront, name));
    } else {
      toolboxStore.dispatch(createDOMMutationBreakpoint(nodeFront, name));
    }
  },

  /**
   * If an editable item is focused, select its container.
   */

  _onFocus(event) {
    let parent = event.target;
    while (!parent.container) {
      parent = parent.parentNode;
    }
    if (parent) {
      this.navigate(parent.container);
    }
  },

  /**
   * Handle a user-requested navigation to a given MarkupContainer,
   * updating the inspector's currently-selected node.
   *
   * @param  {MarkupContainer} container
   *         The container we're navigating to.
   */

  navigate(container) {
    if (!container) {
      return;
    }

    this._markContainerAsSelected(container, "treepanel");
  },

  /**
   * Make sure a node is included in the markup tool.
   *
   * @param  {NodeFront} node
   *         The node in the content document.
   * @param  {Boolean} flashNode
   *         Whether the newly imported node should be flashed
   * @param  {Boolean} slotted
   *         Whether we are importing the slotted version of the node.
   * @return {MarkupContainer} The MarkupContainer object for this element.
   */

  importNode(node, flashNode, slotted) {
    if (!node) {
      return null;
    }

    if (this.hasContainer(node, slotted)) {
      return this.getContainer(node, slotted);
    }

    let container;
    const { nodeType, isPseudoElement } = node;
    if (node === node.walkerFront.rootNode) {
      container = new RootContainer(this, node);
      this._elt.appendChild(container.elt);
    }
    if (node === this.walker.rootNode) {
      this._rootNode = node;
    } else if (slotted) {
      container = new SlottedNodeContainer(this, node, this.inspector);
    } else if (nodeType == nodeConstants.ELEMENT_NODE && !isPseudoElement) {
      container = new MarkupElementContainer(this, node, this.inspector);
    } else if (
      nodeType == nodeConstants.COMMENT_NODE ||
      nodeType == nodeConstants.TEXT_NODE
    ) {
      container = new MarkupTextContainer(this, node, this.inspector);
    } else {
      container = new MarkupReadOnlyContainer(this, node, this.inspector);
    }

    if (flashNode) {
      container.flashMutation();
    }

    this.setContainer(node, container, slotted);
    this._forceUpdateChildren(container);

    this.inspector.emit("container-created", container);

    return container;
  },

  async _onResourceAvailable(resources) {
    for (const resource of resources) {
      if (
        !this.resourceCommand ||
        resource.resourceType !== this.resourceCommand.TYPES.ROOT_NODE ||
        resource.isDestroyed()
      ) {
        // Only handle alive root-node resources
        continue;
      }

      if (resource.targetFront.isTopLevel && resource.isTopLevelDocument) {
        // The topmost root node will lead to the destruction and recreation of
        // the MarkupView. This is handled by the inspector.
        continue;
      }

      const parentNodeFront = resource.parentNode();
      const container = this.getContainer(parentNodeFront);
      if (container) {
        // If there is no container for the parentNodeFront, the markup view is
        // currently not watching this part of the tree.
        this._forceUpdateChildren(container, {
          flash: true,
          updateLevel: true,
        });
      }
    }
  },

  _onTargetAvailable() {},

  _onTargetDestroyed({ targetFront, isModeSwitching }) {
    // Bug 1776250: We only watch targets in order to update containers which
    // might no longer be able to display children hosted in remote processes,
    // which corresponds to a Browser Toolbox mode switch.
    if (isModeSwitching) {
      const container = this.getContainer(targetFront.getParentNodeFront());
      if (container) {
        this._forceUpdateChildren(container, {
          updateLevel: true,
        });
      }
    }
  },

  /**
   * Mutation observer used for included nodes.
   */

  _onWalkerMutations(mutations) {
    for (const mutation of mutations) {
      const type = mutation.type;
      const target = mutation.target;

      const container = this.getContainer(target);
      if (!container) {
        // Container might not exist if this came from a load event for a node
        // we're not viewing.
        continue;
      }

      if (
        type === "attributes" ||
        type === "characterData" ||
        type === "customElementDefined" ||
        type === "events" ||
        type === "pseudoClassLock"
      ) {
        container.update();
      } else if (
        type === "childList" ||
        type === "slotchange" ||
        type === "shadowRootAttached"
      ) {
        this._forceUpdateChildren(container, {
          flash: true,
          updateLevel: true,
        });
      } else if (type === "inlineTextChild") {
        this._forceUpdateChildren(container, { flash: true });
        container.update();
      }
    }

    this._waitForChildren().then(() => {
      if (this._destroyed) {
        // Could not fully update after markup mutations, the markup-view was destroyed
        // while waiting for children. Bail out silently.
        return;
      }
      this._flashMutatedNodes(mutations);
      this.inspector.emit("markupmutation", mutations);

      // Since the htmlEditor is absolutely positioned, a mutation may change
      // the location in which it should be shown.
      if (this.htmlEditor) {
        this.htmlEditor.refresh();
      }
    });
  },

  /**
   * React to display-change and scrollable-change events from the walker. These are
   * events that tell us when something of interest changed on a collection of nodes:
   * whether their display type changed, or whether they became scrollable.
   *
   * @param  {Array} nodes
   *         An array of nodeFronts
   */

  _onWalkerNodeStatesChanged(nodes) {
    for (const node of nodes) {
      const container = this.getContainer(node);
      if (container) {
        container.update();
      }
    }
  },

  /**
   * Given a list of mutations returned by the mutation observer, flash the
   * corresponding containers to attract attention.
   */

  _flashMutatedNodes(mutations) {
    const addedOrEditedContainers = new Set();
    const removedContainers = new Set();

    for (const { type, target, added, removed, newValue } of mutations) {
      const container = this.getContainer(target);

      if (container) {
        if (type === "characterData") {
          addedOrEditedContainers.add(container);
        } else if (type === "attributes" && newValue === null) {
          // Removed attributes should flash the entire node.
          // New or changed attributes will flash the attribute itself
          // in ElementEditor.flashAttribute.
          addedOrEditedContainers.add(container);
        } else if (type === "childList") {
          // If there has been removals, flash the parent
          if (removed.length) {
            removedContainers.add(container);
          }

          // If there has been additions, flash the nodes if their associated
          // container exist (so if their parent is expanded in the inspector).
          added.forEach(node => {
            const addedContainer = this.getContainer(node);
            if (addedContainer) {
              addedOrEditedContainers.add(addedContainer);

              // The node may be added as a result of an append, in which case
              // it will have been removed from another container first, but in
              // these cases we don't want to flash both the removal and the
              // addition
              removedContainers.delete(container);
            }
          });
        }
      }
    }

    for (const container of removedContainers) {
      container.flashMutation();
    }
    for (const container of addedOrEditedContainers) {
      container.flashMutation();
    }
  },

  /**
   * Make sure the given node's parents are expanded and the
   * node is scrolled on to screen.
   */

  showNode(node, { centered = true, slotted, smoothScroll = false } = {}) {
    if (slotted && !this.hasContainer(node, slotted)) {
      throw new Error("Tried to show a slotted node not previously imported");
    } else {
      this._ensureNodeImported(node);
    }

    return this._waitForChildren()
      .then(() => {
        if (this._destroyed) {
          return Promise.reject("markupview destroyed");
        }
        return this._ensureVisible(node);
      })
      .then(() => {
        const container = this.getContainer(node, slotted);
        scrollIntoViewIfNeeded(container.editor.elt, centered, smoothScroll);
      }, this._handleRejectionIfNotDestroyed);
  },

  _ensureNodeImported(node) {
    let parent = node;

    this.importNode(node);

    while ((parent = this._getParentInTree(parent))) {
      this.importNode(parent);
      this.expandNode(parent);
    }
  },

  /**
   * Expand the container's children.
   */

  _expandContainer(container) {
    return this._updateChildren(container, { expand: true }).then(() => {
      if (this._destroyed) {
        // Could not expand the node, the markup-view was destroyed in the meantime. Just
        // silently give up.
        return;
      }
      container.setExpanded(true);
    });
  },

  /**
   * Expand the node's children.
   */

  expandNode(node) {
    const container = this.getContainer(node);
    return this._expandContainer(container);
  },

  /**
   * Expand the entire tree beneath a container.
   *
   * @param  {MarkupContainer} container
   *         The container to expand.
   */

  _expandAll(container) {
    return this._expandContainer(container)
      .then(() => {
        let child = container.children.firstChild;
        const promises = [];
        while (child) {
          promises.push(this._expandAll(child.container));
          child = child.nextSibling;
        }
        return Promise.all(promises);
      })
      .catch(console.error);
  },

  /**
   * Expand the entire tree beneath a node.
   *
   * @param  {DOMNode} node
   *         The node to expand, or null to start from the top.
   * @return {Promise} promise that resolves once all children are expanded.
   */

  expandAll(node) {
    node = node || this._rootNode;
    return this._expandAll(this.getContainer(node));
  },

  /**
   * Collapse the node's children.
   */

  collapseNode(node) {
    const container = this.getContainer(node);
    container.setExpanded(false);
  },

  _collapseAll(container) {
    container.setExpanded(false);
    const children = container.getChildContainers() || [];
    children.forEach(child => this._collapseAll(child));
  },

  /**
   * Collapse the entire tree beneath a node.
   *
   * @param  {DOMNode} node
   *         The node to collapse.
   * @return {Promise} promise that resolves once all children are collapsed.
   */

  collapseAll(node) {
    this._collapseAll(this.getContainer(node));

    // collapseAll is synchronous, return a promise for consistency with expandAll.
    return Promise.resolve();
  },

  /**
   * Returns either the innerHTML or the outerHTML for a remote node.
   *
   * @param  {NodeFront} node
   *         The NodeFront to get the outerHTML / innerHTML for.
   * @param  {Boolean} isOuter
   *         If true, makes the function return the outerHTML,
   *         otherwise the innerHTML.
   * @return {Promise} that will be resolved with the outerHTML / innerHTML.
   */

  _getNodeHTML(node, isOuter) {
    let walkerPromise = null;

    if (isOuter) {
      walkerPromise = node.walkerFront.outerHTML(node);
    } else {
      walkerPromise = node.walkerFront.innerHTML(node);
    }

    return getLongString(walkerPromise);
  },

  /**
   * Retrieve the outerHTML for a remote node.
   *
   * @param  {NodeFront} node
   *         The NodeFront to get the outerHTML for.
   * @return {Promise} that will be resolved with the outerHTML.
   */

  getNodeOuterHTML(node) {
    return this._getNodeHTML(node, true);
  },

  /**
   * Retrieve the innerHTML for a remote node.
   *
   * @param  {NodeFront} node
   *         The NodeFront to get the innerHTML for.
   * @return {Promise} that will be resolved with the innerHTML.
   */

  getNodeInnerHTML(node) {
    return this._getNodeHTML(node);
  },

  /**
   * Listen to mutations, expect a given node to be removed and try and select
   * the node that sits at the same place instead.
   * This is useful when changing the outerHTML or the tag name so that the
   * newly inserted node gets selected instead of the one that just got removed.
   */

  reselectOnRemoved(removedNode, reason) {
    // Only allow one removed node reselection at a time, so that when there are
    // more than 1 request in parallel, the last one wins.
    this.cancelReselectOnRemoved();

    // Get the removedNode index in its parent node to reselect the right node.
    const isRootElement = ["html""svg"].includes(
      removedNode.tagName.toLowerCase()
    );
    const oldContainer = this.getContainer(removedNode);
    const parentContainer = this.getContainer(removedNode.parentNode());
    const childIndex = parentContainer
      .getChildContainers()
      .indexOf(oldContainer);

    const onMutations = (this._removedNodeObserver = mutations => {
      let isNodeRemovalMutation = false;
      for (const mutation of mutations) {
        const containsRemovedNode =
          mutation.removed && mutation.removed.some(n => n === removedNode);
        if (
          mutation.type === "childList" &&
          (containsRemovedNode || isRootElement)
        ) {
          isNodeRemovalMutation = true;
          break;
        }
      }
      if (!isNodeRemovalMutation) {
        return;
      }

      this.inspector.off("markupmutation", onMutations);
      this._removedNodeObserver = null;

      // Don't select the new node if the user has already changed the current
      // selection.
      if (
        this.inspector.selection.nodeFront === parentContainer.node ||
        (this.inspector.selection.nodeFront === removedNode && isRootElement)
      ) {
        const childContainers = parentContainer.getChildContainers();
        if (childContainers?.[childIndex]) {
          const childContainer = childContainers[childIndex];
          this._markContainerAsSelected(childContainer, reason);
          if (childContainer.hasChildren) {
            this.expandNode(childContainer.node);
          }
          this.emit("reselectedonremoved");
        }
      }
    });

    // Start listening for mutations until we find a childList change that has
    // removedNode removed.
    this.inspector.on("markupmutation", onMutations);
  },

  /**
   * Make sure to stop listening for node removal markupmutations and not
   * reselect the corresponding node when that happens.
   * Useful when the outerHTML/tagname edition failed.
   */

  cancelReselectOnRemoved() {
    if (this._removedNodeObserver) {
      this.inspector.off("markupmutation"this._removedNodeObserver);
      this._removedNodeObserver = null;
      this.emit("canceledreselectonremoved");
    }
  },

  /**
   * Replace the outerHTML of any node displayed in the inspector with
   * some other HTML code
   *
   * @param  {NodeFront} node
   *         Node which outerHTML will be replaced.
   * @param  {String} newValue
   *         The new outerHTML to set on the node.
   * @param  {String} oldValue
   *         The old outerHTML that will be used if the user undoes the update.
   * @return {Promise} that will resolve when the outer HTML has been updated.
   */

  updateNodeOuterHTML(node, newValue) {
    const container = this.getContainer(node);
    if (!container) {
      return Promise.reject();
    }

    // Changing the outerHTML removes the node which outerHTML was changed.
    // Listen to this removal to reselect the right node afterwards.
    this.reselectOnRemoved(node, "outerhtml");
    return node.walkerFront.setOuterHTML(node, newValue).catch(() => {
      this.cancelReselectOnRemoved();
    });
  },

  /**
   * Replace the innerHTML of any node displayed in the inspector with
   * some other HTML code
   * @param  {Node} node
   *         node which innerHTML will be replaced.
   * @param  {String} newValue
   *         The new innerHTML to set on the node.
   * @param  {String} oldValue
   *         The old innerHTML that will be used if the user undoes the update.
   * @return {Promise} that will resolve when the inner HTML has been updated.
   */

  updateNodeInnerHTML(node, newValue, oldValue) {
    const container = this.getContainer(node);
    if (!container) {
      return Promise.reject();
    }

    return new Promise((resolve, reject) => {
      container.undo.do(
        () => {
          node.walkerFront.setInnerHTML(node, newValue).then(resolve, reject);
        },
        () => {
          node.walkerFront.setInnerHTML(node, oldValue);
        }
      );
    });
  },

  /**
   * Insert adjacent HTML to any node displayed in the inspector.
   *
   * @param  {NodeFront} node
   *         The reference node.
   * @param  {String} position
   *         The position as specified for Element.insertAdjacentHTML
   *         (i.e. "beforeBegin", "afterBegin", "beforeEnd", "afterEnd").
   * @param  {String} newValue
   *         The adjacent HTML.
   * @return {Promise} that will resolve when the adjacent HTML has
   *         been inserted.
   */

  insertAdjacentHTMLToNode(node, position, value) {
    const container = this.getContainer(node);
    if (!container) {
      return Promise.reject();
    }

    let injectedNodes = [];

    return new Promise((resolve, reject) => {
      container.undo.do(
        () => {
          // eslint-disable-next-line no-unsanitized/method
          node.walkerFront
            .insertAdjacentHTML(node, position, value)
            .then(nodeArray => {
              injectedNodes = nodeArray.nodes;
              return nodeArray;
            })
            .then(resolve, reject);
        },
        () => {
          node.walkerFront.removeNodes(injectedNodes);
        }
      );
    });
  },

  /**
   * Open an editor in the UI to allow editing of a node's html.
   *
   * @param  {NodeFront} node
   *         The NodeFront to edit.
   */

  beginEditingHTML(node) {
    // We use outer html for elements, but inner html for fragments.
    const isOuter = node.nodeType == nodeConstants.ELEMENT_NODE;
    const html = isOuter
      ? this.getNodeOuterHTML(node)
      : this.getNodeInnerHTML(node);
    html.then(oldValue => {
      const container = this.getContainer(node);
      if (!container) {
        return;
      }
      // Load load and create HTML Editor as it is rarely used and fetch complex deps
      if (!this.htmlEditor) {
        const HTMLEditor = require("resource://devtools/client/inspector/markup/views/html-editor.js");
        this.htmlEditor = new HTMLEditor(this.doc);
      }
      this.htmlEditor.show(container.tagLine, oldValue);
      const start = this.telemetry.msSystemNow();
      this.htmlEditor.once("popuphidden", (commit, value) => {
        // Need to focus the <html> element instead of the frame / window
        // in order to give keyboard focus back to doc (from editor).
        this.doc.documentElement.focus();

        if (commit) {
          if (isOuter) {
            this.updateNodeOuterHTML(node, value, oldValue);
          } else {
            this.updateNodeInnerHTML(node, value, oldValue);
          }
        }

        const end = this.telemetry.msSystemNow();
        this.telemetry.recordEvent("edit_html""inspector"null, {
          made_changes: commit,
          time_open: end - start,
        });
      });

      this.emit("begin-editing");
    });
  },

  /**
   * Expand or collapse the given node.
   *
   * @param  {NodeFront} node
   *         The NodeFront to update.
   * @param  {Boolean} expanded
   *         Whether the node should be expanded/collapsed.
   * @param  {Boolean} applyToDescendants
   *         Whether all descendants should also be expanded/collapsed
   */

  setNodeExpanded(node, expanded, applyToDescendants) {
    if (expanded) {
      if (applyToDescendants) {
        this.expandAll(node);
      } else {
        this.expandNode(node);
      }
    } else if (applyToDescendants) {
      this.collapseAll(node);
    } else {
      this.collapseNode(node);
    }
  },

  /**
   * Mark the given node selected, and update the inspector.selection
   * object's NodeFront to keep consistent state between UI and selection.
   *
   * @param  {NodeFront} node
   *         The NodeFront to mark as selected.
   * @return {Boolean} False if the node is already marked as selected, true
   *         otherwise.
   */

  markNodeAsSelected(node) {
    const container = this.getContainer(node);
--> --------------------

--> maximum size reached

--> --------------------

Messung V0.5
C=93 H=94 G=93

¤ Dauer der Verarbeitung: 0.57 Sekunden  (vorverarbeitet)  ¤

*© Formatika GbR, Deutschland






Wurzel

Suchen

Beweissystem der NASA

Beweissystem Isabelle

NIST Cobol Testsuite

Cephes Mathematical Library

Wiener Entwicklungsmethode

Haftungshinweis

Die Informationen auf dieser Webseite wurden nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit, noch Qualität der bereit gestellten Informationen zugesichert.

Bemerkung:

Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.