Mypal/devtools/client/inspector/shared/dom-node-preview.js
2019-03-11 13:26:37 +03:00

353 lines
10 KiB
JavaScript

/* 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);