353 lines
10 KiB
JavaScript
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);
|