Anforderungen  |   Konzepte  |   Entwurf  |   Entwicklung  |   Qualitätssicherung  |   Lebenszyklus  |   Steuerung
 
 
 
 


Quelle  walker.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 { Actor } = require("resource://devtools/shared/protocol.js");
const { walkerSpec } = require("resource://devtools/shared/specs/walker.js");

const {
  LongStringActor,
} = require("resource://devtools/server/actors/string.js");
const {
  EXCLUDED_LISTENER,
} = require("resource://devtools/server/actors/inspector/constants.js");

loader.lazyRequireGetter(
  this,
  "nodeFilterConstants",
  "resource://devtools/shared/dom-node-filter-constants.js"
);

loader.lazyRequireGetter(
  this,
  [
    "getFrameElement",
    "isAfterPseudoElement",
    "isBeforePseudoElement",
    "isDirectShadowHostChild",
    "isMarkerPseudoElement",
    "isFrameBlockedByCSP",
    "isFrameWithChildTarget",
    "isShadowHost",
    "isShadowRoot",
    "loadSheet",
  ],
  "resource://devtools/shared/layout/utils.js",
  true
);

loader.lazyRequireGetter(
  this,
  "throttle",
  "resource://devtools/shared/throttle.js",
  true
);

loader.lazyRequireGetter(
  this,
  [
    "allAnonymousContentTreeWalkerFilter",
    "findGridParentContainerForNode",
    "isNodeDead",
    "noAnonymousContentTreeWalkerFilter",
    "nodeDocument",
    "standardTreeWalkerFilter",
  ],
  "resource://devtools/server/actors/inspector/utils.js",
  true
);

loader.lazyRequireGetter(
  this,
  "CustomElementWatcher",
  "resource://devtools/server/actors/inspector/custom-element-watcher.js",
  true
);
loader.lazyRequireGetter(
  this,
  ["DocumentWalker""SKIP_TO_SIBLING"],
  "resource://devtools/server/actors/inspector/document-walker.js",
  true
);
loader.lazyRequireGetter(
  this,
  ["NodeActor""NodeListActor"],
  "resource://devtools/server/actors/inspector/node.js",
  true
);
loader.lazyRequireGetter(
  this,
  "NodePicker",
  "resource://devtools/server/actors/inspector/node-picker.js",
  true
);
loader.lazyRequireGetter(
  this,
  "LayoutActor",
  "resource://devtools/server/actors/layout.js",
  true
);
loader.lazyRequireGetter(
  this,
  ["getLayoutChangesObserver""releaseLayoutChangesObserver"],
  "resource://devtools/server/actors/reflow.js",
  true
);
loader.lazyRequireGetter(
  this,
  "WalkerSearch",
  "resource://devtools/server/actors/utils/walker-search.js",
  true
);

// ContentDOMReference requires ChromeUtils, which isn't available in worker context.
const lazy = {};
if (!isWorker) {
  loader.lazyGetter(
    lazy,
    "ContentDOMReference",
    () =>
      ChromeUtils.importESModule(
        "resource://gre/modules/ContentDOMReference.sys.mjs",
        // ContentDOMReference needs to be retrieved from the shared global
        // since it is a shared singleton.
        { global: "shared" }
      ).ContentDOMReference
  );
}

loader.lazyServiceGetter(
  this,
  "eventListenerService",
  "@mozilla.org/eventlistenerservice;1",
  "nsIEventListenerService"
);

// Minimum delay between two "new-mutations" events.
const MUTATIONS_THROTTLING_DELAY = 100;
// List of mutation types that should -not- be throttled.
const IMMEDIATE_MUTATIONS = ["pseudoClassLock"];

const HIDDEN_CLASS = "__fx-devtools-hide-shortcut__";

// The possible completions to a ':' with added score to give certain values
// some preference.
const PSEUDO_SELECTORS = [
  [":active", 1],
  [":hover", 1],
  [":focus", 1],
  [":visited", 0],
  [":link", 0],
  [":first-letter", 0],
  [":first-child", 2],
  [":before", 2],
  [":after", 2],
  [":lang(", 0],
  [":not(", 3],
  [":first-of-type", 0],
  [":last-of-type", 0],
  [":only-of-type", 0],
  [":only-child", 2],
  [":nth-child(", 3],
  [":nth-last-child(", 0],
  [":nth-of-type(", 0],
  [":nth-last-of-type(", 0],
  [":last-child", 2],
  [":root", 0],
  [":empty", 0],
  [":target", 0],
  [":enabled", 0],
  [":disabled", 0],
  [":checked", 1],
  ["::selection", 0],
  ["::marker", 0],
];

const HELPER_SHEET =
  "data:text/css;charset=utf-8," +
  encodeURIComponent(`
  .__fx-devtools-hide-shortcut__ {
    visibility: hidden !important;
  }
`);

/**
 * We only send nodeValue up to a certain size by default.  This stuff
 * controls that size.
 */

exports.DEFAULT_VALUE_SUMMARY_LENGTH = 50;
var gValueSummaryLength = exports.DEFAULT_VALUE_SUMMARY_LENGTH;

exports.getValueSummaryLength = function () {
  return gValueSummaryLength;
};

exports.setValueSummaryLength = function (val) {
  gValueSummaryLength = val;
};

/**
 * Server side of the DOM walker.
 */

class WalkerActor extends Actor {
  /**
   * Create the WalkerActor
   * @param {DevToolsServerConnection} conn
   *        The server connection.
   * @param {TargetActor} targetActor
   *        The top-level Actor for this tab.
   * @param {Object} options
   *        - {Boolean} showAllAnonymousContent: Show all native anonymous content
   */

  constructor(conn, targetActor, options) {
    super(conn, walkerSpec);
    this.targetActor = targetActor;
    this.rootWin = targetActor.window;
    this.rootDoc = this.rootWin.document;

    // Map of already created node actors, keyed by their corresponding DOMNode.
    this._nodeActorsMap = new Map();

    this._pendingMutations = [];
    this._activePseudoClassLocks = new Set();
    this._mutationBreakpoints = new WeakMap();
    this._anonParents = new WeakMap();
    this.customElementWatcher = new CustomElementWatcher(
      targetActor.chromeEventHandler
    );

    // In this map, the key-value pairs are the overflow causing elements and their
    // respective ancestor scrollable node actor.
    this.overflowCausingElementsMap = new Map();

    this.showAllAnonymousContent = options.showAllAnonymousContent;

    this.walkerSearch = new WalkerSearch(this);

    // Nodes which have been removed from the client's known
    // ownership tree are considered "orphaned", and stored in
    // this set.
    this._orphaned = new Set();

    // The client can tell the walker that it is interested in a node
    // even when it is orphaned with the `retainNode` method.  This
    // list contains orphaned nodes that were so retained.
    this._retainedOrphans = new Set();

    this.onSubtreeModified = this.onSubtreeModified.bind(this);
    this.onSubtreeModified[EXCLUDED_LISTENER] = true;
    this.onNodeRemoved = this.onNodeRemoved.bind(this);
    this.onNodeRemoved[EXCLUDED_LISTENER] = true;
    this.onAttributeModified = this.onAttributeModified.bind(this);
    this.onAttributeModified[EXCLUDED_LISTENER] = true;

    this.onMutations = this.onMutations.bind(this);
    this.onSlotchange = this.onSlotchange.bind(this);
    this.onShadowrootattached = this.onShadowrootattached.bind(this);
    this.onAnonymousrootcreated = this.onAnonymousrootcreated.bind(this);
    this.onAnonymousrootremoved = this.onAnonymousrootremoved.bind(this);
    this.onFrameLoad = this.onFrameLoad.bind(this);
    this.onFrameUnload = this.onFrameUnload.bind(this);
    this.onCustomElementDefined = this.onCustomElementDefined.bind(this);
    this._throttledEmitNewMutations = throttle(
      this._emitNewMutations.bind(this),
      MUTATIONS_THROTTLING_DELAY
    );

    targetActor.on("will-navigate"this.onFrameUnload);
    targetActor.on("window-ready"this.onFrameLoad);

    this.customElementWatcher.on(
      "element-defined",
      this.onCustomElementDefined
    );

    // Keep a reference to the chromeEventHandler for the current targetActor, to make
    // sure we will be able to remove the listener during the WalkerActor destroy().
    this.chromeEventHandler = targetActor.chromeEventHandler;
    // shadowrootattached is a chrome-only event. We enable it below.
    this.chromeEventHandler.addEventListener(
      "shadowrootattached",
      this.onShadowrootattached
    );
    // anonymousrootcreated is a chrome-only event. We enable it below.
    this.chromeEventHandler.addEventListener(
      "anonymousrootcreated",
      this.onAnonymousrootcreated
    );
    this.chromeEventHandler.addEventListener(
      "anonymousrootremoved",
      this.onAnonymousrootremoved
    );
    for (const { document } of this.targetActor.windows) {
      document.devToolsAnonymousAndShadowEventsEnabled = true;
    }

    // Ensure that the root document node actor is ready and
    // managed.
    this.rootNode = this.document();

    this.layoutChangeObserver = getLayoutChangesObserver(this.targetActor);
    this._onReflows = this._onReflows.bind(this);
    this.layoutChangeObserver.on("reflows"this._onReflows);
    this._onResize = this._onResize.bind(this);
    this.layoutChangeObserver.on("resize"this._onResize);

    this._onEventListenerChange = this._onEventListenerChange.bind(this);
    eventListenerService.addListenerChangeListener(this._onEventListenerChange);
  }

  get nodePicker() {
    if (!this._nodePicker) {
      this._nodePicker = new NodePicker(thisthis.targetActor);
    }

    return this._nodePicker;
  }

  watchRootNode() {
    if (this.rootNode) {
      this.emit("root-available"this.rootNode);
    }
  }

  /**
   * Callback for eventListenerService.addListenerChangeListener
   * @param nsISimpleEnumerator changesEnum
   *    enumerator of nsIEventListenerChange
   */

  _onEventListenerChange(changesEnum) {
    for (const current of changesEnum.enumerate(Ci.nsIEventListenerChange)) {
      const target = current.target;

      if (this._nodeActorsMap.has(target)) {
        const actor = this.getNode(target);
        const mutation = {
          type: "events",
          target: actor.actorID,
          hasEventListeners: actor.hasEventListeners(/* refreshCache */ true),
        };
        this.queueMutation(mutation);
      }
    }
  }

  // Returns the JSON representation of this object over the wire.
  form() {
    return {
      actor: this.actorID,
      root: this.rootNode.form(),
      rfpCSSColorScheme: ChromeUtils.shouldResistFingerprinting(
        "CSSPrefersColorScheme",
        null
      ),
      traits: {},
    };
  }

  toString() {
    return "[WalkerActor " + this.actorID + "]";
  }

  getDocumentWalkerFilter() {
    // Allow native anonymous content (like <video> controls) if preffed on
    return this.showAllAnonymousContent
      ? allAnonymousContentTreeWalkerFilter
      : standardTreeWalkerFilter;
  }

  getDocumentWalker(node, skipTo) {
    return new DocumentWalker(node, this.rootWin, {
      filter: this.getDocumentWalkerFilter(),
      skipTo,
      showAnonymousContent: true,
    });
  }

  destroy() {
    if (this._destroyed) {
      return;
    }
    this._destroyed = true;
    super.destroy();
    try {
      this.clearPseudoClassLocks();
      this._activePseudoClassLocks = null;

      this.overflowCausingElementsMap.clear();
      this.overflowCausingElementsMap = null;

      this._hoveredNode = null;
      this.rootWin = null;
      this.rootDoc = null;
      this.rootNode = null;
      this.layoutHelpers = null;
      this._orphaned = null;
      this._retainedOrphans = null;

      this.targetActor.off("will-navigate"this.onFrameUnload);
      this.targetActor.off("window-ready"this.onFrameLoad);
      this.customElementWatcher.off(
        "element-defined",
        this.onCustomElementDefined
      );

      this.chromeEventHandler.removeEventListener(
        "shadowrootattached",
        this.onShadowrootattached
      );
      this.chromeEventHandler.removeEventListener(
        "anonymousrootcreated",
        this.onAnonymousrootcreated
      );
      this.chromeEventHandler.removeEventListener(
        "anonymousrootremoved",
        this.onAnonymousrootremoved
      );

      // This attribute is just for devtools, so we can unset once we're done.
      for (const { document } of this.targetActor.windows) {
        document.devToolsAnonymousAndShadowEventsEnabled = false;
      }

      this.onFrameLoad = null;
      this.onFrameUnload = null;

      this.customElementWatcher.destroy();
      this.customElementWatcher = null;

      this.walkerSearch.destroy();

      if (this._nodePicker) {
        this._nodePicker.destroy();
        this._nodePicker = null;
      }

      this.layoutChangeObserver.off("reflows"this._onReflows);
      this.layoutChangeObserver.off("resize"this._onResize);
      this.layoutChangeObserver = null;
      releaseLayoutChangesObserver(this.targetActor);

      eventListenerService.removeListenerChangeListener(
        this._onEventListenerChange
      );

      // Only nullify some key attributes after having removed all the listeners
      // as they may still be used in the related listeners.
      this._nodeActorsMap = null;
      this.onMutations = null;

      this.layoutActor = null;
      this.targetActor = null;
      this.chromeEventHandler = null;

      this.emit("destroyed");
    } catch (e) {
      console.error(e);
    }
  }

  release() {}

  unmanage(actor) {
    if (actor instanceof NodeActor) {
      if (
        this._activePseudoClassLocks &&
        this._activePseudoClassLocks.has(actor)
      ) {
        this.clearPseudoClassLocks(actor);
      }

      this.customElementWatcher.unmanageNode(actor);

      this._nodeActorsMap.delete(actor.rawNode);
    }
    super.unmanage(actor);
  }

  /**
   * Determine if the walker has come across this DOM node before.
   * @param {DOMNode} rawNode
   * @return {Boolean}
   */

  hasNode(rawNode) {
    return this._nodeActorsMap.has(rawNode);
  }

  /**
   * If the walker has come across this DOM node before, then get the
   * corresponding node actor.
   * @param {DOMNode} rawNode
   * @return {NodeActor}
   */

  getNode(rawNode) {
    return this._nodeActorsMap.get(rawNode);
  }

  /**
   * Internal helper that will either retrieve the existing NodeActor for the
   * provided node or create the actor on the fly if it doesn't exist.
   * This method should only be called when we are sure that the node should be
   * known by the client and that the parent node is already known.
   *
   * Otherwise prefer `getNode` to only retrieve known actors or `attachElement`
   * to create node actors recursively.
   *
   * @param  {DOMNode} node
   *         The node for which we want to create or get an actor
   * @return {NodeActor} The corresponding NodeActor
   */

  _getOrCreateNodeActor(node) {
    let actor = this.getNode(node);
    if (actor) {
      return actor;
    }

    actor = new NodeActor(this, node);

    // Add the node actor as a child of this walker actor, assigning
    // it an actorID.
    this.manage(actor);
    this._nodeActorsMap.set(node, actor);

    if (node.nodeType === Node.DOCUMENT_NODE) {
      actor.watchDocument(node, this.onMutations);
    }

    if (isShadowRoot(actor.rawNode)) {
      actor.watchDocument(node.ownerDocument, this.onMutations);
      actor.watchSlotchange(this.onSlotchange);
    }

    this.customElementWatcher.manageNode(actor);

    return actor;
  }

  /**
   * When a custom element is defined, send a customElementDefined mutation for all the
   * NodeActors using this tag name.
   */

  onCustomElementDefined({ actors }) {
    actors.forEach(actor =>
      this.queueMutation({
        target: actor.actorID,
        type: "customElementDefined",
        customElementLocation: actor.getCustomElementLocation(),
      })
    );
  }

  _onReflows() {
    // Going through the nodes the walker knows about, see which ones have had their
    // containerType, display, scrollable or overflow state changed and send events if any.
    const containerTypeChanges = [];
    const displayTypeChanges = [];
    const scrollableStateChanges = [];

    const currentOverflowCausingElementsMap = new Map();

    for (const [node, actor] of this._nodeActorsMap) {
      if (Cu.isDeadWrapper(node)) {
        continue;
      }

      const displayType = actor.displayType;
      const isDisplayed = actor.isDisplayed;

      if (
        displayType !== actor.currentDisplayType ||
        isDisplayed !== actor.wasDisplayed
      ) {
        displayTypeChanges.push(actor);

        // Updating the original value
        actor.currentDisplayType = displayType;
        actor.wasDisplayed = isDisplayed;
      }

      const isScrollable = actor.isScrollable;
      if (isScrollable !== actor.wasScrollable) {
        scrollableStateChanges.push(actor);
        actor.wasScrollable = isScrollable;
      }

      if (isScrollable) {
        this.updateOverflowCausingElements(
          actor,
          currentOverflowCausingElementsMap
        );
      }

      const containerType = actor.containerType;
      if (containerType !== actor.currentContainerType) {
        containerTypeChanges.push(actor);
        actor.currentContainerType = containerType;
      }
    }

    // Get the NodeActor for each node in the symmetric difference of
    // currentOverflowCausingElementsMap and this.overflowCausingElementsMap
    const overflowStateChanges = [...currentOverflowCausingElementsMap.keys()]
      .filter(node => !this.overflowCausingElementsMap.has(node))
      .concat(
        [...this.overflowCausingElementsMap.keys()].filter(
          node => !currentOverflowCausingElementsMap.has(node)
        )
      )
      .filter(node => this.hasNode(node))
      .map(node => this.getNode(node));

    this.overflowCausingElementsMap = currentOverflowCausingElementsMap;

    if (overflowStateChanges.length) {
      this.emit("overflow-change", overflowStateChanges);
    }

    if (displayTypeChanges.length) {
      this.emit("display-change", displayTypeChanges);
    }

    if (scrollableStateChanges.length) {
      this.emit("scrollable-change", scrollableStateChanges);
    }

    if (containerTypeChanges.length) {
      this.emit("container-type-change", containerTypeChanges);
    }
  }

  /**
   * When the browser window gets resized, relay the event to the front.
   */

  _onResize() {
    this.emit("resize");
  }

  /**
   * Ensures that the node is attached and it can be accessed from the root.
   *
   * @param {(Node|NodeActor)} nodes The nodes
   * @return {Object} An object compatible with the disconnectedNode type.
   */

  attachElement(node) {
    const { nodes, newParents } = this.attachElements([node]);
    return {
      node: nodes[0],
      newParents,
    };
  }

  /**
   * Ensures that the nodes are attached and they can be accessed from the root.
   *
   * @param {(Node[]|NodeActor[])} nodes The nodes
   * @return {Object} An object compatible with the disconnectedNodeArray type.
   */

  attachElements(nodes) {
    const nodeActors = [];
    const newParents = new Set();
    for (let node of nodes) {
      if (!(node instanceof NodeActor)) {
        // If an anonymous node was passed in and we aren't supposed to know
        // about it, then use the closest ancestor.
        if (!this.showAllAnonymousContent) {
          while (
            node &&
            standardTreeWalkerFilter(node) != nodeFilterConstants.FILTER_ACCEPT
          ) {
            node = this.rawParentNode(node);
          }
          if (!node) {
            continue;
          }
        }

        node = this._getOrCreateNodeActor(node);
      }

      this.ensurePathToRoot(node, newParents);
      // If nodes may be an array of raw nodes, we're sure to only have
      // NodeActors with the following array.
      nodeActors.push(node);
    }

    return {
      nodes: nodeActors,
      newParents: [...newParents],
    };
  }

  /**
   * Return the document node that contains the given node,
   * or the root node if no node is specified.
   * @param NodeActor node
   *        The node whose document is needed, or null to
   *        return the root.
   */

  document(node) {
    const doc = isNodeDead(node) ? this.rootDoc : nodeDocument(node.rawNode);
    return this._getOrCreateNodeActor(doc);
  }

  /**
   * Return the documentElement for the document containing the
   * given node.
   * @param NodeActor node
   *        The node whose documentElement is requested, or null
   *        to use the root document.
   */

  documentElement(node) {
    const elt = isNodeDead(node)
      ? this.rootDoc.documentElement
      : nodeDocument(node.rawNode).documentElement;
    return this._getOrCreateNodeActor(elt);
  }

  parentNode(node) {
    const parent = this.rawParentNode(node);
    if (parent) {
      return this._getOrCreateNodeActor(parent);
    }

    return null;
  }

  rawParentNode(node) {
    const rawNode = node instanceof NodeActor ? node.rawNode : node;
    if (rawNode == this.rootDoc) {
      return null;
    }
    return InspectorUtils.getParentForNode(rawNode, /* anonymous = */ true);
  }

  /**
   * If the given NodeActor only has a single text node as a child with a text
   * content small enough to be inlined, return that child's NodeActor.
   *
   * @param NodeActor node
   */

  inlineTextChild({ rawNode }) {
    // Quick checks to prevent creating a new walker if possible.
    if (
      isMarkerPseudoElement(rawNode) ||
      isBeforePseudoElement(rawNode) ||
      isAfterPseudoElement(rawNode) ||
      isShadowHost(rawNode) ||
      rawNode.nodeType != Node.ELEMENT_NODE ||
      !!rawNode.children.length ||
      isFrameWithChildTarget(this.targetActor, rawNode) ||
      isFrameBlockedByCSP(rawNode)
    ) {
      return undefined;
    }

    const children = this._rawChildren(rawNode, /* includeAssigned = */ true);
    const firstChild = children[0];

    // Bail out if:
    // - more than one child
    // - unique child is not a text node
    // - unique child is a text node, but is too long to be inlined
    // - we are a slot -> these are always represented on their own lines with
    //                    a link to the original node.
    // - we are a flex item -> these are always shown on their own lines so they can be
    //                         selected by the flexbox inspector.
    const isAssignedToSlot =
      firstChild &&
      rawNode.nodeName === "SLOT" &&
      isDirectShadowHostChild(firstChild);

    const isFlexItem = !!firstChild?.parentFlexElement;

    if (
      !firstChild ||
      children.length > 1 ||
      firstChild.nodeType !== Node.TEXT_NODE ||
      firstChild.nodeValue.length > gValueSummaryLength ||
      isAssignedToSlot ||
      isFlexItem
    ) {
      return undefined;
    }

    return this._getOrCreateNodeActor(firstChild);
  }

  /**
   * Mark a node as 'retained'.
   *
   * A retained node is not released when `releaseNode` is called on its
   * parent, or when a parent is released with the `cleanup` option to
   * `getMutations`.
   *
   * When a retained node's parent is released, a retained mode is added to
   * the walker's "retained orphans" list.
   *
   * Retained nodes can be deleted by providing the `force` option to
   * `releaseNode`.  They will also be released when their document
   * has been destroyed.
   *
   * Retaining a node makes no promise about its children;  They can
   * still be removed by normal means.
   */

  retainNode(node) {
    node.retained = true;
  }

  /**
   * Remove the 'retained' mark from a node.  If the node was a
   * retained orphan, release it.
   */

  unretainNode(node) {
    node.retained = false;
    if (this._retainedOrphans.has(node)) {
      this._retainedOrphans.delete(node);
      this.releaseNode(node);
    }
  }

  /**
   * Release actors for a node and all child nodes.
   */

  releaseNode(node, options = {}) {
    if (isNodeDead(node)) {
      return;
    }

    if (node.retained && !options.force) {
      this._retainedOrphans.add(node);
      return;
    }

    if (node.retained) {
      // Forcing a retained node to go away.
      this._retainedOrphans.delete(node);
    }

    for (const child of this._rawChildren(node.rawNode)) {
      const childActor = this.getNode(child);
      if (childActor) {
        this.releaseNode(childActor, options);
      }
    }

    node.destroy();
  }

  /**
   * Add any nodes between `node` and the walker's root node that have not
   * yet been seen by the client.
   */

  ensurePathToRoot(node, newParents = new Set()) {
    if (!node) {
      return newParents;
    }
    let parent = this.rawParentNode(node);
    while (parent) {
      let parentActor = this.getNode(parent);
      if (parentActor) {
        // This parent did exist, so the client knows about it.
        return newParents;
      }
      // This parent didn't exist, so hasn't been seen by the client yet.
      parentActor = this._getOrCreateNodeActor(parent);
      newParents.add(parentActor);
      parent = this.rawParentNode(parentActor);
    }
    return newParents;
  }

  /**
   * Return the number of children under the provided NodeActor.
   *
   * @param NodeActor node
   *    See JSDoc for children()
   * @param object options
   *    See JSDoc for children()
   * @return Number the number of children
   */

  countChildren(node, options = {}) {
    return this._getChildren(node, options).nodes.length;
  }

  /**
   * Return children of the given node.  By default this method will return
   * all children of the node, but there are options that can restrict this
   * to a more manageable subset.
   *
   * @param NodeActor node
   *    The node whose children you're curious about.
   * @param object options
   *    Named options:
   *    `maxNodes`: The set of nodes returned by the method will be no longer
   *       than maxNodes.
   *    `start`: If a node is specified, the list of nodes will start
   *       with the given child.  Mutally exclusive with `center`.
   *    `center`: If a node is specified, the given node will be as centered
   *       as possible in the list, given how close to the ends of the child
   *       list it is.  Mutually exclusive with `start`.
   *
   * @returns an object with three items:
   *    hasFirst: true if the first child of the node is included in the list.
   *    hasLast: true if the last child of the node is included in the list.
   *    nodes: Array of NodeActor representing the nodes returned by the request.
   */

  children(node, options = {}) {
    const { hasFirst, hasLast, nodes } = this._getChildren(node, options);
    return {
      hasFirst,
      hasLast,
      nodes: nodes.map(n => this._getOrCreateNodeActor(n)),
    };
  }

  /**
   * Returns the raw children of the DOM node, with anonymous content filtered as needed
   * @param Node rawNode.
   * @param boolean includeAssigned
   *   Whether <slot> assigned children should be returned. See
   *   HTMLSlotElement.assignedNodes().
   * @returns Array<Node> the list of children.
   */

  _rawChildren(rawNode, includeAssigned) {
    const filter = this.showAllAnonymousContent
      ? allAnonymousContentTreeWalkerFilter
      : standardTreeWalkerFilter;
    const ret = [];
    const children = InspectorUtils.getChildrenForNode(
      rawNode,
      /* anonymous = */ true,
      includeAssigned
    );
    for (const child of children) {
      if (filter(child) == nodeFilterConstants.FILTER_ACCEPT) {
        ret.push(child);
      }
    }
    return ret;
  }

  /**
   * Return chidlren of the given node. Contrary to children children(), this method only
   * returns DOMNodes. Therefore it will not create NodeActor wrappers and will not
   * update the nodeActors map for the discovered nodes either. This makes this method
   * safe to call when you are not sure if the discovered nodes will be communicated to
   * the client.
   *
   * @param NodeActor node
   *    See JSDoc for children()
   * @param object options
   *    See JSDoc for children()
   * @return  an object with three items:
   *    hasFirst: true if the first child of the node is included in the list.
   *    hasLast: true if the last child of the node is included in the list.
   *    nodes: Array of DOMNodes.
   */

  // eslint-disable-next-line complexity
  _getChildren(node, options = {}) {
    if (isNodeDead(node) || isFrameBlockedByCSP(node.rawNode)) {
      return { hasFirst: true, hasLast: true, nodes: [] };
    }

    if (options.center && options.start) {
      throw Error("Can't specify both 'center' and 'start' options.");
    }

    let maxNodes = options.maxNodes || -1;
    if (maxNodes == -1) {
      maxNodes = Number.MAX_VALUE;
    }

    let nodes = this._rawChildren(node.rawNode, /* includeAssigned = */ true);
    let hasFirst = true;
    let hasLast = true;
    if (nodes.length > maxNodes) {
      let startIndex;
      if (options.center) {
        const centerIndex = nodes.indexOf(options.center.rawNode);
        const backwardCount = Math.floor(maxNodes / 2);
        // If centering would hit the end, just read the last maxNodes nodes.
        if (centerIndex - backwardCount + maxNodes >= nodes.length) {
          startIndex = nodes.length - maxNodes;
        } else {
          startIndex = Math.max(0, centerIndex - backwardCount);
        }
      } else if (options.start) {
        startIndex = Math.max(0, nodes.indexOf(options.start.rawNode));
      } else {
        startIndex = 0;
      }
      const endIndex = Math.min(startIndex + maxNodes, nodes.length);
      hasFirst = startIndex == 0;
      hasLast = endIndex >= nodes.length;
      nodes = nodes.slice(startIndex, endIndex);
    }

    return { hasFirst, hasLast, nodes };
  }

  /**
   * Get the next sibling of a given node.  Getting nodes one at a time
   * might be inefficient, be careful.
   */

  nextSibling(node) {
    if (isNodeDead(node)) {
      return null;
    }

    const walker = this.getDocumentWalker(node.rawNode);
    const sibling = walker.nextSibling();
    return sibling ? this._getOrCreateNodeActor(sibling) : null;
  }

  /**
   * Get the previous sibling of a given node.  Getting nodes one at a time
   * might be inefficient, be careful.
   */

  previousSibling(node) {
    if (isNodeDead(node)) {
      return null;
    }

    const walker = this.getDocumentWalker(node.rawNode);
    const sibling = walker.previousSibling();
    return sibling ? this._getOrCreateNodeActor(sibling) : null;
  }

  /**
   * Helper function for the `children` method: Read forward in the sibling
   * list into an array with `count` items, including the current node.
   */

  _readForward(walker, count) {
    const ret = [];

    let node = walker.currentNode;
    do {
      if (!walker.isSkippedNode(node)) {
        // The walker can be on a node that would be filtered out if it didn't find any
        // other node to fallback to.
        ret.push(node);
      }
      node = walker.nextSibling();
    } while (node && --count);
    return ret;
  }

  /**
   * Return the first node in the document that matches the given selector.
   * See https://developer.mozilla.org/en-US/docs/Web/API/Element.querySelector
   *
   * @param NodeActor baseNode
   * @param string selector
   */

  querySelector(baseNode, selector) {
    if (isNodeDead(baseNode)) {
      return {};
    }

    const node = baseNode.rawNode.querySelector(selector);
    if (!node) {
      return {};
    }

    return this.attachElement(node);
  }

  /**
   * Return a NodeListActor with all nodes that match the given selector.
   * See https://developer.mozilla.org/en-US/docs/Web/API/Element.querySelectorAll
   *
   * @param NodeActor baseNode
   * @param string selector
   */

  querySelectorAll(baseNode, selector) {
    let nodeList = null;

    try {
      nodeList = baseNode.rawNode.querySelectorAll(selector);
    } catch (e) {
      // Bad selector. Do nothing as the selector can come from a searchbox.
    }

    return new NodeListActor(this, nodeList);
  }

  /**
   * Return the node in the baseNode rootNode matching the passed id referenced in a
   * idref/idreflist attribute, as those are scoped within a shadow root.
   *
   * @param NodeActor baseNode
   * @param string id
   */

  getIdrefNode(baseNode, id) {
    if (isNodeDead(baseNode)) {
      return {};
    }

    // Get the document or the shadow root for baseNode
    const rootNode = baseNode.rawNode.getRootNode({ composed: false });
    if (!rootNode) {
      return {};
    }

    const node = rootNode.getElementById(id);
    if (!node) {
      return {};
    }

    return this.attachElement(node);
  }

  /**
   * Get a list of nodes that match the given selector in all known frames of
   * the current content page.
   * @param {String} selector.
   * @return {Array}
   */

  _multiFrameQuerySelectorAll(selector) {
    let nodes = [];

    for (const { document } of this.targetActor.windows) {
      try {
        nodes = [...nodes, ...document.querySelectorAll(selector)];
      } catch (e) {
        // Bad selector. Do nothing as the selector can come from a searchbox.
      }
    }

    return nodes;
  }

  /**
   * Get a list of nodes that match the given XPath in all known frames of
   * the current content page.
   * @param {String} xPath.
   * @return {Array}
   */

  _multiFrameXPath(xPath) {
    const nodes = [];

    for (const window of this.targetActor.windows) {
      const document = window.document;
      try {
        const result = document.evaluate(
          xPath,
          document.documentElement,
          null,
          window.XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
          null
        );

        for (let i = 0; i < result.snapshotLength; i++) {
          nodes.push(result.snapshotItem(i));
        }
      } catch (e) {
        // Bad XPath. Do nothing as the XPath can come from a searchbox.
      }
    }

    return nodes;
  }

  /**
   * Return a NodeListActor with all nodes that match the given XPath in all
   * frames of the current content page.
   * @param {String} xPath
   */

  multiFrameXPath(xPath) {
    return new NodeListActor(thisthis._multiFrameXPath(xPath));
  }

  /**
   * Search the document for a given string.
   * Results will be searched with the walker-search module (searches through
   * tag names, attribute names and values, and text contents).
   *
   * @returns {searchresult}
   *            - {NodeList} list
   *            - {Array<Object>} metadata. Extra information with indices that
   *                              match up with node list.
   */

  search(query) {
    const results = this.walkerSearch.search(query);
    const nodeList = new NodeListActor(
      this,
      results.map(r => r.node)
    );

    return {
      list: nodeList,
      metadata: [],
    };
  }

  /**
   * Returns a list of matching results for CSS selector autocompletion.
   *
   * @param string query
   *        The selector query being completed
   * @param string completing
   *        The exact token being completed out of the query
   * @param string selectorState
   *        One of "pseudo", "id", "tag", "class", "null"
   */

  // eslint-disable-next-line complexity
  getSuggestionsForQuery(query, completing, selectorState) {
    const sugs = {
      classes: new Map(),
      tags: new Map(),
      ids: new Map(),
    };
    let result = [];
    let nodes = null;
    // Filtering and sorting the results so that protocol transfer is miminal.
    switch (selectorState) {
      case "pseudo":
        result = PSEUDO_SELECTORS.filter(item => {
          return item[0].startsWith(":" + completing);
        });
        break;

      case "class":
        if (!query) {
          nodes = this._multiFrameQuerySelectorAll("[class]");
        } else {
          nodes = this._multiFrameQuerySelectorAll(query);
        }
        for (const node of nodes) {
          for (const className of node.classList) {
            sugs.classes.set(className, (sugs.classes.get(className) | 0) + 1);
          }
        }
        sugs.classes.delete("");
        sugs.classes.delete(HIDDEN_CLASS);
        for (const [className, count] of sugs.classes) {
          if (className.startsWith(completing)) {
            result.push(["." + CSS.escape(className), count, selectorState]);
          }
        }
        break;

      case "id":
        if (!query) {
          nodes = this._multiFrameQuerySelectorAll("[id]");
        } else {
          nodes = this._multiFrameQuerySelectorAll(query);
        }
        for (const node of nodes) {
          sugs.ids.set(node.id, (sugs.ids.get(node.id) | 0) + 1);
        }
        for (const [id, count] of sugs.ids) {
          if (id.startsWith(completing) && id !== "") {
            result.push(["#" + CSS.escape(id), count, selectorState]);
          }
        }
        break;

      case "tag":
        if (!query) {
          nodes = this._multiFrameQuerySelectorAll("*");
        } else {
          nodes = this._multiFrameQuerySelectorAll(query);
        }
        for (const node of nodes) {
          const tag = node.localName;
          sugs.tags.set(tag, (sugs.tags.get(tag) | 0) + 1);
        }
        for (const [tag, count] of sugs.tags) {
          if (new RegExp("^" + completing + ".*""i").test(tag)) {
            result.push([tag, count, selectorState]);
          }
        }

        // For state 'tag' (no preceding # or .) and when there's no query (i.e.
        // only one word) then search for the matching classes and ids
        if (!query) {
          result = [
            ...result,
            ...this.getSuggestionsForQuery(null, completing, "class")
              .suggestions,
            ...this.getSuggestionsForQuery(null, completing, "id").suggestions,
          ];
        }

        break;

      case "null":
        nodes = this._multiFrameQuerySelectorAll(query);
        for (const node of nodes) {
          sugs.ids.set(node.id, (sugs.ids.get(node.id) | 0) + 1);
          const tag = node.localName;
          sugs.tags.set(tag, (sugs.tags.get(tag) | 0) + 1);
          for (const className of node.classList) {
            sugs.classes.set(className, (sugs.classes.get(className) | 0) + 1);
          }
        }
        for (const [tag, count] of sugs.tags) {
          tag && result.push([tag, count]);
        }
        for (const [id, count] of sugs.ids) {
          id && result.push(["#" + id, count]);
        }
        sugs.classes.delete("");
        sugs.classes.delete(HIDDEN_CLASS);
        for (const [className, count] of sugs.classes) {
          className && result.push(["." + className, count]);
        }
    }

    // Sort by count (desc) and name (asc)
    result = result.sort((a, b) => {
      // Computed a sortable string with first the inverted count, then the name
      let sortA = 10000 - a[1] + a[0];
      let sortB = 10000 - b[1] + b[0];

      // Prefixing ids, classes and tags, to group results
      const firstA = a[0].substring(0, 1);
      const firstB = b[0].substring(0, 1);

      const getSortKeyPrefix = firstLetter => {
        if (firstLetter === "#") {
          return "2";
        }
        if (firstLetter === ".") {
          return "1";
        }
        return "0";
      };

      sortA = getSortKeyPrefix(firstA) + sortA;
      sortB = getSortKeyPrefix(firstB) + sortB;

      // String compare
      return sortA.localeCompare(sortB);
    });

    result = result.slice(0, 25);

    return {
      query,
      suggestions: result,
    };
  }

  /**
   * Add a pseudo-class lock to a node.
   *
   * @param NodeActor node
   * @param string pseudo
   *    A pseudoclass: ':hover', ':active', ':focus', ':focus-within'
   * @param options
   *    Options object:
   *    `parents`: True if the pseudo-class should be added
   *      to parent nodes.
   *    `enabled`: False if the pseudo-class should be locked
   *      to 'off'. Defaults to true.
   *
   * @returns An empty packet.  A "pseudoClassLock" mutation will
   *    be queued for any changed nodes.
   */

  addPseudoClassLock(node, pseudo, options = {}) {
    if (isNodeDead(node)) {
      return;
    }

    // There can be only one node locked per pseudo, so dismiss all existing
    // ones
    for (const locked of this._activePseudoClassLocks) {
      if (InspectorUtils.hasPseudoClassLock(locked.rawNode, pseudo)) {
        this._removePseudoClassLock(locked, pseudo);
      }
    }

    const enabled = options.enabled === undefined || options.enabled;
    this._addPseudoClassLock(node, pseudo, enabled);

    if (!options.parents) {
      return;
    }

    const walker = this.getDocumentWalker(node.rawNode);
    let cur;
    while ((cur = walker.parentNode())) {
      const curNode = this._getOrCreateNodeActor(cur);
      this._addPseudoClassLock(curNode, pseudo, enabled);
    }
  }

  _queuePseudoClassMutation(node) {
    this.queueMutation({
      target: node.actorID,
      type: "pseudoClassLock",
      pseudoClassLocks: node.writePseudoClassLocks(),
    });
  }

  _addPseudoClassLock(node, pseudo, enabled) {
    if (node.rawNode.nodeType !== Node.ELEMENT_NODE) {
      return false;
    }
    InspectorUtils.addPseudoClassLock(node.rawNode, pseudo, enabled);
    this._activePseudoClassLocks.add(node);
    this._queuePseudoClassMutation(node);
    return true;
  }

  hideNode(node) {
    if (isNodeDead(node)) {
      return;
    }

    loadSheet(node.rawNode.ownerGlobal, HELPER_SHEET);
    node.rawNode.classList.add(HIDDEN_CLASS);
  }

  unhideNode(node) {
    if (isNodeDead(node)) {
      return;
    }

    node.rawNode.classList.remove(HIDDEN_CLASS);
  }

  /**
   * Remove a pseudo-class lock from a node.
   *
   * @param NodeActor node
   * @param string pseudo
   *    A pseudoclass: ':hover', ':active', ':focus', ':focus-within'
   * @param options
   *    Options object:
   *    `parents`: True if the pseudo-class should be removed
   *      from parent nodes.
   *
   * @returns An empty response.  "pseudoClassLock" mutations
   *    will be emitted for any changed nodes.
   */

  removePseudoClassLock(node, pseudo, options = {}) {
    if (isNodeDead(node)) {
      return;
    }

    this._removePseudoClassLock(node, pseudo);

    // Remove pseudo class for children as we don't want to allow
    // turning it on for some childs without setting it on some parents
    for (const locked of this._activePseudoClassLocks) {
      if (
        node.rawNode.contains(locked.rawNode) &&
        InspectorUtils.hasPseudoClassLock(locked.rawNode, pseudo)
      ) {
        this._removePseudoClassLock(locked, pseudo);
      }
    }

    if (!options.parents) {
      return;
    }

    const walker = this.getDocumentWalker(node.rawNode);
    let cur;
    while ((cur = walker.parentNode())) {
      const curNode = this._getOrCreateNodeActor(cur);
      this._removePseudoClassLock(curNode, pseudo);
    }
  }

  _removePseudoClassLock(node, pseudo) {
    if (node.rawNode.nodeType != Node.ELEMENT_NODE) {
      return false;
    }
    InspectorUtils.removePseudoClassLock(node.rawNode, pseudo);
    if (!node.writePseudoClassLocks()) {
      this._activePseudoClassLocks.delete(node);
    }

    this._queuePseudoClassMutation(node);
    return true;
  }

  /**
   * Clear all the pseudo-classes on a given node or all nodes.
   * @param {NodeActor} node Optional node to clear pseudo-classes on
   */

  clearPseudoClassLocks(node) {
    if (node && isNodeDead(node)) {
      return;
    }

    if (node) {
      InspectorUtils.clearPseudoClassLocks(node.rawNode);
      this._activePseudoClassLocks.delete(node);
      this._queuePseudoClassMutation(node);
    } else {
      for (const locked of this._activePseudoClassLocks) {
        InspectorUtils.clearPseudoClassLocks(locked.rawNode);
        this._activePseudoClassLocks.delete(locked);
        this._queuePseudoClassMutation(locked);
      }
    }
  }

  /**
   * Get a node's innerHTML property.
   */

  innerHTML(node) {
    let html = "";
    if (!isNodeDead(node)) {
      html = node.rawNode.innerHTML;
    }
    return new LongStringActor(this.conn, html);
  }

  /**
   * Set a node's innerHTML property.
   *
   * @param {NodeActor} node The node.
   * @param {string} value The piece of HTML content.
   */

  setInnerHTML(node, value) {
    if (isNodeDead(node)) {
      return;
    }

    const rawNode = node.rawNode;
    if (
      rawNode.nodeType !== rawNode.ownerDocument.ELEMENT_NODE &&
      rawNode.nodeType !== rawNode.ownerDocument.DOCUMENT_FRAGMENT_NODE
    ) {
      throw new Error("Can only change innerHTML to element or fragment nodes");
    }
    // eslint-disable-next-line no-unsanitized/property
    rawNode.innerHTML = value;
  }

  /**
   * Get a node's outerHTML property.
   *
   * @param {NodeActor} node The node.
   */

  outerHTML(node) {
    let outerHTML = "";
    if (!isNodeDead(node)) {
      outerHTML = node.rawNode.outerHTML;
    }
    return new LongStringActor(this.conn, outerHTML);
  }

  /**
   * Set a node's outerHTML property.
   *
   * @param {NodeActor} node The node.
   * @param {string} value The piece of HTML content.
   */

  setOuterHTML(node, value) {
    if (isNodeDead(node)) {
      return;
    }

    const rawNode = node.rawNode;
    const doc = nodeDocument(rawNode);
    const win = doc.defaultView;
    let parser;
    if (!win) {
      throw new Error("The window object shouldn't be null");
    } else {
      // We create DOMParser under window object because we want a content
      // DOMParser, which means all the DOM objects created by this DOMParser
      // will be in the same DocGroup as rawNode.parentNode. Then the newly
      // created nodes can be adopted into rawNode.parentNode.
      parser = new win.DOMParser();
    }

    const mimeType = rawNode.tagName === "svg" ? "image/svg+xml" : "text/html";
    const parsedDOM = parser.parseFromString(value, mimeType);
    const parentNode = rawNode.parentNode;

    // Special case for head and body.  Setting document.body.outerHTML
    // creates an extra <head> tag, and document.head.outerHTML creates
    // an extra <body>.  So instead we will call replaceChild with the
    // parsed DOM, assuming that they aren't trying to set both tags at once.
    if (rawNode.tagName === "BODY") {
      if (parsedDOM.head.innerHTML === "") {
        parentNode.replaceChild(parsedDOM.body, rawNode);
      } else {
        // eslint-disable-next-line no-unsanitized/property
        rawNode.outerHTML = value;
      }
    } else if (rawNode.tagName === "HEAD") {
      if (parsedDOM.body.innerHTML === "") {
        parentNode.replaceChild(parsedDOM.head, rawNode);
      } else {
        // eslint-disable-next-line no-unsanitized/property
        rawNode.outerHTML = value;
      }
    } else if (node.isDocumentElement()) {
      // Unable to set outerHTML on the document element. Fall back by
      // setting attributes manually. Then replace all the child nodes.
      const finalAttributeModifications = [];
      const attributeModifications = {};
      for (const attribute of rawNode.attributes) {
        attributeModifications[attribute.name] = null;
      }
      for (const attribute of parsedDOM.documentElement.attributes) {
        attributeModifications[attribute.name] = attribute.value;
      }
      for (const key in attributeModifications) {
        finalAttributeModifications.push({
          attributeName: key,
          newValue: attributeModifications[key],
        });
      }
      node.modifyAttributes(finalAttributeModifications);

      rawNode.replaceChildren(...parsedDOM.firstElementChild.childNodes);
    } else {
      // eslint-disable-next-line no-unsanitized/property
      rawNode.outerHTML = value;
    }
  }

  /**
   * Insert adjacent HTML to a node.
   *
   * @param {Node} node
   * @param {string} position One of "beforeBegin", "afterBegin", "beforeEnd",
   *                          "afterEnd" (see Element.insertAdjacentHTML).
   * @param {string} value The HTML content.
   */

  insertAdjacentHTML(node, position, value) {
    if (isNodeDead(node)) {
      return { node: [], newParents: [] };
    }

    const rawNode = node.rawNode;
    const isInsertAsSibling =
      position === "beforeBegin" || position === "afterEnd";

    // Don't insert anything adjacent to the document element.
    if (isInsertAsSibling && node.isDocumentElement()) {
      throw new Error("Can't insert adjacent element to the root.");
    }

    const rawParentNode = rawNode.parentNode;
    if (!rawParentNode && isInsertAsSibling) {
      throw new Error("Can't insert as sibling without parent node.");
    }

    // We can't use insertAdjacentHTML, because we want to return the nodes
    // being created (so the front can remove them if the user undoes
    // the change). So instead, use Range.createContextualFragment().
    const range = rawNode.ownerDocument.createRange();
    if (position === "beforeBegin" || position === "afterEnd") {
      range.selectNode(rawNode);
    } else {
      range.selectNodeContents(rawNode);
    }
    // eslint-disable-next-line no-unsanitized/method
    const docFrag = range.createContextualFragment(value);
    const newRawNodes = Array.from(docFrag.childNodes);
    switch (position) {
      case "beforeBegin":
        rawParentNode.insertBefore(docFrag, rawNode);
        break;
      case "afterEnd":
        // Note: if the second argument is null, rawParentNode.insertBefore
        // behaves like rawParentNode.appendChild.
        rawParentNode.insertBefore(docFrag, rawNode.nextSibling);
        break;
      case "afterBegin":
        rawNode.insertBefore(docFrag, rawNode.firstChild);
        break;
      case "beforeEnd":
        rawNode.appendChild(docFrag);
        break;
      default:
        throw new Error(
          "Invalid position value. Must be either " +
            "'beforeBegin', 'beforeEnd', 'afterBegin' or 'afterEnd'."
        );
    }

    return this.attachElements(newRawNodes);
  }

  /**
   * Duplicate a specified node
   *
   * @param {NodeActor} node The node to duplicate.
   */

  duplicateNode({ rawNode }) {
    const clonedNode = rawNode.cloneNode(true);
    rawNode.parentNode.insertBefore(clonedNode, rawNode.nextSibling);
  }

  /**
   * Test whether a node is a document or a document element.
   *
   * @param {NodeActor} node The node to remove.
   * @return {boolean} True if the node is a document or a document element.
   */

  isDocumentOrDocumentElementNode(node) {
    return (
      (node.rawNode.ownerDocument &&
        node.rawNode.ownerDocument.documentElement === this.rawNode) ||
      node.rawNode.nodeType === Node.DOCUMENT_NODE
    );
  }

  /**
   * Removes a node from its parent node.
   *
   * @param {NodeActor} node The node to remove.
   * @returns The node's nextSibling before it was removed.
   */

  removeNode(node) {
    if (isNodeDead(node) || this.isDocumentOrDocumentElementNode(node)) {
      throw Error("Cannot remove document, document elements or dead nodes.");
    }

    const nextSibling = this.nextSibling(node);
    node.rawNode.remove();
    // Mutation events will take care of the rest.
    return nextSibling;
  }

  /**
   * Removes an array of nodes from their parent node.
   *
   * @param {NodeActor[]} nodes The nodes to remove.
   */

  removeNodes(nodes) {
    // Check that all nodes are valid before processing the removals.
    for (const node of nodes) {
      if (isNodeDead(node) || this.isDocumentOrDocumentElementNode(node)) {
        throw Error("Cannot remove document, document elements or dead nodes");
      }
    }

    for (const node of nodes) {
      node.rawNode.remove();
      // Mutation events will take care of the rest.
    }
  }

  /**
   * Insert a node into the DOM.
   */

  insertBefore(node, parent, sibling) {
    if (
      isNodeDead(node) ||
      isNodeDead(parent) ||
      (sibling && isNodeDead(sibling))
    ) {
      return;
    }

    const rawNode = node.rawNode;
    const rawParent = parent.rawNode;
    const rawSibling = sibling ? sibling.rawNode : null;

    // Don't bother inserting a node if the document position isn't going
    // to change. This prevents needless iframes reloading and mutations.
    if (rawNode.parentNode === rawParent) {
      let currentNextSibling = this.nextSibling(node);
      currentNextSibling = currentNextSibling
        ? currentNextSibling.rawNode
        : null;

      if (rawNode === rawSibling || currentNextSibling === rawSibling) {
        return;
      }
    }

    rawParent.insertBefore(rawNode, rawSibling);
  }

  /**
   * Editing a node's tagname actually means creating a new node with the same
   * attributes, removing the node and inserting the new one instead.
   * This method does not return anything as mutation events are taking care of
   * informing the consumers about changes.
   */

  editTagName(node, tagName) {
    if (isNodeDead(node)) {
      return null;
    }

    const oldNode = node.rawNode;

    // Create a new element with the same attributes as the current element and
    // prepare to replace the current node with it.
    let newNode;
    try {
      newNode = nodeDocument(oldNode).createElement(tagName);
    } catch (x) {
      // Failed to create a new element with that tag name, ignore the change,
      // and signal the error to the front.
      return Promise.reject(
        new Error("Could not change node's tagName to " + tagName)
      );
    }

    const attrs = oldNode.attributes;
    for (let i = 0; i < attrs.length; i++) {
      newNode.setAttribute(attrs[i].name, attrs[i].value);
    }

    // Insert the new node, and transfer the old node's children.
    oldNode.parentNode.insertBefore(newNode, oldNode);
    while (oldNode.firstChild) {
      newNode.appendChild(oldNode.firstChild);
    }

    oldNode.remove();
    return null;
  }

  /**
   * Gets the state of the mutation breakpoint types for this actor.
   *
   * @param {NodeActor} node The node to get breakpoint info for.
   */

  getMutationBreakpoints(node) {
    let bps;
    if (!isNodeDead(node)) {
      bps = this._breakpointInfoForNode(node.rawNode);
    }

    return (
      bps || {
        subtree: false,
        removal: false,
        attribute: false,
      }
    );
  }

  /**
   * Set the state of some subset of mutation breakpoint types for this actor.
   *
   * @param {NodeActor} node The node to set breakpoint info for.
   * @param {Object} bps A subset of the breakpoints for this actor that
   *                            should be updated to new states.
   */

  setMutationBreakpoints(node, bps) {
    if (isNodeDead(node)) {
      return;
    }
    const rawNode = node.rawNode;

    if (
      rawNode.ownerDocument &&
      rawNode.getRootNode({ composed: true }) != rawNode.ownerDocument
    ) {
      // We only allow watching for mutations on nodes that are attached to
      // documents. That allows us to clean up our mutation listeners when all
      // of the watched nodes have been removed from the document.
      return;
    }

    // This argument has nullable fields so we want to only update boolean
    // field values.
    const bpsForNode = Object.keys(bps).reduce((obj, bp) => {
      if (typeof bps[bp] === "boolean") {
        obj[bp] = bps[bp];
      }
      return obj;
    }, {});

    this._updateMutationBreakpointState("api", rawNode, {
      ...this.getMutationBreakpoints(node),
      ...bpsForNode,
    });
  }

  /**
   * Update the mutation breakpoint state for the given DOM node.
   *
   * @param {Node} rawNode The DOM node.
   * @param {Object} bpsForNode The state of each mutation bp type we support.
   */

  _updateMutationBreakpointState(mutationReason, rawNode, bpsForNode) {
    const rawDoc = rawNode.ownerDocument || rawNode;

    const docMutationBreakpoints = this._mutationBreakpointsForDoc(
      rawDoc,
      true /* createIfNeeded */
    );
    let originalBpsForNode = this._breakpointInfoForNode(rawNode);

    if (!bpsForNode && !originalBpsForNode) {
      return;
    }

    bpsForNode = bpsForNode || {};
    originalBpsForNode = originalBpsForNode || {};

    if (Object.values(bpsForNode).some(Boolean)) {
      docMutationBreakpoints.nodes.set(rawNode, bpsForNode);
    } else {
      docMutationBreakpoints.nodes.delete(rawNode);
    }
    if (originalBpsForNode.subtree && !bpsForNode.subtree) {
      docMutationBreakpoints.counts.subtree -= 1;
    } else if (!originalBpsForNode.subtree && bpsForNode.subtree) {
      docMutationBreakpoints.counts.subtree += 1;
    }

    if (originalBpsForNode.removal && !bpsForNode.removal) {
      docMutationBreakpoints.counts.removal -= 1;
    } else if (!originalBpsForNode.removal && bpsForNode.removal) {
      docMutationBreakpoints.counts.removal += 1;
    }

    if (originalBpsForNode.attribute && !bpsForNode.attribute) {
      docMutationBreakpoints.counts.attribute -= 1;
    } else if (!originalBpsForNode.attribute && bpsForNode.attribute) {
      docMutationBreakpoints.counts.attribute += 1;
    }

    this._updateDocumentMutationListeners(rawDoc);

    const actor = this.getNode(rawNode);
    if (actor) {
      this.queueMutation({
        target: actor.actorID,
        type: "mutationBreakpoint",
        mutationBreakpoints: this.getMutationBreakpoints(actor),
        mutationReason,
      });
    }
  }

  /**
   * Controls whether this DOM document has event listeners attached for
   * handling of DOM mutation breakpoints.
   *
   * @param {Document} rawDoc The DOM document.
   */

  _updateDocumentMutationListeners(rawDoc) {
    const docMutationBreakpoints = this._mutationBreakpointsForDoc(rawDoc);
    if (!docMutationBreakpoints) {
      rawDoc.devToolsWatchingDOMMutations = false;
      return;
    }

    const anyBreakpoint =
      docMutationBreakpoints.counts.subtree > 0 ||
      docMutationBreakpoints.counts.removal > 0 ||
      docMutationBreakpoints.counts.attribute > 0;

    rawDoc.devToolsWatchingDOMMutations = anyBreakpoint;

    if (docMutationBreakpoints.counts.subtree > 0) {
      this.chromeEventHandler.addEventListener(
        "devtoolschildinserted",
        this.onSubtreeModified,
        true /* capture */
      );
    } else {
      this.chromeEventHandler.removeEventListener(
        "devtoolschildinserted",
        this.onSubtreeModified,
        true /* capture */
      );
    }

    if (anyBreakpoint) {
      this.chromeEventHandler.addEventListener(
        "devtoolschildremoved",
        this.onNodeRemoved,
        true /* capture */
      );
    } else {
      this.chromeEventHandler.removeEventListener(
        "devtoolschildremoved",
        this.onNodeRemoved,
        true /* capture */
      );
    }

    if (docMutationBreakpoints.counts.attribute > 0) {
      this.chromeEventHandler.addEventListener(
        "devtoolsattrmodified",
        this.onAttributeModified,
        true /* capture */
      );
    } else {
      this.chromeEventHandler.removeEventListener(
        "devtoolsattrmodified",
        this.onAttributeModified,
        true /* capture */
      );
    }
  }

  _breakOnMutation(mutationType, targetNode, ancestorNode, action) {
    this.targetActor.threadActor.pauseForMutationBreakpoint(
      mutationType,
      targetNode,
      ancestorNode,
      action
    );
  }

  _mutationBreakpointsForDoc(rawDoc, createIfNeeded = false) {
    let docMutationBreakpoints = this._mutationBreakpoints.get(rawDoc);
    if (!docMutationBreakpoints && createIfNeeded) {
      docMutationBreakpoints = {
        counts: {
          subtree: 0,
          removal: 0,
          attribute: 0,
        },
        nodes: new Map(),
      };
      this._mutationBreakpoints.set(rawDoc, docMutationBreakpoints);
    }
    return docMutationBreakpoints;
  }

  _breakpointInfoForNode(target) {
    const docMutationBreakpoints = this._mutationBreakpointsForDoc(
      target.ownerDocument || target
    );
    return (
      (docMutationBreakpoints && docMutationBreakpoints.nodes.get(target)) ||
      null
    );
  }

  onNodeRemoved(evt) {
    const mutationBpInfo = this._breakpointInfoForNode(evt.target);
    const hasNodeRemovalEvent = mutationBpInfo?.removal;

    this._clearMutationBreakpointsFromSubtree(evt.target);
    if (hasNodeRemovalEvent) {
      this._breakOnMutation("nodeRemoved", evt.target);
    } else {
      this.onSubtreeModified(evt);
    }
  }

  onAttributeModified(evt) {
    const mutationBpInfo = this._breakpointInfoForNode(evt.target);
    if (mutationBpInfo?.attribute) {
      this._breakOnMutation("attributeModified", evt.target);
    }
  }

  onSubtreeModified(evt) {
    const action = evt.type === "devtoolschildinserted" ? "add" : "remove";
    let node = evt.target;
    if (node.isNativeAnonymous && !this.showAllAnonymousContent) {
      return;
    }
    while ((node = node.parentNode) !== null) {
      const mutationBpInfo = this._breakpointInfoForNode(node);
      if (mutationBpInfo?.subtree) {
        this._breakOnMutation("subtreeModified", evt.target, node, action);
        break;
      }
    }
  }

  _clearMutationBreakpointsFromSubtree(targetNode) {
    const targetDoc = targetNode.ownerDocument || targetNode;
    const docMutationBreakpoints = this._mutationBreakpointsForDoc(targetDoc);
    if (!docMutationBreakpoints || docMutationBreakpoints.nodes.size === 0) {
      // Bail early for performance. If the doc has no mutation BPs, there is
      // no reason to iterate through the children looking for things to detach.
      return;
    }

    // The walker is not limited to the subtree of the argument node, so we
    // need to ensure that we stop walking when we leave the subtree.
    const nextWalkerSibling = this._getNextTraversalSibling(targetNode);

    const walker = new DocumentWalker(targetNode, this.rootWin, {
      filter: noAnonymousContentTreeWalkerFilter,
      skipTo: SKIP_TO_SIBLING,
    });

    do {
      this._updateMutationBreakpointState("detach", walker.currentNode, null);
    } while (walker.nextNode() && walker.currentNode !== nextWalkerSibling);
  }

  _getNextTraversalSibling(targetNode) {
    const walker = new DocumentWalker(targetNode, this.rootWin, {
      filter: noAnonymousContentTreeWalkerFilter,
      skipTo: SKIP_TO_SIBLING,
    });

    while (!walker.nextSibling()) {
      if (!walker.parentNode()) {
        // If we try to step past the walker root, there is no next sibling.
        return null;
      }
    }
    return walker.currentNode;
  }

  /**
   * Get any pending mutation records.  Must be called by the client after
   * the `new-mutations` notification is received.  Returns an array of
   * mutation records.
   *
   * Mutation records have a basic structure:
   *
   * {
   *   type: attributes|characterData|childList,
   *   target: <domnode actor ID>,
   * }
   *
   * And additional attributes based on the mutation type:
   *
   * `attributes` type:
   *   attributeName: <string> - the attribute that changed
   *   attributeNamespace: <string> - the attribute's namespace URI, if any.
   *   newValue: <string> - The new value of the attribute, if any.
   *
   * `characterData` type:
   *   newValue: <string> - the new nodeValue for the node
   *
   * `childList` type is returned when the set of children for a node
   * has changed.  Includes extra data, which can be used by the client to
   * maintain its ownership subtree.
   *
   *   added: array of <domnode actor ID> - The list of actors *previously
   *     seen by the client* that were added to the target node.
   *   removed: array of <domnode actor ID> The list of actors *previously
   *     seen by the client* that were removed from the target node.
   *   inlineTextChild: If the node now has a single text child, it will
   *     be sent here.
   *
   * Actors that are included in a MutationRecord's `removed` but
   * not in an `added` have been removed from the client's ownership
   * tree (either by being moved under a node the client has seen yet
   * or by being removed from the tree entirely), and is considered
   * 'orphaned'.
   *
   * Keep in mind that if a node that the client hasn't seen is moved
   * into or out of the target node, it will not be included in the
   * removedNodes and addedNodes list, so if the client is interested
   * in the new set of children it needs to issue a `children` request.
   */

  getMutations(options = {}) {
    const pending = this._pendingMutations || [];
    this._pendingMutations = [];
    this._waitingForGetMutations = false;

    if (options.cleanup) {
      for (const node of this._orphaned) {
        // Release the orphaned node.  Nodes or children that have been
        // retained will be moved to this._retainedOrphans.
        this.releaseNode(node);
      }
      this._orphaned = new Set();
    }

    return pending;
  }

  queueMutation(mutation) {
    if (!this.actorID || this._destroyed) {
      // We've been destroyed, don't bother queueing this mutation.
      return;
    }

    // Add the mutation to the list of mutations to be retrieved next.
    this._pendingMutations.push(mutation);

    // Bail out if we already emitted a new-mutations event and are waiting for a client
    // to retrieve them.
    if (this._waitingForGetMutations) {
      return;
    }

    if (IMMEDIATE_MUTATIONS.includes(mutation.type)) {
      this._emitNewMutations();
    } else {
      /**
       * If many mutations are fired at the same time, clients might sequentially request
       * children/siblings for updated nodes, which can be costly. By throttling the calls
       * to getMutations, duplicated mutations will be ignored.
       */

      this._throttledEmitNewMutations();
    }
  }

  _emitNewMutations() {
    if (!this.actorID || this._destroyed) {
      // Bail out if the actor was destroyed after throttling this call.
      return;
    }

    if (this._waitingForGetMutations || !this._pendingMutations.length) {
      // Bail out if we already fired the new-mutation event or if no mutations are
      // waiting to be retrieved.
      return;
    }

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

--> maximum size reached

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

Messung V0.5
C=92 H=89 G=90

¤ Dauer der Verarbeitung: 0.28 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.






                                                                                                                                                                                                                                                                                                                                                                                                     


Neuigkeiten

     Aktuelles
     Motto des Tages

Software

     Produkte
     Quellcodebibliothek

Aktivitäten

     Artikel über Sicherheit
     Anleitung zur Aktivierung von SSL

Muße

     Gedichte
     Musik
     Bilder

Jenseits des Üblichen ....
    

Besucherstatistik

Besucherstatistik

Monitoring

Montastic status badge