Bug 1465873 - part6: Allow selection and breadcrumbs to walk from shadowRoot to host element;r=bgrins draft
authorJulian Descottes <jdescottes@mozilla.com>
Thu, 28 Jun 2018 13:11:08 +0200
changeset 814187 33f1f6e8dd221754a4a8fb32f954e5d277110917
parent 814186 fcdb2d81471e05d8335968ddd81867929798ccd7
child 814211 a3212e0d4522a697450894f32fa345664bd7c17b
push id115123
push userjdescottes@mozilla.com
push dateWed, 04 Jul 2018 17:42:29 +0000
reviewersbgrins
bugs1465873
milestone63.0a1
Bug 1465873 - part6: Allow selection and breadcrumbs to walk from shadowRoot to host element;r=bgrins By returning the shadow root as the parentNode of some elements, the breadcrumbs could no longer display the chain of elements correctly, because shadowRoot.parentNode is null. This changeset: - returns the host actor ID as part of the shadowRoot form - adds a parentOrHost convenience method on the node form - uses said method in selection and breadcrumbs when walking up the ancestor chain I don't think we should unconditionally return the host element as the parentNode of the shadow root, because that is too disconnected from the reality. MozReview-Commit-ID: JLeDb4VuT1q
devtools/client/framework/selection.js
devtools/client/inspector/breadcrumbs.js
devtools/server/actors/inspector/node.js
devtools/shared/fronts/inspector.js
devtools/shared/fronts/node.js
--- a/devtools/client/framework/selection.js
+++ b/devtools/client/framework/selection.js
@@ -168,17 +168,17 @@ Selection.prototype = {
     if (!node || !node.actorID) {
       return false;
     }
 
     while (node) {
       if (node === this._walker.rootNode) {
         return true;
       }
-      node = node.parentNode();
+      node = node.parentOrHost();
     }
     return false;
   },
 
   isHTMLNode: function() {
     const xhtmlNs = "http://www.w3.org/1999/xhtml";
     return this.isNode() && this.nodeFront.namespaceURI == xhtmlNs;
   },
--- a/devtools/client/inspector/breadcrumbs.js
+++ b/devtools/client/inspector/breadcrumbs.js
@@ -734,17 +734,17 @@ HTMLBreadcrumbs.prototype = {
         fragment.insertBefore(button, lastButtonInserted);
         lastButtonInserted = button;
         this.nodeHierarchy.splice(originalLength, 0, {
           node,
           button,
           currentPrettyPrintText: this.prettyPrintNodeAsText(node)
         });
       }
-      node = node.parentNode();
+      node = node.parentOrHost();
     }
     this.container.appendChild(fragment, this.container.firstChild);
   },
 
   /**
    * Find the "youngest" ancestor of a node which is already in the breadcrumbs.
    * @param {NodeFront} node.
    * @return {Number} Index of the ancestor in the cache, or -1 if not found.
--- a/devtools/server/actors/inspector/node.js
+++ b/devtools/server/actors/inspector/node.js
@@ -97,19 +97,22 @@ const NodeActor = protocol.ActorClassWit
   // Returns the JSON representation of this object over the wire.
   form: function(detail) {
     if (detail === "actorid") {
       return this.actorID;
     }
 
     const parentNode = this.walker.parentNode(this);
     const inlineTextChild = this.walker.inlineTextChild(this);
+    const shadowRoot = isShadowRoot(this.rawNode);
+    const hostActor = shadowRoot ? this.walker.getNode(this.rawNode.host) : null;
 
     const form = {
       actor: this.actorID,
+      host: hostActor ? hostActor.actorID : undefined,
       baseURI: this.rawNode.baseURI,
       parent: parentNode ? parentNode.actorID : undefined,
       nodeType: this.rawNode.nodeType,
       namespaceURI: this.rawNode.namespaceURI,
       nodeName: this.rawNode.nodeName,
       nodeValue: this.rawNode.nodeValue,
       displayName: InspectorActorUtils.getNodeDisplayName(this.rawNode),
       numChildren: this.numChildren,
@@ -123,17 +126,17 @@ const NodeActor = protocol.ActorClassWit
 
       attrs: this.writeAttrs(),
       isBeforePseudoElement: isBeforePseudoElement(this.rawNode),
       isAfterPseudoElement: isAfterPseudoElement(this.rawNode),
       isAnonymous: isAnonymous(this.rawNode),
       isNativeAnonymous: isNativeAnonymous(this.rawNode),
       isXBLAnonymous: isXBLAnonymous(this.rawNode),
       isShadowAnonymous: isShadowAnonymous(this.rawNode),
-      isShadowRoot: isShadowRoot(this.rawNode),
+      isShadowRoot: shadowRoot,
       isShadowHost: isShadowHost(this.rawNode),
       isDirectShadowHostChild: isDirectShadowHostChild(this.rawNode),
       pseudoClassLocks: this.writePseudoClassLocks(),
 
       isDisplayed: this.isDisplayed,
       isInHTMLDocument: this.rawNode.ownerDocument &&
         this.rawNode.ownerDocument.contentType === "text/html",
       hasEventListeners: this._hasEventListeners,
--- a/devtools/shared/fronts/inspector.js
+++ b/devtools/shared/fronts/inspector.js
@@ -75,23 +75,23 @@ const WalkerFront = FrontClassWithSpec(w
     this._rootNodeDeferred = defer();
     this._rootNodeDeferred.promise.then(() => {
       this.emit("new-root");
     });
   },
 
   /**
    * When reading an actor form off the wire, we want to hook it up to its
-   * parent front.  The protocol guarantees that the parent will be seen
-   * by the client in either a previous or the current request.
+   * parent or host front.  The protocol guarantees that the parent will
+   * be seen by the client in either a previous or the current request.
    * So if we've already seen this parent return it, otherwise create
    * a bare-bones stand-in node.  The stand-in node will be updated
    * with a real form by the end of the deserialization.
    */
-  ensureParentFront: function(id) {
+  ensureDOMNodeFront: function(id) {
     const front = this.get(id);
     if (front) {
       return front;
     }
 
     return types.getType("domnode").read({ actor: id }, this, "standin");
   },
 
--- a/devtools/shared/fronts/node.js
+++ b/devtools/shared/fronts/node.js
@@ -158,36 +158,48 @@ const NodeFront = FrontClassWithSpec(nod
     // 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 parentNodeFront = ctx.marshallPool().ensureParentFront(form.parent);
+      const parentNodeFront = ctx.marshallPool().ensureDOMNodeFront(form.parent);
       this.reparent(parentNodeFront);
     }
 
+    if (form.host) {
+      this.host = ctx.marshallPool().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: function() {
     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: function() {
+    return this.isShadowRoot ? this.host : this._parent;
+  },
+
+  /**
    * 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: function(change) {
     if (change.type === "attributes") {
       // We'll need to lazily reparse the attributes after this change.