Bug 1053898 - Display slotted nodes in markup view;r=gl draft
authorJulian Descottes <jdescottes@mozilla.com>
Tue, 06 Mar 2018 20:50:13 +0100
changeset 773677 e01c7931e66fcecbeb14f8de9901add062df6b98
parent 773676 b5ee2de3c22c0d6511c48ab5e4ceb64c6bd3143d
child 773678 426fa586e9fa6212c7f9283946d3242b2296543f
push id104269
push userjdescottes@mozilla.com
push dateWed, 28 Mar 2018 08:16:27 +0000
reviewersgl
bugs1053898
milestone61.0a1
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
devtools/client/inspector/markup/markup.js
devtools/client/inspector/markup/views/markup-container.js
devtools/client/inspector/markup/views/moz.build
devtools/client/inspector/markup/views/root-container.js
devtools/client/inspector/markup/views/slotted-node-container.js
devtools/client/inspector/markup/views/slotted-node-editor.js
--- 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;