summaryrefslogtreecommitdiffstats
path: root/devtools/client/fronts/node.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/fronts/node.js')
-rw-r--r--devtools/client/fronts/node.js633
1 files changed, 633 insertions, 0 deletions
diff --git a/devtools/client/fronts/node.js b/devtools/client/fronts/node.js
new file mode 100644
index 0000000000..8df63f2c1b
--- /dev/null
+++ b/devtools/client/fronts/node.js
@@ -0,0 +1,633 @@
+/* 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 {
+ FrontClassWithSpec,
+ types,
+ registerFront,
+} = require("resource://devtools/shared/protocol.js");
+const {
+ nodeSpec,
+ nodeListSpec,
+} = require("resource://devtools/shared/specs/node.js");
+const {
+ SimpleStringFront,
+} = require("resource://devtools/client/fronts/string.js");
+
+loader.lazyRequireGetter(
+ this,
+ "nodeConstants",
+ "resource://devtools/shared/dom-node-constants.js"
+);
+
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "browserToolboxScope",
+ "devtools.browsertoolbox.scope"
+);
+
+const BROWSER_TOOLBOX_SCOPE_EVERYTHING = "everything";
+
+const HIDDEN_CLASS = "__fx-devtools-hide-shortcut__";
+
+/**
+ * Client side of a node list as returned by querySelectorAll()
+ */
+class NodeListFront extends FrontClassWithSpec(nodeListSpec) {
+ marshallPool() {
+ return this.getParent();
+ }
+
+ // Update the object given a form representation off the wire.
+ form(json) {
+ this.length = json.length;
+ }
+
+ item(index) {
+ return super.item(index).then(response => {
+ return response.node;
+ });
+ }
+
+ items(start, end) {
+ return super.items(start, end).then(response => {
+ return response.nodes;
+ });
+ }
+}
+
+exports.NodeListFront = NodeListFront;
+registerFront(NodeListFront);
+
+/**
+ * Convenience API for building a list of attribute modifications
+ * for the `modifyAttributes` request.
+ */
+class AttributeModificationList {
+ constructor(node) {
+ this.node = node;
+ this.modifications = [];
+ }
+
+ apply() {
+ const ret = this.node.modifyAttributes(this.modifications);
+ return ret;
+ }
+
+ destroy() {
+ this.node = null;
+ this.modification = null;
+ }
+
+ setAttributeNS(ns, name, value) {
+ this.modifications.push({
+ attributeNamespace: ns,
+ attributeName: name,
+ newValue: value,
+ });
+ }
+
+ setAttribute(name, value) {
+ this.setAttributeNS(undefined, name, value);
+ }
+
+ removeAttributeNS(ns, name) {
+ this.setAttributeNS(ns, name, undefined);
+ }
+
+ removeAttribute(name) {
+ this.setAttributeNS(undefined, name, undefined);
+ }
+}
+
+/**
+ * Client side of the node actor.
+ *
+ * Node fronts are strored in a tree that mirrors the DOM tree on the
+ * server, but with a few key differences:
+ * - Not all children will be necessary loaded for each node.
+ * - The order of children isn't guaranteed to be the same as the DOM.
+ * Children are stored in a doubly-linked list, to make addition/removal
+ * and traversal quick.
+ *
+ * Due to the order/incompleteness of the child list, it is safe to use
+ * the parent node from clients, but the `children` request should be used
+ * to traverse children.
+ */
+class NodeFront extends FrontClassWithSpec(nodeSpec) {
+ constructor(conn, targetFront, parentFront) {
+ super(conn, targetFront, parentFront);
+ // The parent node
+ this._parent = null;
+ // The first child of this node.
+ this._child = null;
+ // The next sibling of this node.
+ this._next = null;
+ // The previous sibling of this node.
+ this._prev = null;
+ // Store the flag to use it after destroy, where targetFront is set to null.
+ this._hasParentProcessTarget = targetFront.isParentProcess;
+ }
+
+ /**
+ * Destroy a node front. The node must have been removed from the
+ * ownership tree before this is called, unless the whole walker front
+ * is being destroyed.
+ */
+ destroy() {
+ super.destroy();
+ }
+
+ // Update the object given a form representation off the wire.
+ form(form, ctx) {
+ // backward-compatibility: shortValue indicates we are connected to old server
+ if (form.shortValue) {
+ // If the value is not complete, set nodeValue to null, it will be fetched
+ // when calling getNodeValue()
+ form.nodeValue = form.incompleteValue ? null : form.shortValue;
+ }
+
+ this.traits = form.traits || {};
+
+ // Shallow copy of the form. We could just store a reference, but
+ // eventually we'll want to update some of the data.
+ this._form = Object.assign({}, form);
+ this._form.attrs = this._form.attrs ? this._form.attrs.slice() : [];
+
+ if (form.parent) {
+ // Get the owner actor for this actor (the walker), and find the
+ // parent node of this actor from it, creating a standin node if
+ // necessary.
+ const owner = ctx.marshallPool();
+ if (typeof owner.ensureDOMNodeFront === "function") {
+ const parentNodeFront = owner.ensureDOMNodeFront(form.parent);
+ this.reparent(parentNodeFront);
+ }
+ }
+
+ if (form.host) {
+ const owner = ctx.marshallPool();
+ if (typeof owner.ensureDOMNodeFront === "function") {
+ this.host = owner.ensureDOMNodeFront(form.host);
+ }
+ }
+
+ if (form.inlineTextChild) {
+ this.inlineTextChild = types
+ .getType("domnode")
+ .read(form.inlineTextChild, ctx);
+ } else {
+ this.inlineTextChild = undefined;
+ }
+ }
+
+ /**
+ * Returns the parent NodeFront for this NodeFront.
+ */
+ parentNode() {
+ return this._parent;
+ }
+
+ /**
+ * Returns the NodeFront corresponding to the parentNode of this NodeFront, or the
+ * NodeFront corresponding to the host element for shadowRoot elements.
+ */
+ parentOrHost() {
+ return this.isShadowRoot ? this.host : this._parent;
+ }
+
+ /**
+ * Returns the owner DocumentElement|ShadowRootElement NodeFront for this NodeFront,
+ * or null if such element can't be found.
+ *
+ * @returns {NodeFront|null}
+ */
+ getOwnerRootNodeFront() {
+ let currentNode = this;
+ while (currentNode) {
+ if (
+ currentNode.isShadowRoot ||
+ currentNode.nodeType === Node.DOCUMENT_NODE
+ ) {
+ return currentNode;
+ }
+
+ currentNode = currentNode.parentNode();
+ }
+
+ return null;
+ }
+
+ /**
+ * Process a mutation entry as returned from the walker's `getMutations`
+ * request. Only tries to handle changes of the node's contents
+ * themselves (character data and attribute changes), the walker itself
+ * will keep the ownership tree up to date.
+ */
+ updateMutation(change) {
+ if (change.type === "attributes") {
+ // We'll need to lazily reparse the attributes after this change.
+ this._attrMap = undefined;
+
+ // Update any already-existing attributes.
+ let found = false;
+ for (let i = 0; i < this.attributes.length; i++) {
+ const attr = this.attributes[i];
+ if (
+ attr.name == change.attributeName &&
+ attr.namespace == change.attributeNamespace
+ ) {
+ if (change.newValue !== null) {
+ attr.value = change.newValue;
+ } else {
+ this.attributes.splice(i, 1);
+ }
+ found = true;
+ break;
+ }
+ }
+ // This is a new attribute. The null check is because of Bug 1192270,
+ // in the case of a newly added then removed attribute
+ if (!found && change.newValue !== null) {
+ this.attributes.push({
+ name: change.attributeName,
+ namespace: change.attributeNamespace,
+ value: change.newValue,
+ });
+ }
+ } else if (change.type === "characterData") {
+ this._form.nodeValue = change.newValue;
+ } else if (change.type === "pseudoClassLock") {
+ this._form.pseudoClassLocks = change.pseudoClassLocks;
+ } else if (change.type === "events") {
+ this._form.hasEventListeners = change.hasEventListeners;
+ } else if (change.type === "mutationBreakpoint") {
+ this._form.mutationBreakpoints = change.mutationBreakpoints;
+ }
+ }
+
+ // Some accessors to make NodeFront feel more like a Node
+
+ get id() {
+ return this.getAttribute("id");
+ }
+
+ get nodeType() {
+ return this._form.nodeType;
+ }
+ get namespaceURI() {
+ return this._form.namespaceURI;
+ }
+ get nodeName() {
+ return this._form.nodeName;
+ }
+ get displayName() {
+ const { displayName, nodeName } = this._form;
+
+ // Keep `nodeName.toLowerCase()` for backward compatibility
+ return displayName || nodeName.toLowerCase();
+ }
+ get doctypeString() {
+ return (
+ "<!DOCTYPE " +
+ this._form.name +
+ (this._form.publicId ? ' PUBLIC "' + this._form.publicId + '"' : "") +
+ (this._form.systemId ? ' "' + this._form.systemId + '"' : "") +
+ ">"
+ );
+ }
+
+ get baseURI() {
+ return this._form.baseURI;
+ }
+
+ get browsingContextID() {
+ return this._form.browsingContextID;
+ }
+
+ get className() {
+ return this.getAttribute("class") || "";
+ }
+
+ // Check if the node has children but the current DevTools session is unable
+ // to retrieve them.
+ // Typically: a <frame> or <browser> element which loads a document in another
+ // process, but the toolbox' configuration prevents to inspect it (eg the
+ // parent-process only Browser Toolbox).
+ get childrenUnavailable() {
+ return (
+ // If form.useChildTargetToFetchChildren is true, it means the node HAS
+ // children in another target.
+ // Note: useChildTargetToFetchChildren might be undefined, force
+ // conversion to boolean. See Bug 1783613 to try and improve this.
+ !!this._form.useChildTargetToFetchChildren &&
+ // But if useChildTargetToFetchChildren is false, it means the client
+ // configuration prevents from displaying such children.
+ // This is the only case where children are considered as unavailable:
+ // they exist, but can't be retrieved by configuration.
+ !this.useChildTargetToFetchChildren
+ );
+ }
+ get hasChildren() {
+ return this.numChildren > 0;
+ }
+ get numChildren() {
+ if (this.childrenUnavailable) {
+ return 0;
+ }
+
+ return this._form.numChildren;
+ }
+ get useChildTargetToFetchChildren() {
+ if (
+ this._hasParentProcessTarget &&
+ browserToolboxScope != BROWSER_TOOLBOX_SCOPE_EVERYTHING
+ ) {
+ return false;
+ }
+
+ return !!this._form.useChildTargetToFetchChildren;
+ }
+ get hasEventListeners() {
+ return this._form.hasEventListeners;
+ }
+
+ get isMarkerPseudoElement() {
+ return this._form.isMarkerPseudoElement;
+ }
+ get isBeforePseudoElement() {
+ return this._form.isBeforePseudoElement;
+ }
+ get isAfterPseudoElement() {
+ return this._form.isAfterPseudoElement;
+ }
+ get isPseudoElement() {
+ return (
+ this.isBeforePseudoElement ||
+ this.isAfterPseudoElement ||
+ this.isMarkerPseudoElement
+ );
+ }
+ get isAnonymous() {
+ return this._form.isAnonymous;
+ }
+ get isInHTMLDocument() {
+ return this._form.isInHTMLDocument;
+ }
+ get tagName() {
+ return this.nodeType === nodeConstants.ELEMENT_NODE ? this.nodeName : null;
+ }
+
+ get isDocumentElement() {
+ return !!this._form.isDocumentElement;
+ }
+
+ get isTopLevelDocument() {
+ return this._form.isTopLevelDocument;
+ }
+
+ get isShadowRoot() {
+ return this._form.isShadowRoot;
+ }
+
+ get shadowRootMode() {
+ return this._form.shadowRootMode;
+ }
+
+ get isShadowHost() {
+ return this._form.isShadowHost;
+ }
+
+ get customElementLocation() {
+ return this._form.customElementLocation;
+ }
+
+ get isDirectShadowHostChild() {
+ return this._form.isDirectShadowHostChild;
+ }
+
+ // doctype properties
+ get name() {
+ return this._form.name;
+ }
+ get publicId() {
+ return this._form.publicId;
+ }
+ get systemId() {
+ return this._form.systemId;
+ }
+
+ getAttribute(name) {
+ const attr = this._getAttribute(name);
+ return attr ? attr.value : null;
+ }
+ hasAttribute(name) {
+ this._cacheAttributes();
+ return name in this._attrMap;
+ }
+
+ get hidden() {
+ const cls = this.getAttribute("class");
+ return cls && cls.indexOf(HIDDEN_CLASS) > -1;
+ }
+
+ get attributes() {
+ return this._form.attrs;
+ }
+
+ get mutationBreakpoints() {
+ return this._form.mutationBreakpoints;
+ }
+
+ get pseudoClassLocks() {
+ return this._form.pseudoClassLocks || [];
+ }
+ hasPseudoClassLock(pseudo) {
+ return this.pseudoClassLocks.some(locked => locked === pseudo);
+ }
+
+ get displayType() {
+ return this._form.displayType;
+ }
+
+ get isDisplayed() {
+ return this._form.isDisplayed;
+ }
+
+ get isScrollable() {
+ return this._form.isScrollable;
+ }
+
+ get causesOverflow() {
+ return this._form.causesOverflow;
+ }
+
+ get containerType() {
+ return this._form.containerType;
+ }
+
+ get isTreeDisplayed() {
+ let parent = this;
+ while (parent) {
+ if (!parent.isDisplayed) {
+ return false;
+ }
+ parent = parent.parentNode();
+ }
+ return true;
+ }
+
+ get inspectorFront() {
+ return this.parentFront.parentFront;
+ }
+
+ get walkerFront() {
+ return this.parentFront;
+ }
+
+ getNodeValue() {
+ // backward-compatibility: if nodevalue is null and shortValue is defined, the actual
+ // value of the node needs to be fetched on the server.
+ if (this._form.nodeValue === null && this._form.shortValue) {
+ return super.getNodeValue();
+ }
+
+ const str = this._form.nodeValue || "";
+ return Promise.resolve(new SimpleStringFront(str));
+ }
+
+ /**
+ * Return a new AttributeModificationList for this node.
+ */
+ startModifyingAttributes() {
+ return new AttributeModificationList(this);
+ }
+
+ _cacheAttributes() {
+ if (typeof this._attrMap != "undefined") {
+ return;
+ }
+ this._attrMap = {};
+ for (const attr of this.attributes) {
+ this._attrMap[attr.name] = attr;
+ }
+ }
+
+ _getAttribute(name) {
+ this._cacheAttributes();
+ return this._attrMap[name] || undefined;
+ }
+
+ /**
+ * Set this node's parent. Note that the children saved in
+ * this tree are unordered and incomplete, so shouldn't be used
+ * instead of a `children` request.
+ */
+ reparent(parent) {
+ if (this._parent === parent) {
+ return;
+ }
+
+ if (this._parent && this._parent._child === this) {
+ this._parent._child = this._next;
+ }
+ if (this._prev) {
+ this._prev._next = this._next;
+ }
+ if (this._next) {
+ this._next._prev = this._prev;
+ }
+ this._next = null;
+ this._prev = null;
+ this._parent = parent;
+ if (!parent) {
+ // Subtree is disconnected, we're done
+ return;
+ }
+ this._next = parent._child;
+ if (this._next) {
+ this._next._prev = this;
+ }
+ parent._child = this;
+ }
+
+ /**
+ * Return all the known children of this node.
+ */
+ treeChildren() {
+ const ret = [];
+ for (let child = this._child; child != null; child = child._next) {
+ ret.push(child);
+ }
+ return ret;
+ }
+
+ /**
+ * Do we use a local target?
+ * Useful to know if a rawNode is available or not.
+ *
+ * This will, one day, be removed. External code should
+ * not need to know if the target is remote or not.
+ */
+ isLocalToBeDeprecated() {
+ return !!this.conn._transport._serverConnection;
+ }
+
+ /**
+ * Get a Node for the given node front. This only works locally,
+ * and is only intended as a stopgap during the transition to the remote
+ * protocol. If you depend on this you're likely to break soon.
+ */
+ rawNode(rawNode) {
+ if (!this.isLocalToBeDeprecated()) {
+ console.warn("Tried to use rawNode on a remote connection.");
+ return null;
+ }
+ const {
+ DevToolsServer,
+ } = require("resource://devtools/server/devtools-server.js");
+ const actor = DevToolsServer.searchAllConnectionsForActor(this.actorID);
+ if (!actor) {
+ // Can happen if we try to get the raw node for an already-expired
+ // actor.
+ return null;
+ }
+ return actor.rawNode;
+ }
+
+ async connectToFrame() {
+ if (!this.useChildTargetToFetchChildren) {
+ console.warn("Tried to open connection to an invalid frame.");
+ return null;
+ }
+ if (
+ this._childBrowsingContextTarget &&
+ !this._childBrowsingContextTarget.isDestroyed()
+ ) {
+ return this._childBrowsingContextTarget;
+ }
+
+ // Get the target for this frame element
+ this._childBrowsingContextTarget =
+ await this.targetFront.getWindowGlobalTarget(
+ this._form.browsingContextID
+ );
+
+ // Bug 1776250: When the target is destroyed, we need to easily find the
+ // parent node front so that we can update its frontend container in the
+ // markup-view.
+ this._childBrowsingContextTarget.setParentNodeFront(this);
+
+ return this._childBrowsingContextTarget;
+ }
+}
+
+exports.NodeFront = NodeFront;
+registerFront(NodeFront);