diff options
Diffstat (limited to 'devtools/client/inspector/shared/dom-node-preview.js')
-rw-r--r-- | devtools/client/inspector/shared/dom-node-preview.js | 352 |
1 files changed, 352 insertions, 0 deletions
diff --git a/devtools/client/inspector/shared/dom-node-preview.js b/devtools/client/inspector/shared/dom-node-preview.js new file mode 100644 index 000000000..d96384785 --- /dev/null +++ b/devtools/client/inspector/shared/dom-node-preview.js @@ -0,0 +1,352 @@ +/* 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 {Task} = require("devtools/shared/task"); +const EventEmitter = require("devtools/shared/event-emitter"); +const {createNode} = require("devtools/client/animationinspector/utils"); +const { LocalizationHelper } = require("devtools/shared/l10n"); + +const STRINGS_URI = "devtools/client/locales/inspector.properties"; +const L10N = new LocalizationHelper(STRINGS_URI); + +/** + * UI component responsible for displaying a preview of a dom node. + * @param {InspectorPanel} inspector Requires a reference to the inspector-panel + * to highlight and select the node, as well as refresh it when there are + * mutations. + * @param {Object} options Supported properties are: + * - compact {Boolean} Defaults to false. + * By default, nodes are previewed like <tag id="id" class="class"> + * If true, nodes will be previewed like tag#id.class instead. + */ +function DomNodePreview(inspector, options = {}) { + this.inspector = inspector; + this.options = options; + + this.onPreviewMouseOver = this.onPreviewMouseOver.bind(this); + this.onPreviewMouseOut = this.onPreviewMouseOut.bind(this); + this.onSelectElClick = this.onSelectElClick.bind(this); + this.onMarkupMutations = this.onMarkupMutations.bind(this); + this.onHighlightElClick = this.onHighlightElClick.bind(this); + this.onHighlighterLocked = this.onHighlighterLocked.bind(this); + + EventEmitter.decorate(this); +} + +exports.DomNodePreview = DomNodePreview; + +DomNodePreview.prototype = { + init: function (containerEl) { + let document = containerEl.ownerDocument; + + // Init the markup for displaying the target node. + this.el = createNode({ + parent: containerEl, + attributes: { + "class": "animation-target" + } + }); + + // Icon to select the node in the inspector. + this.highlightNodeEl = createNode({ + parent: this.el, + nodeType: "span", + attributes: { + "class": "node-highlighter", + "title": L10N.getStr("inspector.nodePreview.highlightNodeLabel") + } + }); + + // Wrapper used for mouseover/out event handling. + this.previewEl = createNode({ + parent: this.el, + nodeType: "span", + attributes: { + "title": L10N.getStr("inspector.nodePreview.selectNodeLabel") + } + }); + + if (!this.options.compact) { + this.previewEl.appendChild(document.createTextNode("<")); + } + + // Only used for ::before and ::after pseudo-elements. + this.pseudoEl = createNode({ + parent: this.previewEl, + nodeType: "span", + attributes: { + "class": "pseudo-element theme-fg-color5" + } + }); + + // Tag name. + this.tagNameEl = createNode({ + parent: this.previewEl, + nodeType: "span", + attributes: { + "class": "tag-name theme-fg-color3" + } + }); + + // Id attribute container. + this.idEl = createNode({ + parent: this.previewEl, + nodeType: "span" + }); + + if (!this.options.compact) { + createNode({ + parent: this.idEl, + nodeType: "span", + attributes: { + "class": "attribute-name theme-fg-color2" + }, + textContent: "id" + }); + this.idEl.appendChild(document.createTextNode("=\"")); + } else { + createNode({ + parent: this.idEl, + nodeType: "span", + attributes: { + "class": "theme-fg-color6" + }, + textContent: "#" + }); + } + + createNode({ + parent: this.idEl, + nodeType: "span", + attributes: { + "class": "attribute-value theme-fg-color6" + } + }); + + if (!this.options.compact) { + this.idEl.appendChild(document.createTextNode("\"")); + } + + // Class attribute container. + this.classEl = createNode({ + parent: this.previewEl, + nodeType: "span" + }); + + if (!this.options.compact) { + createNode({ + parent: this.classEl, + nodeType: "span", + attributes: { + "class": "attribute-name theme-fg-color2" + }, + textContent: "class" + }); + this.classEl.appendChild(document.createTextNode("=\"")); + } else { + createNode({ + parent: this.classEl, + nodeType: "span", + attributes: { + "class": "theme-fg-color6" + }, + textContent: "." + }); + } + + createNode({ + parent: this.classEl, + nodeType: "span", + attributes: { + "class": "attribute-value theme-fg-color6" + } + }); + + if (!this.options.compact) { + this.classEl.appendChild(document.createTextNode("\"")); + this.previewEl.appendChild(document.createTextNode(">")); + } + + this.startListeners(); + }, + + startListeners: function () { + // Init events for highlighting and selecting the node. + this.previewEl.addEventListener("mouseover", this.onPreviewMouseOver); + this.previewEl.addEventListener("mouseout", this.onPreviewMouseOut); + this.previewEl.addEventListener("click", this.onSelectElClick); + this.highlightNodeEl.addEventListener("click", this.onHighlightElClick); + + // Start to listen for markupmutation events. + this.inspector.on("markupmutation", this.onMarkupMutations); + + // Listen to the target node highlighter. + HighlighterLock.on("highlighted", this.onHighlighterLocked); + }, + + stopListeners: function () { + HighlighterLock.off("highlighted", this.onHighlighterLocked); + this.inspector.off("markupmutation", this.onMarkupMutations); + this.previewEl.removeEventListener("mouseover", this.onPreviewMouseOver); + this.previewEl.removeEventListener("mouseout", this.onPreviewMouseOut); + this.previewEl.removeEventListener("click", this.onSelectElClick); + this.highlightNodeEl.removeEventListener("click", this.onHighlightElClick); + }, + + destroy: function () { + HighlighterLock.unhighlight().catch(e => console.error(e)); + + this.stopListeners(); + + this.el.remove(); + this.el = this.tagNameEl = this.idEl = this.classEl = this.pseudoEl = null; + this.highlightNodeEl = this.previewEl = null; + this.nodeFront = this.inspector = null; + }, + + get highlighterUtils() { + if (this.inspector && this.inspector.toolbox) { + return this.inspector.toolbox.highlighterUtils; + } + return null; + }, + + onPreviewMouseOver: function () { + if (!this.nodeFront || !this.highlighterUtils) { + return; + } + this.highlighterUtils.highlightNodeFront(this.nodeFront) + .catch(e => console.error(e)); + }, + + onPreviewMouseOut: function () { + if (!this.nodeFront || !this.highlighterUtils) { + return; + } + this.highlighterUtils.unhighlight() + .catch(e => console.error(e)); + }, + + onSelectElClick: function () { + if (!this.nodeFront) { + return; + } + this.inspector.selection.setNodeFront(this.nodeFront, "dom-node-preview"); + }, + + onHighlightElClick: function (e) { + e.stopPropagation(); + + let classList = this.highlightNodeEl.classList; + let isHighlighted = classList.contains("selected"); + + if (isHighlighted) { + classList.remove("selected"); + HighlighterLock.unhighlight().then(() => { + this.emit("target-highlighter-unlocked"); + }, error => console.error(error)); + } else { + classList.add("selected"); + HighlighterLock.highlight(this).then(() => { + this.emit("target-highlighter-locked"); + }, error => console.error(error)); + } + }, + + onHighlighterLocked: function (e, domNodePreview) { + if (domNodePreview !== this) { + this.highlightNodeEl.classList.remove("selected"); + } + }, + + onMarkupMutations: function (e, mutations) { + if (!this.nodeFront) { + return; + } + + for (let {target} of mutations) { + if (target === this.nodeFront) { + // Re-render with the same nodeFront to update the output. + this.render(this.nodeFront); + break; + } + } + }, + + render: function (nodeFront) { + this.nodeFront = nodeFront; + let {displayName, attributes} = nodeFront; + + if (nodeFront.isPseudoElement) { + this.pseudoEl.textContent = nodeFront.isBeforePseudoElement + ? "::before" + : "::after"; + this.pseudoEl.style.display = "inline"; + this.tagNameEl.style.display = "none"; + } else { + this.tagNameEl.textContent = displayName; + this.pseudoEl.style.display = "none"; + this.tagNameEl.style.display = "inline"; + } + + let idIndex = attributes.findIndex(({name}) => name === "id"); + if (idIndex > -1 && attributes[idIndex].value) { + this.idEl.querySelector(".attribute-value").textContent = + attributes[idIndex].value; + this.idEl.style.display = "inline"; + } else { + this.idEl.style.display = "none"; + } + + let classIndex = attributes.findIndex(({name}) => name === "class"); + if (classIndex > -1 && attributes[classIndex].value) { + let value = attributes[classIndex].value; + if (this.options.compact) { + value = value.split(" ").join("."); + } + + this.classEl.querySelector(".attribute-value").textContent = value; + this.classEl.style.display = "inline"; + } else { + this.classEl.style.display = "none"; + } + } +}; + +/** + * HighlighterLock is a helper used to lock the highlighter on DOM nodes in the + * page. + * It instantiates a new highlighter that is then shared amongst all instances + * of DomNodePreview. This is useful because that means showing the highlighter + * on one node will unhighlight the previously highlighted one, but will not + * interfere with the default inspector highlighter. + */ +var HighlighterLock = { + highlighter: null, + isShown: false, + + highlight: Task.async(function* (animationTargetNode) { + if (!this.highlighter) { + let util = animationTargetNode.inspector.toolbox.highlighterUtils; + this.highlighter = yield util.getHighlighterByType("BoxModelHighlighter"); + } + + yield this.highlighter.show(animationTargetNode.nodeFront); + this.isShown = true; + this.emit("highlighted", animationTargetNode); + }), + + unhighlight: Task.async(function* () { + if (!this.highlighter || !this.isShown) { + return; + } + + yield this.highlighter.hide(); + this.isShown = false; + this.emit("unhighlighted"); + }) +}; + +EventEmitter.decorate(HighlighterLock); |