/* 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 (
""
);
}
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 or 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 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);