1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
11 } = require("resource://devtools/shared/protocol.js");
15 } = require("resource://devtools/shared/specs/node.js");
18 } = require("resource://devtools/client/fronts/string.js");
20 loader
.lazyRequireGetter(
23 "resource://devtools/shared/dom-node-constants.js"
26 const { XPCOMUtils
} = ChromeUtils
.importESModule(
27 "resource://gre/modules/XPCOMUtils.sys.mjs"
30 XPCOMUtils
.defineLazyPreferenceGetter(
32 "browserToolboxScope",
33 "devtools.browsertoolbox.scope"
36 const BROWSER_TOOLBOX_SCOPE_EVERYTHING
= "everything";
38 const HIDDEN_CLASS
= "__fx-devtools-hide-shortcut__";
41 * Client side of a node list as returned by querySelectorAll()
43 class NodeListFront
extends FrontClassWithSpec(nodeListSpec
) {
45 return this.getParent();
48 // Update the object given a form representation off the wire.
50 this.length
= json
.length
;
54 return super.item(index
).then(response
=> {
60 return super.items(start
, end
).then(response
=> {
61 return response
.nodes
;
66 exports
.NodeListFront
= NodeListFront
;
67 registerFront(NodeListFront
);
70 * Convenience API for building a list of attribute modifications
71 * for the `modifyAttributes` request.
73 class AttributeModificationList
{
76 this.modifications
= [];
80 const ret
= this.node
.modifyAttributes(this.modifications
);
86 this.modification
= null;
89 setAttributeNS(ns
, name
, value
) {
90 this.modifications
.push({
91 attributeNamespace
: ns
,
97 setAttribute(name
, value
) {
98 this.setAttributeNS(undefined, name
, value
);
101 removeAttributeNS(ns
, name
) {
102 this.setAttributeNS(ns
, name
, undefined);
105 removeAttribute(name
) {
106 this.setAttributeNS(undefined, name
, undefined);
111 * Client side of the node actor.
113 * Node fronts are strored in a tree that mirrors the DOM tree on the
114 * server, but with a few key differences:
115 * - Not all children will be necessary loaded for each node.
116 * - The order of children isn't guaranteed to be the same as the DOM.
117 * Children are stored in a doubly-linked list, to make addition/removal
118 * and traversal quick.
120 * Due to the order/incompleteness of the child list, it is safe to use
121 * the parent node from clients, but the `children` request should be used
122 * to traverse children.
124 class NodeFront
extends FrontClassWithSpec(nodeSpec
) {
125 constructor(conn
, targetFront
, parentFront
) {
126 super(conn
, targetFront
, parentFront
);
129 // The first child of this node.
131 // The next sibling of this node.
133 // The previous sibling of this node.
135 // Store the flag to use it after destroy, where targetFront is set to null.
136 this._hasParentProcessTarget
= targetFront
.isParentProcess
;
140 * Destroy a node front. The node must have been removed from the
141 * ownership tree before this is called, unless the whole walker front
142 * is being destroyed.
148 // Update the object given a form representation off the wire.
150 // backward-compatibility: shortValue indicates we are connected to old server
151 if (form
.shortValue
) {
152 // If the value is not complete, set nodeValue to null, it will be fetched
153 // when calling getNodeValue()
154 form
.nodeValue
= form
.incompleteValue
? null : form
.shortValue
;
157 this.traits
= form
.traits
|| {};
159 // Shallow copy of the form. We could just store a reference, but
160 // eventually we'll want to update some of the data.
161 this._form
= Object
.assign({}, form
);
162 this._form
.attrs
= this._form
.attrs
? this._form
.attrs
.slice() : [];
165 // Get the owner actor for this actor (the walker), and find the
166 // parent node of this actor from it, creating a standin node if
168 const owner
= ctx
.marshallPool();
169 if (typeof owner
.ensureDOMNodeFront
=== "function") {
170 const parentNodeFront
= owner
.ensureDOMNodeFront(form
.parent
);
171 this.reparent(parentNodeFront
);
176 const owner
= ctx
.marshallPool();
177 if (typeof owner
.ensureDOMNodeFront
=== "function") {
178 this.host
= owner
.ensureDOMNodeFront(form
.host
);
182 if (form
.inlineTextChild
) {
183 this.inlineTextChild
= types
185 .read(form
.inlineTextChild
, ctx
);
187 this.inlineTextChild
= undefined;
192 * Returns the parent NodeFront for this NodeFront.
199 * Returns the NodeFront corresponding to the parentNode of this NodeFront, or the
200 * NodeFront corresponding to the host element for shadowRoot elements.
203 return this.isShadowRoot
? this.host
: this._parent
;
207 * Returns the owner DocumentElement|ShadowRootElement NodeFront for this NodeFront,
208 * or null if such element can't be found.
210 * @returns {NodeFront|null}
212 getOwnerRootNodeFront() {
213 let currentNode
= this;
214 while (currentNode
) {
216 currentNode
.isShadowRoot
||
217 currentNode
.nodeType
=== Node
.DOCUMENT_NODE
222 currentNode
= currentNode
.parentNode();
229 * Process a mutation entry as returned from the walker's `getMutations`
230 * request. Only tries to handle changes of the node's contents
231 * themselves (character data and attribute changes), the walker itself
232 * will keep the ownership tree up to date.
234 updateMutation(change
) {
235 if (change
.type
=== "attributes") {
236 // We'll need to lazily reparse the attributes after this change.
237 this._attrMap
= undefined;
239 // Update any already-existing attributes.
241 for (let i
= 0; i
< this.attributes
.length
; i
++) {
242 const attr
= this.attributes
[i
];
244 attr
.name
== change
.attributeName
&&
245 attr
.namespace == change
.attributeNamespace
247 if (change
.newValue
!== null) {
248 attr
.value
= change
.newValue
;
250 this.attributes
.splice(i
, 1);
256 // This is a new attribute. The null check is because of Bug 1192270,
257 // in the case of a newly added then removed attribute
258 if (!found
&& change
.newValue
!== null) {
259 this.attributes
.push({
260 name
: change
.attributeName
,
261 namespace: change
.attributeNamespace
,
262 value
: change
.newValue
,
265 } else if (change
.type
=== "characterData") {
266 this._form
.nodeValue
= change
.newValue
;
267 } else if (change
.type
=== "pseudoClassLock") {
268 this._form
.pseudoClassLocks
= change
.pseudoClassLocks
;
269 } else if (change
.type
=== "events") {
270 this._form
.hasEventListeners
= change
.hasEventListeners
;
271 } else if (change
.type
=== "mutationBreakpoint") {
272 this._form
.mutationBreakpoints
= change
.mutationBreakpoints
;
276 // Some accessors to make NodeFront feel more like a Node
279 return this.getAttribute("id");
283 return this._form
.nodeType
;
286 return this._form
.namespaceURI
;
289 return this._form
.nodeName
;
292 const { displayName
, nodeName
} = this._form
;
294 // Keep `nodeName.toLowerCase()` for backward compatibility
295 return displayName
|| nodeName
.toLowerCase();
297 get doctypeString() {
301 (this._form
.publicId
? ' PUBLIC "' + this._form
.publicId
+ '"' : "") +
302 (this._form
.systemId
? ' "' + this._form
.systemId
+ '"' : "") +
308 return this._form
.baseURI
;
311 get browsingContextID() {
312 return this._form
.browsingContextID
;
316 return this.getAttribute("class") || "";
319 // Check if the node has children but the current DevTools session is unable
321 // Typically: a <frame> or <browser> element which loads a document in another
322 // process, but the toolbox' configuration prevents to inspect it (eg the
323 // parent-process only Browser Toolbox).
324 get childrenUnavailable() {
326 // If form.useChildTargetToFetchChildren is true, it means the node HAS
327 // children in another target.
328 // Note: useChildTargetToFetchChildren might be undefined, force
329 // conversion to boolean. See Bug 1783613 to try and improve this.
330 !!this._form
.useChildTargetToFetchChildren
&&
331 // But if useChildTargetToFetchChildren is false, it means the client
332 // configuration prevents from displaying such children.
333 // This is the only case where children are considered as unavailable:
334 // they exist, but can't be retrieved by configuration.
335 !this.useChildTargetToFetchChildren
339 return this.numChildren
> 0;
342 if (this.childrenUnavailable
) {
346 return this._form
.numChildren
;
348 get useChildTargetToFetchChildren() {
350 this._hasParentProcessTarget
&&
351 browserToolboxScope
!= BROWSER_TOOLBOX_SCOPE_EVERYTHING
356 return !!this._form
.useChildTargetToFetchChildren
;
358 get hasEventListeners() {
359 return this._form
.hasEventListeners
;
362 get isMarkerPseudoElement() {
363 return this._form
.isMarkerPseudoElement
;
365 get isBeforePseudoElement() {
366 return this._form
.isBeforePseudoElement
;
368 get isAfterPseudoElement() {
369 return this._form
.isAfterPseudoElement
;
371 get isPseudoElement() {
373 this.isBeforePseudoElement
||
374 this.isAfterPseudoElement
||
375 this.isMarkerPseudoElement
379 return this._form
.isAnonymous
;
381 get isInHTMLDocument() {
382 return this._form
.isInHTMLDocument
;
385 return this.nodeType
=== nodeConstants
.ELEMENT_NODE
? this.nodeName
: null;
388 get isDocumentElement() {
389 return !!this._form
.isDocumentElement
;
392 get isTopLevelDocument() {
393 return this._form
.isTopLevelDocument
;
397 return this._form
.isShadowRoot
;
400 get shadowRootMode() {
401 return this._form
.shadowRootMode
;
405 return this._form
.isShadowHost
;
408 get customElementLocation() {
409 return this._form
.customElementLocation
;
412 get isDirectShadowHostChild() {
413 return this._form
.isDirectShadowHostChild
;
416 // doctype properties
418 return this._form
.name
;
421 return this._form
.publicId
;
424 return this._form
.systemId
;
428 const attr
= this._getAttribute(name
);
429 return attr
? attr
.value
: null;
432 this._cacheAttributes();
433 return name
in this._attrMap
;
437 const cls
= this.getAttribute("class");
438 return cls
&& cls
.indexOf(HIDDEN_CLASS
) > -1;
442 return this._form
.attrs
;
445 get mutationBreakpoints() {
446 return this._form
.mutationBreakpoints
;
449 get pseudoClassLocks() {
450 return this._form
.pseudoClassLocks
|| [];
452 hasPseudoClassLock(pseudo
) {
453 return this.pseudoClassLocks
.some(locked
=> locked
=== pseudo
);
457 return this._form
.displayType
;
461 return this._form
.isDisplayed
;
465 return this._form
.isScrollable
;
468 get causesOverflow() {
469 return this._form
.causesOverflow
;
472 get containerType() {
473 return this._form
.containerType
;
476 get isTreeDisplayed() {
479 if (!parent
.isDisplayed
) {
482 parent
= parent
.parentNode();
487 get inspectorFront() {
488 return this.parentFront
.parentFront
;
492 return this.parentFront
;
496 // backward-compatibility: if nodevalue is null and shortValue is defined, the actual
497 // value of the node needs to be fetched on the server.
498 if (this._form
.nodeValue
=== null && this._form
.shortValue
) {
499 return super.getNodeValue();
502 const str
= this._form
.nodeValue
|| "";
503 return Promise
.resolve(new SimpleStringFront(str
));
507 * Return a new AttributeModificationList for this node.
509 startModifyingAttributes() {
510 return new AttributeModificationList(this);
514 if (typeof this._attrMap
!= "undefined") {
518 for (const attr
of this.attributes
) {
519 this._attrMap
[attr
.name
] = attr
;
523 _getAttribute(name
) {
524 this._cacheAttributes();
525 return this._attrMap
[name
] || undefined;
529 * Set this node's parent. Note that the children saved in
530 * this tree are unordered and incomplete, so shouldn't be used
531 * instead of a `children` request.
534 if (this._parent
=== parent
) {
538 if (this._parent
&& this._parent
._child
=== this) {
539 this._parent
._child
= this._next
;
542 this._prev
._next
= this._next
;
545 this._next
._prev
= this._prev
;
549 this._parent
= parent
;
551 // Subtree is disconnected, we're done
554 this._next
= parent
._child
;
556 this._next
._prev
= this;
558 parent
._child
= this;
562 * Return all the known children of this node.
566 for (let child
= this._child
; child
!= null; child
= child
._next
) {
573 * Do we use a local target?
574 * Useful to know if a rawNode is available or not.
576 * This will, one day, be removed. External code should
577 * not need to know if the target is remote or not.
579 isLocalToBeDeprecated() {
580 return !!this.conn
._transport
._serverConnection
;
584 * Get a Node for the given node front. This only works locally,
585 * and is only intended as a stopgap during the transition to the remote
586 * protocol. If you depend on this you're likely to break soon.
589 if (!this.isLocalToBeDeprecated()) {
590 console
.warn("Tried to use rawNode on a remote connection.");
595 } = require("resource://devtools/server/devtools-server.js");
596 const actor
= DevToolsServer
.searchAllConnectionsForActor(this.actorID
);
598 // Can happen if we try to get the raw node for an already-expired
602 return actor
.rawNode
;
605 async
connectToFrame() {
606 if (!this.useChildTargetToFetchChildren
) {
607 console
.warn("Tried to open connection to an invalid frame.");
611 this._childBrowsingContextTarget
&&
612 !this._childBrowsingContextTarget
.isDestroyed()
614 return this._childBrowsingContextTarget
;
617 // Get the target for this frame element
618 this._childBrowsingContextTarget
=
619 await
this.targetFront
.getWindowGlobalTarget(
620 this._form
.browsingContextID
623 // Bug 1776250: When the target is destroyed, we need to easily find the
624 // parent node front so that we can update its frontend container in the
626 this._childBrowsingContextTarget
.setParentNodeFront(this);
628 return this._childBrowsingContextTarget
;
632 exports
.NodeFront
= NodeFront
;
633 registerFront(NodeFront
);