summaryrefslogtreecommitdiffstats
path: root/devtools/client/inspector/shared/dom-node-preview.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/inspector/shared/dom-node-preview.js')
-rw-r--r--devtools/client/inspector/shared/dom-node-preview.js352
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);