Bug 1443923 - part6: Update markup view when a custom element is defined;r=bgrins draft
authorJulian Descottes <jdescottes@mozilla.com>
Sat, 07 Jul 2018 09:57:13 +0200
changeset 822572 db9b069684939667ac9c78b5ef24b7ef445c7a82
parent 822571 a1ea7c9a777550a731466c8eb203f11b16dda981
child 822573 e83a66b6de449539eb4bb8243202b4c53698abed
push id117402
push userjdescottes@mozilla.com
push dateWed, 25 Jul 2018 13:32:04 +0000
reviewersbgrins
bugs1443923
milestone63.0a1
Bug 1443923 - part6: Update markup view when a custom element is defined;r=bgrins MozReview-Commit-ID: BIDonbaoewh
devtools/client/inspector/markup/markup.js
devtools/server/actors/inspector/custom-element-watcher.js
devtools/server/actors/inspector/moz.build
devtools/server/actors/inspector/walker.js
devtools/shared/fronts/inspector.js
--- a/devtools/client/inspector/markup/markup.js
+++ b/devtools/client/inspector/markup/markup.js
@@ -1085,21 +1085,30 @@ MarkupView.prototype = {
 
       const container = this.getContainer(target);
       if (!container) {
         // Container might not exist if this came from a load event for a node
         // we're not viewing.
         continue;
       }
 
-      if (type === "attributes" || type === "characterData"
-        || type === "events" || type === "pseudoClassLock") {
+      if (
+        type === "attributes" ||
+        type === "characterData" ||
+        type === "customElementDefined" ||
+        type === "events" ||
+        type === "pseudoClassLock"
+      ) {
         container.update();
-      } else if (type === "childList" || type === "nativeAnonymousChildList"
-        || type === "slotchange" || type === "shadowRootAttached") {
+      } else if (
+        type === "childList" ||
+        type === "nativeAnonymousChildList" ||
+        type === "slotchange" ||
+        type === "shadowRootAttached"
+      ) {
         container.childrenDirty = true;
         // Update the children to take care of changes in the markup view DOM
         // and update container (and its subtree) DOM tree depth level for
         // accessibility where necessary.
         this._updateChildren(container, {flash: true}).then(() =>
           container.updateLevel());
       } else if (type === "inlineTextChild") {
         container.childrenDirty = true;
new file mode 100644
--- /dev/null
+++ b/devtools/server/actors/inspector/custom-element-watcher.js
@@ -0,0 +1,132 @@
+/* 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 {Cu} = require("chrome");
+const InspectorUtils = require("InspectorUtils");
+const EventEmitter = require("devtools/shared/event-emitter");
+
+/**
+ * The CustomElementWatcher can be used to be notified if a custom element definition
+ * is created for a node.
+ *
+ * When a custom element is defined for a monitored name, an "element-defined" event is
+ * fired with the Set of impacted node actors as argument.
+ */
+class CustomElementWatcher extends EventEmitter {
+  constructor(chromeEventHandler) {
+    super();
+
+    this.chromeEventHandler = chromeEventHandler;
+    this._onCustomElementDefined = this._onCustomElementDefined.bind(this);
+    this.chromeEventHandler.addEventListener("customelementdefined",
+      this._onCustomElementDefined);
+
+    /**
+     * Each window keeps its own custom element registry, all of them are watched
+     * separately. The struture of the watchedRegistries is as follows
+     *
+     * WeakMap(
+     *   registry -> Map (
+     *     name -> Set(NodeActors)
+     *   )
+     * )
+     */
+    this.watchedRegistries = new WeakMap();
+  }
+
+  destroy() {
+    this.watchedRegistries = null;
+    this.chromeEventHandler.removeEventListener("customelementdefined",
+      this._onCustomElementDefined);
+  }
+
+  /**
+   * Watch for custom element definitions matching the name of the provided NodeActor.
+   */
+  manageNode(nodeActor) {
+    if (!this._isValidNode(nodeActor)) {
+      return;
+    }
+
+    if (!this._shouldWatchDefinition(nodeActor)) {
+      return;
+    }
+
+    const registry = nodeActor.rawNode.ownerGlobal.customElements;
+    const registryMap = this._getMapForRegistry(registry);
+
+    const name = nodeActor.rawNode.localName;
+    if (!registryMap.has(name)) {
+      // Create a new entry in the Map for this name.
+      registryMap.set(name, new Set());
+    }
+    registryMap.get(name).add(nodeActor);
+  }
+
+  /**
+   * Stop watching the provided NodeActor.
+   */
+  unmanageNode(nodeActor) {
+    if (!this._isValidNode(nodeActor)) {
+      return;
+    }
+
+    const win = nodeActor.rawNode.ownerGlobal;
+    const registry = win.customElements;
+    const registryMap = this._getMapForRegistry(registry);
+    const name = nodeActor.rawNode.localName;
+    if (registryMap && registryMap.has(name)) {
+      registryMap.get(name).delete(nodeActor);
+    }
+  }
+
+  /**
+   * Retrieve the map of name->nodeActors for a given CustomElementsRegistry.
+   * Will create the map if not created yet.
+   */
+  _getMapForRegistry(registry) {
+    if (!this.watchedRegistries.has(registry)) {
+      this.watchedRegistries.set(registry, new Map());
+    }
+    return this.watchedRegistries.get(registry);
+  }
+
+  _shouldWatchDefinition(nodeActor) {
+    const doc = nodeActor.rawNode.ownerDocument;
+    const namespaceURI = doc.documentElement.namespaceURI;
+    const name = nodeActor.rawNode.localName;
+    const isValidName = InspectorUtils.isCustomElementName(name, namespaceURI);
+
+    const customElements = doc.defaultView.customElements;
+    return isValidName && !customElements.get(name);
+  }
+
+  _onCustomElementDefined(event) {
+    const doc = event.target;
+    const registry = doc.defaultView.customElements;
+    const registryMap = this.watchedRegistries.get(registry);
+
+    const name = event.detail;
+    const nodeActors = registryMap.get(name);
+
+    this.emit("element-defined", nodeActors);
+    registryMap.delete(name);
+  }
+
+  /**
+   * Some nodes (e.g. inside of <template> tags) don't have a documentElement or an
+   * ownerGlobal and can't be watched by this helper.
+   */
+  _isValidNode(nodeActor) {
+    const node = nodeActor.rawNode;
+    return !Cu.isDeadWrapper(node) &&
+           node.ownerGlobal &&
+           node.ownerDocument &&
+           node.ownerDocument.documentElement;
+  }
+}
+
+exports.CustomElementWatcher = CustomElementWatcher;
--- a/devtools/server/actors/inspector/moz.build
+++ b/devtools/server/actors/inspector/moz.build
@@ -1,16 +1,17 @@
 # -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # 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/.
 
 DevToolsModules(
   'css-logic.js',
+  'custom-element-watcher.js',
   'document-walker.js',
   'event-parsers.js',
   'inspector.js',
   'node.js',
   'utils.js',
   'walker.js',
 )
 
--- a/devtools/server/actors/inspector/walker.js
+++ b/devtools/server/actors/inspector/walker.js
@@ -24,16 +24,17 @@ loader.lazyRequireGetter(this, "loadShee
 
 loader.lazyRequireGetter(this, "throttle", "devtools/shared/throttle", true);
 
 loader.lazyRequireGetter(this, "allAnonymousContentTreeWalkerFilter", "devtools/server/actors/inspector/utils", true);
 loader.lazyRequireGetter(this, "isNodeDead", "devtools/server/actors/inspector/utils", true);
 loader.lazyRequireGetter(this, "nodeDocument", "devtools/server/actors/inspector/utils", true);
 loader.lazyRequireGetter(this, "standardTreeWalkerFilter", "devtools/server/actors/inspector/utils", true);
 
+loader.lazyRequireGetter(this, "CustomElementWatcher", "devtools/server/actors/inspector/custom-element-watcher", true);
 loader.lazyRequireGetter(this, "DocumentWalker", "devtools/server/actors/inspector/document-walker", true);
 loader.lazyRequireGetter(this, "SKIP_TO_SIBLING", "devtools/server/actors/inspector/document-walker", true);
 loader.lazyRequireGetter(this, "NodeActor", "devtools/server/actors/inspector/node", true);
 loader.lazyRequireGetter(this, "NodeListActor", "devtools/server/actors/inspector/node", true);
 loader.lazyRequireGetter(this, "LayoutActor", "devtools/server/actors/layout", true);
 loader.lazyRequireGetter(this, "getLayoutChangesObserver", "devtools/server/actors/reflow", true);
 loader.lazyRequireGetter(this, "releaseLayoutChangesObserver", "devtools/server/actors/reflow", true);
 loader.lazyRequireGetter(this, "WalkerSearch", "devtools/server/actors/utils/walker-search", true);
@@ -123,16 +124,18 @@ var WalkerActor = protocol.ActorClassWit
   initialize: function(conn, targetActor, options) {
     protocol.Actor.prototype.initialize.call(this, conn);
     this.targetActor = targetActor;
     this.rootWin = targetActor.window;
     this.rootDoc = this.rootWin.document;
     this._refMap = new Map();
     this._pendingMutations = [];
     this._activePseudoClassLocks = new Set();
+    this.customElementWatcher = new CustomElementWatcher(targetActor.chromeEventHandler);
+
     this.showAllAnonymousContent = options.showAllAnonymousContent;
 
     this.walkerSearch = new WalkerSearch(this);
 
     // Nodes which have been removed from the client's known
     // ownership tree are considered "orphaned", and stored in
     // this set.
     this._orphaned = new Set();
@@ -142,22 +145,25 @@ var WalkerActor = protocol.ActorClassWit
     // list contains orphaned nodes that were so retained.
     this._retainedOrphans = new Set();
 
     this.onMutations = this.onMutations.bind(this);
     this.onSlotchange = this.onSlotchange.bind(this);
     this.onShadowrootattached = this.onShadowrootattached.bind(this);
     this.onFrameLoad = this.onFrameLoad.bind(this);
     this.onFrameUnload = this.onFrameUnload.bind(this);
+    this.onCustomElementDefined = this.onCustomElementDefined.bind(this);
     this._throttledEmitNewMutations = throttle(this._emitNewMutations.bind(this),
       MUTATIONS_THROTTLING_DELAY);
 
     targetActor.on("will-navigate", this.onFrameUnload);
     targetActor.on("window-ready", this.onFrameLoad);
 
+    this.customElementWatcher.on("element-defined", this.onCustomElementDefined);
+
     // Keep a reference to the chromeEventHandler for the current targetActor, to make
     // sure we will be able to remove the listener during the WalkerActor destroy().
     this.chromeEventHandler = targetActor.chromeEventHandler;
     // shadowrootattached is a chrome-only event.
     this.chromeEventHandler.addEventListener("shadowrootattached",
       this.onShadowrootattached);
 
     // Ensure that the root document node actor is ready and
@@ -243,22 +249,27 @@ var WalkerActor = protocol.ActorClassWit
       this.rootNode = null;
       this.layoutHelpers = null;
       this._orphaned = null;
       this._retainedOrphans = null;
       this._refMap = null;
 
       this.targetActor.off("will-navigate", this.onFrameUnload);
       this.targetActor.off("window-ready", this.onFrameLoad);
+      this.customElementWatcher.off("element-defined", this.onCustomElementDefined);
+
       this.chromeEventHandler.removeEventListener("shadowrootattached",
         this.onShadowrootattached);
 
       this.onFrameLoad = null;
       this.onFrameUnload = null;
 
+      this.customElementWatcher.destroy();
+      this.customElementWatcher = null;
+
       this.walkerSearch.destroy();
 
       this.layoutChangeObserver.off("reflows", this._onReflows);
       this.layoutChangeObserver.off("resize", this._onResize);
       this.layoutChangeObserver = null;
       releaseLayoutChangesObserver(this.targetActor);
 
       eventListenerService.removeListenerChangeListener(
@@ -279,16 +290,19 @@ var WalkerActor = protocol.ActorClassWit
   release: function() {},
 
   unmanage: function(actor) {
     if (actor instanceof NodeActor) {
       if (this._activePseudoClassLocks &&
           this._activePseudoClassLocks.has(actor)) {
         this.clearPseudoClassLocks(actor);
       }
+
+      this.customElementWatcher.unmanageNode(actor);
+
       this._refMap.delete(actor.rawNode);
     }
     protocol.Actor.prototype.unmanage.call(this, actor);
   },
 
   /**
    * Determine if the walker has come across this DOM node before.
    * @param {DOMNode} rawNode
@@ -325,19 +339,33 @@ var WalkerActor = protocol.ActorClassWit
       actor.watchDocument(node, this.onMutations);
     }
 
     if (isShadowRoot(actor.rawNode)) {
       actor.watchDocument(node.ownerDocument, this.onMutations);
       actor.watchSlotchange(this.onSlotchange);
     }
 
+    this.customElementWatcher.manageNode(actor);
+
     return actor;
   },
 
+  /**
+   * When a custom element is defined for one of the names currently watched, send a
+   * customElementDefined mutation for all the NodeActors using this tag name.
+   */
+  onCustomElementDefined: function(actors) {
+    actors.forEach(actor => this.queueMutation({
+      target: actor.actorID,
+      type: "customElementDefined",
+      customElementLocation: actor.getCustomElementLocation(),
+    }));
+  },
+
   _onReflows: function(reflows) {
     // Going through the nodes the walker knows about, see which ones have
     // had their display changed and send a display-change event if any
     const changes = [];
     for (const [node, actor] of this._refMap) {
       if (Cu.isDeadWrapper(node)) {
         continue;
       }
--- a/devtools/shared/fronts/inspector.js
+++ b/devtools/shared/fronts/inspector.js
@@ -378,16 +378,18 @@ const WalkerFront = FrontClassWithSpec(w
           // to be destroyed now.
           emittedMutation.target = targetFront.actorID;
           emittedMutation.targetParent = targetFront.parentNode();
 
           // Release the document node and all of its children, even retained.
           this._releaseFront(targetFront, true);
         } else if (change.type === "shadowRootAttached") {
           targetFront._form.isShadowHost = true;
+        } else if (change.type === "customElementDefined") {
+          targetFront._form.customElementLocation = change.customElementLocation;
         } else if (change.type === "unretained") {
           // Retained orphans were force-released without the intervention of
           // client (probably a navigated frame).
           for (const released of change.nodes) {
             const releasedFront = this.get(released);
             this._retainedOrphans.delete(released);
             this._releaseFront(releasedFront, true);
           }