Bug 1443923 - part6: Update markup view when a custom element is defined;r=bgrins
MozReview-Commit-ID: BIDonbaoewh
--- 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);
}