Bug 1053898 - Display slotted nodes in markup view;r=gl
Add new container and editor dedicated to represent slotted nodes.
Add isSlotted to the interface of Container elements (returns false
everywhere except for slotted containers).
MozReview-Commit-ID: DRxyqThpegm
--- a/devtools/client/inspector/markup/markup.js
+++ b/devtools/client/inspector/markup/markup.js
@@ -15,16 +15,17 @@ const AutocompletePopup = require("devto
const KeyShortcuts = require("devtools/client/shared/key-shortcuts");
const {scrollIntoViewIfNeeded} = require("devtools/client/shared/scroll");
const {UndoStack} = require("devtools/client/shared/undo");
const {HTMLTooltip} = require("devtools/client/shared/widgets/tooltip/HTMLTooltip");
const {PrefObserver} = require("devtools/client/shared/prefs");
const MarkupElementContainer = require("devtools/client/inspector/markup/views/element-container");
const MarkupReadOnlyContainer = require("devtools/client/inspector/markup/views/read-only-container");
const MarkupTextContainer = require("devtools/client/inspector/markup/views/text-container");
+const SlottedNodeContainer = require("devtools/client/inspector/markup/views/slotted-node-container");
const RootContainer = require("devtools/client/inspector/markup/views/root-container");
const INSPECTOR_L10N =
new LocalizationHelper("devtools/client/locales/inspector.properties");
// Page size for pageup/pagedown
const PAGE_SIZE = 10;
const DEFAULT_MAX_CHILDREN = 100;
@@ -82,16 +83,19 @@ function MarkupView(inspector, frame, co
autoSelect: true,
theme: "auto",
});
this.undo = new UndoStack();
this.undo.installController(controllerWindow);
this._containers = new Map();
+ // This weakmap will hold keys used with the _containers map, in order to retrieve the
+ // slotted container for a given node front.
+ this._slottedContainerKeys = new WeakMap();
// Binding functions that need to be called in scope.
this._handleRejectionIfNotDestroyed = this._handleRejectionIfNotDestroyed.bind(this);
this._mutationObserver = this._mutationObserver.bind(this);
this._onDisplayChange = this._onDisplayChange.bind(this);
this._onMouseClick = this._onMouseClick.bind(this);
this._onMouseUp = this._onMouseUp.bind(this);
this._onNewSelection = this._onNewSelection.bind(this);
@@ -473,27 +477,74 @@ MarkupView.prototype = {
this._briefBoxModelPromise.resolve = _resolve;
return promise.all([onShown, this._briefBoxModelPromise]);
},
/**
* Get the MarkupContainer object for a given node, or undefined if
* none exists.
+ *
+ * @param {NodeFront} nodeFront
+ * The node to get the container for.
+ * @param {Boolean} slotted
+ * true to get the slotted version of the container.
+ * @return {MarkupContainer} The container for the provided node.
*/
- getContainer: function(node) {
- return this._containers.get(node);
+ getContainer: function(node, slotted) {
+ let key = this._getContainerKey(node, slotted);
+ return this._containers.get(key);
+ },
+
+ /**
+ * Register a given container for a given node/slotted node.
+ *
+ * @param {NodeFront} nodeFront
+ * The node to set the container for.
+ * @param {Boolean} slotted
+ * true if the container represents the slotted version of the node.
+ */
+ setContainer: function(node, container, slotted) {
+ let key = this._getContainerKey(node, slotted);
+ return this._containers.set(key, container);
},
- setContainer: function(node, container) {
- return this._containers.set(node, container);
+ /**
+ * Check if a MarkupContainer object exists for a given node/slotted node
+ *
+ * @param {NodeFront} nodeFront
+ * The node to check.
+ * @param {Boolean} slotted
+ * true to check for a container matching the slotted version of the node.
+ * @return {Boolean} True if a container exists, false otherwise.
+ */
+ hasContainer: function(node, slotted) {
+ let key = this._getContainerKey(node, slotted);
+ return this._containers.has(key);
},
- hasContainer: function(node) {
- return this._containers.has(node);
+ _getContainerKey: function(node, slotted) {
+ if (!slotted) {
+ return node;
+ }
+
+ if (!this._slottedContainerKeys.has(node)) {
+ this._slottedContainerKeys.set(node, { node });
+ }
+ return this._slottedContainerKeys.get(node);
+ },
+
+ _isContainerSelected: function(container) {
+ if (!container) {
+ return false;
+ }
+
+ let selection = this.inspector.selection;
+ return container.node == selection.nodeFront &&
+ container.isSlotted() == selection.isSlotted();
},
update: function() {
let updateChildren = (node) => {
this.getContainer(node).update();
for (let child of node.treeChildren()) {
updateChildren(child);
}
@@ -563,33 +614,32 @@ MarkupView.prototype = {
let reason = this.inspector.selection.reason;
let unwantedReasons = [
"inspector-open",
"navigateaway",
"nodeselected",
"test"
];
- let isHighlight = this._hoveredContainer &&
- (this._hoveredContainer.node === this.inspector.selection.nodeFront);
+ let isHighlight = this._isContainerSelected(this._hoveredContainer);
return !isHighlight && reason && !unwantedReasons.includes(reason);
},
/**
* React to new-node-front selection events.
* Highlights the node if needed, and make sure it is shown and selected in
* the view.
*/
_onNewSelection: function() {
let selection = this.inspector.selection;
if (this.htmlEditor) {
this.htmlEditor.hide();
}
- if (this._hoveredContainer && this._hoveredContainer.node !== selection.nodeFront) {
+ if (this._isContainerSelected(this._hoveredContainer)) {
this._hoveredContainer.hovered = false;
this._hoveredContainer = null;
}
if (!selection.isNode()) {
this.unmarkSelectedNode();
return;
}
@@ -597,24 +647,26 @@ MarkupView.prototype = {
let done = this.inspector.updating("markup-view");
let onShowBoxModel, onShow;
// Highlight the element briefly if needed.
if (this._shouldNewSelectionBeHighlighted()) {
onShowBoxModel = this._brieflyShowBoxModel(selection.nodeFront);
}
- onShow = this.showNode(selection.nodeFront).then(() => {
+ let slotted = selection.isSlotted();
+ onShow = this.showNode(selection.nodeFront, { slotted }).then(() => {
// We could be destroyed by now.
if (this._destroyer) {
return promise.reject("markupview destroyed");
}
// Mark the node as selected.
- this.markNodeAsSelected(selection.nodeFront);
+ let container = this.getContainer(selection.nodeFront, slotted);
+ this._markContainerAsSelected(container);
// Make sure the new selection is navigated to.
this.maybeNavigateToNewSelection();
return undefined;
}).catch(this._handleRejectionIfNotDestroyed);
promise.all([onShowBoxModel, onShow]).then(done);
},
@@ -960,47 +1012,51 @@ MarkupView.prototype = {
/**
* Make sure a node is included in the markup tool.
*
* @param {NodeFront} node
* The node in the content document.
* @param {Boolean} flashNode
* Whether the newly imported node should be flashed
+ * @param {Boolean} slotted
+ * Whether we are importing the slotted version of the node.
* @return {MarkupContainer} The MarkupContainer object for this element.
*/
- importNode: function(node, flashNode) {
+ importNode: function(node, flashNode, slotted) {
if (!node) {
return null;
}
- if (this.hasContainer(node)) {
- return this.getContainer(node);
+ if (this.hasContainer(node, slotted)) {
+ return this.getContainer(node, slotted);
}
let container;
let {nodeType, isPseudoElement} = node;
if (node === this.walker.rootNode) {
container = new RootContainer(this, node);
this._elt.appendChild(container.elt);
this._rootNode = node;
+ } else if (slotted) {
+ container = new SlottedNodeContainer(this, node, this.inspector);
} else if (nodeType == nodeConstants.ELEMENT_NODE && !isPseudoElement) {
container = new MarkupElementContainer(this, node, this.inspector);
} else if (nodeType == nodeConstants.COMMENT_NODE ||
nodeType == nodeConstants.TEXT_NODE) {
container = new MarkupTextContainer(this, node, this.inspector);
} else {
container = new MarkupReadOnlyContainer(this, node, this.inspector);
}
if (flashNode) {
container.flashMutation();
}
- this.setContainer(node, container);
+ this.setContainer(node, container, slotted);
container.childrenDirty = true;
this._updateChildren(container);
this.inspector.emit("container-created", container);
return container;
},
@@ -1130,34 +1186,43 @@ MarkupView.prototype = {
container.flashMutation();
}
},
/**
* Make sure the given node's parents are expanded and the
* node is scrolled on to screen.
*/
- showNode: function(node, centered = true) {
+ showNode: function(node, {centered = true, slotted} = {}) {
+ if (slotted && !this.hasContainer(node, slotted)) {
+ throw new Error("Tried to show a slotted node not previously imported");
+ } else {
+ this._ensureNodeImported(node);
+ }
+
+ return this._waitForChildren().then(() => {
+ if (this._destroyer) {
+ return promise.reject("markupview destroyed");
+ }
+ return this._ensureVisible(node);
+ }).then(() => {
+ let container = this.getContainer(node, slotted);
+ scrollIntoViewIfNeeded(container.editor.elt, centered);
+ }, this._handleRejectionIfNotDestroyed);
+ },
+
+ _ensureNodeImported: function(node) {
let parent = node;
this.importNode(node);
while ((parent = parent.parentNode())) {
this.importNode(parent);
this.expandNode(parent);
}
-
- return this._waitForChildren().then(() => {
- if (this._destroyer) {
- return promise.reject("markupview destroyed");
- }
- return this._ensureVisible(node);
- }).then(() => {
- scrollIntoViewIfNeeded(this.getContainer(node).editor.elt, centered);
- }, this._handleRejectionIfNotDestroyed);
},
/**
* Expand the container's children.
*/
_expandContainer: function(container) {
return this._updateChildren(container, {expand: true}).then(() => {
if (this._destroyer) {
@@ -1529,18 +1594,19 @@ MarkupView.prototype = {
// Select the new container.
this._selectedContainer = container;
if (node) {
this._selectedContainer.selected = true;
}
// Change the current selection if needed.
- if (this.inspector.selection.nodeFront !== node) {
- this.inspector.selection.setNodeFront(node, { reason });
+ if (!this._isContainerSelected(this._selectedContainer)) {
+ let isSlotted = container.isSlotted();
+ this.inspector.selection.setNodeFront(node, { reason, isSlotted });
}
return true;
},
/**
* Make sure that every ancestor of the selection are updated
* and included in the list of visible children.
@@ -1610,16 +1676,21 @@ MarkupView.prototype = {
* @param {MarkupContainer} container
* The markup container whose children need updating
* @param {Object} options
* Options are {expand:boolean,flash:boolean}
* @return {Promise} that will be resolved when the children are ready
* (which may be immediately).
*/
_updateChildren: function(container, options) {
+ // Slotted containers do not display any children.
+ if (container.isSlotted()) {
+ return promise.resolve(container);
+ }
+
let expand = options && options.expand;
let flash = options && options.flash;
container.hasChildren = container.node.hasChildren;
// Accessibility should either ignore empty children or semantically
// consider them a group.
container.setChildrenRole();
@@ -1699,23 +1770,18 @@ MarkupView.prototype = {
// while the request was in progress, we need to do it again.
if (container.childrenDirty) {
return this._updateChildren(container, {expand: centered || expand});
}
let fragment = this.doc.createDocumentFragment();
for (let child of children.nodes) {
- let { isDirectShadowHostChild } = child;
- if (!isShadowHost && isDirectShadowHostChild) {
- // Temporarily skip light DOM nodes if the container's node is not a host
- // element, which means that the node is a "slotted" node.
- continue;
- }
- let childContainer = this.importNode(child, flash);
+ let slotted = !isShadowHost && child.isDirectShadowHostChild;
+ let childContainer = this.importNode(child, flash, slotted);
fragment.appendChild(childContainer.elt);
}
while (container.children.firstChild) {
container.children.firstChild.remove();
}
if (!children.hasFirst) {
--- a/devtools/client/inspector/markup/views/markup-container.js
+++ b/devtools/client/inspector/markup/views/markup-container.js
@@ -436,16 +436,20 @@ MarkupContainer.prototype = {
!this.node.isAnonymous &&
!this.node.isDocumentElement &&
tagName !== "body" &&
tagName !== "head" &&
this.win.getSelection().isCollapsed &&
this.node.parentNode().tagName !== null;
},
+ isSlotted: function() {
+ return false;
+ },
+
/**
* Move keyboard focus to a next/previous focusable element inside container
* that is not part of its children (only if current focus is on first or last
* element).
*
* @param {DOMNode} current currently focused element
* @param {Boolean} back direction
* @return {DOMNode} newly focused element if any
--- a/devtools/client/inspector/markup/views/moz.build
+++ b/devtools/client/inspector/markup/views/moz.build
@@ -7,11 +7,13 @@
DevToolsModules(
'element-container.js',
'element-editor.js',
'html-editor.js',
'markup-container.js',
'read-only-container.js',
'read-only-editor.js',
'root-container.js',
+ 'slotted-node-container.js',
+ 'slotted-node-editor.js',
'text-container.js',
'text-editor.js',
)
--- a/devtools/client/inspector/markup/views/root-container.js
+++ b/devtools/client/inspector/markup/views/root-container.js
@@ -44,12 +44,16 @@ RootContainer.prototype = {
/**
* Set an appropriate role of the container's children node.
*/
setChildrenRole: function() {},
/**
* Set an appropriate DOM tree depth level for a node and its subtree.
*/
- updateLevel: function() {}
+ updateLevel: function() {},
+
+ isSlotted: function() {
+ return false;
+ }
};
module.exports = RootContainer;
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/markup/views/slotted-node-container.js
@@ -0,0 +1,30 @@
+/* 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 SlottedNodeEditor = require("devtools/client/inspector/markup/views/slotted-node-editor");
+const MarkupContainer = require("devtools/client/inspector/markup/views/markup-container");
+const { extend } = require("devtools/shared/extend");
+
+function SlottedNodeContainer(markupView, node) {
+ MarkupContainer.prototype.initialize.call(this, markupView, node,
+ "slottednodecontainer");
+
+ this.editor = new SlottedNodeEditor(this, node);
+ this.tagLine.appendChild(this.editor.elt);
+ this.hasChildren = false;
+}
+
+SlottedNodeContainer.prototype = extend(MarkupContainer.prototype, {
+ isDraggable: function() {
+ return false;
+ },
+
+ isSlotted: function() {
+ return true;
+ }
+});
+
+module.exports = SlottedNodeContainer;
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/markup/views/slotted-node-editor.js
@@ -0,0 +1,48 @@
+/* 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";
+
+function SlottedNodeEditor(container, node) {
+ this.container = container;
+ this.markup = this.container.markup;
+ this.buildMarkup();
+ this.tag.textContent = "<" + node.nodeName.toLowerCase() + ">";
+
+ // Make the "tag" part of this editor focusable.
+ this.tag.setAttribute("tabindex", "-1");
+}
+
+SlottedNodeEditor.prototype = {
+ buildMarkup: function() {
+ let doc = this.markup.doc;
+
+ this.elt = doc.createElement("span");
+ this.elt.classList.add("editor");
+
+ this.tag = doc.createElement("span");
+ this.tag.classList.add("tag");
+ this.elt.appendChild(this.tag);
+ },
+
+ destroy: function() {
+ // We might be already destroyed.
+ if (!this.elt) {
+ return;
+ }
+
+ this.elt.remove();
+ this.elt = null;
+ this.tag = null;
+ },
+
+ /**
+ * Stub method for consistency with ElementEditor.
+ */
+ getInfoAtNode: function() {
+ return null;
+ }
+};
+
+module.exports = SlottedNodeEditor;