Backed out changeset b71c8c052463 (bug 1943846) for causing mass failures. CLOSED...
[gecko.git] / devtools / client / fronts / node.js
blobd56f2d93f11ef700541262a7e9e8363c867498c3
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/. */
5 "use strict";
7 const {
8 FrontClassWithSpec,
9 types,
10 registerFront,
11 } = require("resource://devtools/shared/protocol.js");
12 const {
13 nodeSpec,
14 nodeListSpec,
15 } = require("resource://devtools/shared/specs/node.js");
16 const {
17 SimpleStringFront,
18 } = require("resource://devtools/client/fronts/string.js");
20 loader.lazyRequireGetter(
21 this,
22 "nodeConstants",
23 "resource://devtools/shared/dom-node-constants.js"
26 const { XPCOMUtils } = ChromeUtils.importESModule(
27 "resource://gre/modules/XPCOMUtils.sys.mjs"
30 XPCOMUtils.defineLazyPreferenceGetter(
31 this,
32 "browserToolboxScope",
33 "devtools.browsertoolbox.scope"
36 const BROWSER_TOOLBOX_SCOPE_EVERYTHING = "everything";
38 const HIDDEN_CLASS = "__fx-devtools-hide-shortcut__";
40 /**
41 * Client side of a node list as returned by querySelectorAll()
43 class NodeListFront extends FrontClassWithSpec(nodeListSpec) {
44 marshallPool() {
45 return this.getParent();
48 // Update the object given a form representation off the wire.
49 form(json) {
50 this.length = json.length;
53 item(index) {
54 return super.item(index).then(response => {
55 return response.node;
56 });
59 items(start, end) {
60 return super.items(start, end).then(response => {
61 return response.nodes;
62 });
66 exports.NodeListFront = NodeListFront;
67 registerFront(NodeListFront);
69 /**
70 * Convenience API for building a list of attribute modifications
71 * for the `modifyAttributes` request.
73 class AttributeModificationList {
74 constructor(node) {
75 this.node = node;
76 this.modifications = [];
79 apply() {
80 const ret = this.node.modifyAttributes(this.modifications);
81 return ret;
84 destroy() {
85 this.node = null;
86 this.modification = null;
89 setAttributeNS(ns, name, value) {
90 this.modifications.push({
91 attributeNamespace: ns,
92 attributeName: name,
93 newValue: value,
94 });
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);
127 // The parent node
128 this._parent = null;
129 // The first child of this node.
130 this._child = null;
131 // The next sibling of this node.
132 this._next = null;
133 // The previous sibling of this node.
134 this._prev = null;
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.
144 destroy() {
145 super.destroy();
148 // Update the object given a form representation off the wire.
149 form(form, ctx) {
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() : [];
164 if (form.parent) {
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
167 // necessary.
168 const owner = ctx.marshallPool();
169 if (typeof owner.ensureDOMNodeFront === "function") {
170 const parentNodeFront = owner.ensureDOMNodeFront(form.parent);
171 this.reparent(parentNodeFront);
175 if (form.host) {
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
184 .getType("domnode")
185 .read(form.inlineTextChild, ctx);
186 } else {
187 this.inlineTextChild = undefined;
192 * Returns the parent NodeFront for this NodeFront.
194 parentNode() {
195 return this._parent;
199 * Returns the NodeFront corresponding to the parentNode of this NodeFront, or the
200 * NodeFront corresponding to the host element for shadowRoot elements.
202 parentOrHost() {
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) {
215 if (
216 currentNode.isShadowRoot ||
217 currentNode.nodeType === Node.DOCUMENT_NODE
219 return currentNode;
222 currentNode = currentNode.parentNode();
225 return null;
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.
240 let found = false;
241 for (let i = 0; i < this.attributes.length; i++) {
242 const attr = this.attributes[i];
243 if (
244 attr.name == change.attributeName &&
245 attr.namespace == change.attributeNamespace
247 if (change.newValue !== null) {
248 attr.value = change.newValue;
249 } else {
250 this.attributes.splice(i, 1);
252 found = true;
253 break;
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
278 get id() {
279 return this.getAttribute("id");
282 get nodeType() {
283 return this._form.nodeType;
285 get namespaceURI() {
286 return this._form.namespaceURI;
288 get nodeName() {
289 return this._form.nodeName;
291 get displayName() {
292 const { displayName, nodeName } = this._form;
294 // Keep `nodeName.toLowerCase()` for backward compatibility
295 return displayName || nodeName.toLowerCase();
297 get doctypeString() {
298 return (
299 "<!DOCTYPE " +
300 this._form.name +
301 (this._form.publicId ? ' PUBLIC "' + this._form.publicId + '"' : "") +
302 (this._form.systemId ? ' "' + this._form.systemId + '"' : "") +
307 get baseURI() {
308 return this._form.baseURI;
311 get browsingContextID() {
312 return this._form.browsingContextID;
315 get className() {
316 return this.getAttribute("class") || "";
319 // Check if the node has children but the current DevTools session is unable
320 // to retrieve them.
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() {
325 return (
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
338 get hasChildren() {
339 return this.numChildren > 0;
341 get numChildren() {
342 if (this.childrenUnavailable) {
343 return 0;
346 return this._form.numChildren;
348 get useChildTargetToFetchChildren() {
349 if (
350 this._hasParentProcessTarget &&
351 browserToolboxScope != BROWSER_TOOLBOX_SCOPE_EVERYTHING
353 return false;
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() {
372 return (
373 this.isBeforePseudoElement ||
374 this.isAfterPseudoElement ||
375 this.isMarkerPseudoElement
378 get isAnonymous() {
379 return this._form.isAnonymous;
381 get isInHTMLDocument() {
382 return this._form.isInHTMLDocument;
384 get tagName() {
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;
396 get isShadowRoot() {
397 return this._form.isShadowRoot;
400 get shadowRootMode() {
401 return this._form.shadowRootMode;
404 get isShadowHost() {
405 return this._form.isShadowHost;
408 get customElementLocation() {
409 return this._form.customElementLocation;
412 get isDirectShadowHostChild() {
413 return this._form.isDirectShadowHostChild;
416 // doctype properties
417 get name() {
418 return this._form.name;
420 get publicId() {
421 return this._form.publicId;
423 get systemId() {
424 return this._form.systemId;
427 getAttribute(name) {
428 const attr = this._getAttribute(name);
429 return attr ? attr.value : null;
431 hasAttribute(name) {
432 this._cacheAttributes();
433 return name in this._attrMap;
436 get hidden() {
437 const cls = this.getAttribute("class");
438 return cls && cls.indexOf(HIDDEN_CLASS) > -1;
441 get attributes() {
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);
456 get displayType() {
457 return this._form.displayType;
460 get isDisplayed() {
461 return this._form.isDisplayed;
464 get isScrollable() {
465 return this._form.isScrollable;
468 get causesOverflow() {
469 return this._form.causesOverflow;
472 get containerType() {
473 return this._form.containerType;
476 get isTreeDisplayed() {
477 let parent = this;
478 while (parent) {
479 if (!parent.isDisplayed) {
480 return false;
482 parent = parent.parentNode();
484 return true;
487 get inspectorFront() {
488 return this.parentFront.parentFront;
491 get walkerFront() {
492 return this.parentFront;
495 getNodeValue() {
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);
513 _cacheAttributes() {
514 if (typeof this._attrMap != "undefined") {
515 return;
517 this._attrMap = {};
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.
533 reparent(parent) {
534 if (this._parent === parent) {
535 return;
538 if (this._parent && this._parent._child === this) {
539 this._parent._child = this._next;
541 if (this._prev) {
542 this._prev._next = this._next;
544 if (this._next) {
545 this._next._prev = this._prev;
547 this._next = null;
548 this._prev = null;
549 this._parent = parent;
550 if (!parent) {
551 // Subtree is disconnected, we're done
552 return;
554 this._next = parent._child;
555 if (this._next) {
556 this._next._prev = this;
558 parent._child = this;
562 * Return all the known children of this node.
564 treeChildren() {
565 const ret = [];
566 for (let child = this._child; child != null; child = child._next) {
567 ret.push(child);
569 return ret;
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.
588 rawNode() {
589 if (!this.isLocalToBeDeprecated()) {
590 console.warn("Tried to use rawNode on a remote connection.");
591 return null;
593 const {
594 DevToolsServer,
595 } = require("resource://devtools/server/devtools-server.js");
596 const actor = DevToolsServer.searchAllConnectionsForActor(this.actorID);
597 if (!actor) {
598 // Can happen if we try to get the raw node for an already-expired
599 // actor.
600 return null;
602 return actor.rawNode;
605 async connectToFrame() {
606 if (!this.useChildTargetToFetchChildren) {
607 console.warn("Tried to open connection to an invalid frame.");
608 return null;
610 if (
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
625 // markup-view.
626 this._childBrowsingContextTarget.setParentNodeFront(this);
628 return this._childBrowsingContextTarget;
632 exports.NodeFront = NodeFront;
633 registerFront(NodeFront);