Bug 1053898 - Display slotted nodes in markup view;r=gl
authorJulian Descottes <jdescottes@mozilla.com>
Tue, 06 Mar 2018 20:50:13 +0100
changeset 410643 b7f16d0eee43e2a35e110cd5a596cc30898b2187
parent 410642 0365dca9be7c759701827032c44470f391ea68a4
child 410644 5d2c3fe3111237716313367a8c147295d24922c1
push id33733
push useraciure@mozilla.com
push dateThu, 29 Mar 2018 22:05:29 +0000
treeherdermozilla-central@7ca58ce09779 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersgl
bugs1053898
milestone61.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
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;