diff options
Diffstat (limited to 'devtools/client/inspector/shared/highlighters-overlay.js')
-rw-r--r-- | devtools/client/inspector/shared/highlighters-overlay.js | 315 |
1 files changed, 315 insertions, 0 deletions
diff --git a/devtools/client/inspector/shared/highlighters-overlay.js b/devtools/client/inspector/shared/highlighters-overlay.js new file mode 100644 index 000000000..c054c72af --- /dev/null +++ b/devtools/client/inspector/shared/highlighters-overlay.js @@ -0,0 +1,315 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* 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"; + +/** + * The highlighter overlays are in-content highlighters that appear when hovering over + * property values. + */ + +const promise = require("promise"); +const EventEmitter = require("devtools/shared/event-emitter"); +const { VIEW_NODE_VALUE_TYPE } = require("devtools/client/inspector/shared/node-types"); + +/** + * Manages all highlighters in the style-inspector. + * + * @param {CssRuleView|CssComputedView} view + * Either the rule-view or computed-view panel + */ +function HighlightersOverlay(view) { + this.view = view; + + let {CssRuleView} = require("devtools/client/inspector/rules/rules"); + this.isRuleView = view instanceof CssRuleView; + + this.highlighters = {}; + + // NodeFront of the grid container that is highlighted. + this.gridHighlighterShown = null; + // Name of the highlighter shown on mouse hover. + this.hoveredHighlighterShown = null; + // Name of the selector highlighter shown. + this.selectorHighlighterShown = null; + + this.highlighterUtils = this.view.inspector.toolbox.highlighterUtils; + + // Only initialize the overlay if at least one of the highlighter types is + // supported. + this.supportsHighlighters = + this.highlighterUtils.supportsCustomHighlighters(); + + this._onClick = this._onClick.bind(this); + this._onMouseMove = this._onMouseMove.bind(this); + this._onMouseOut = this._onMouseOut.bind(this); + this._onWillNavigate = this._onWillNavigate.bind(this); + + EventEmitter.decorate(this); +} + +HighlightersOverlay.prototype = { + /** + * Add the highlighters overlay to the view. This will start tracking mouse + * movements and display highlighters when needed. + */ + addToView: function () { + if (!this.supportsHighlighters || this._isStarted || this._isDestroyed) { + return; + } + + let el = this.view.element; + el.addEventListener("click", this._onClick, true); + el.addEventListener("mousemove", this._onMouseMove, false); + el.addEventListener("mouseout", this._onMouseOut, false); + el.ownerDocument.defaultView.addEventListener("mouseout", this._onMouseOut, false); + + if (this.isRuleView) { + this.view.inspector.target.on("will-navigate", this._onWillNavigate); + } + + this._isStarted = true; + }, + + /** + * Remove the overlay from the current view. This will stop tracking mouse + * movement and showing highlighters. + */ + removeFromView: function () { + if (!this.supportsHighlighters || !this._isStarted || this._isDestroyed) { + return; + } + + let el = this.view.element; + el.removeEventListener("click", this._onClick, true); + el.removeEventListener("mousemove", this._onMouseMove, false); + el.removeEventListener("mouseout", this._onMouseOut, false); + + if (this.isRuleView) { + this.view.inspector.target.off("will-navigate", this._onWillNavigate); + } + + this._isStarted = false; + }, + + _onClick: function (event) { + // Bail out if the target is not a grid property value. + if (!this._isDisplayGridValue(event.target)) { + return; + } + + event.stopPropagation(); + + this._getHighlighter("CssGridHighlighter").then(highlighter => { + let node = this.view.inspector.selection.nodeFront; + + // Toggle off the grid highlighter if the grid highlighter toggle is clicked + // for the current highlighted grid. + if (node === this.gridHighlighterShown) { + return highlighter.hide(); + } + + return highlighter.show(node); + }).then(isGridShown => { + // Toggle all the grid icons in the current rule view. + for (let gridIcon of this.view.element.querySelectorAll(".ruleview-grid")) { + gridIcon.classList.toggle("active", isGridShown); + } + + if (isGridShown) { + this.gridHighlighterShown = this.view.inspector.selection.nodeFront; + this.emit("highlighter-shown"); + } else { + this.gridHighlighterShown = null; + this.emit("highlighter-hidden"); + } + }).catch(e => console.error(e)); + }, + + _onMouseMove: function (event) { + // Bail out if the target is the same as for the last mousemove. + if (event.target === this._lastHovered) { + return; + } + + // Only one highlighter can be displayed at a time, hide the currently shown. + this._hideHoveredHighlighter(); + + this._lastHovered = event.target; + + let nodeInfo = this.view.getNodeInfo(event.target); + if (!nodeInfo) { + return; + } + + // Choose the type of highlighter required for the hovered node. + let type; + if (this._isRuleViewTransform(nodeInfo) || + this._isComputedViewTransform(nodeInfo)) { + type = "CssTransformHighlighter"; + } + + if (type) { + this.hoveredHighlighterShown = type; + let node = this.view.inspector.selection.nodeFront; + this._getHighlighter(type) + .then(highlighter => highlighter.show(node)) + .then(shown => { + if (shown) { + this.emit("highlighter-shown"); + } + }); + } + }, + + _onMouseOut: function (event) { + // Only hide the highlighter if the mouse leaves the currently hovered node. + if (!this._lastHovered || + (event && this._lastHovered.contains(event.relatedTarget))) { + return; + } + + // Otherwise, hide the highlighter. + this._lastHovered = null; + this._hideHoveredHighlighter(); + }, + + /** + * Clear saved highlighter shown properties on will-navigate. + */ + _onWillNavigate: function () { + this.gridHighlighterShown = null; + this.hoveredHighlighterShown = null; + this.selectorHighlighterShown = null; + }, + + /** + * Is the current hovered node a css transform property value in the rule-view. + * + * @param {Object} nodeInfo + * @return {Boolean} + */ + _isRuleViewTransform: function (nodeInfo) { + let isTransform = nodeInfo.type === VIEW_NODE_VALUE_TYPE && + nodeInfo.value.property === "transform"; + let isEnabled = nodeInfo.value.enabled && + !nodeInfo.value.overridden && + !nodeInfo.value.pseudoElement; + return this.isRuleView && isTransform && isEnabled; + }, + + /** + * Is the current hovered node a css transform property value in the + * computed-view. + * + * @param {Object} nodeInfo + * @return {Boolean} + */ + _isComputedViewTransform: function (nodeInfo) { + let isTransform = nodeInfo.type === VIEW_NODE_VALUE_TYPE && + nodeInfo.value.property === "transform"; + return !this.isRuleView && isTransform; + }, + + /** + * Is the current clicked node a grid display property value in the + * rule-view. + * + * @param {DOMNode} node + * @return {Boolean} + */ + _isDisplayGridValue: function (node) { + return this.isRuleView && node.classList.contains("ruleview-grid"); + }, + + /** + * Hide the currently shown grid highlighter. + */ + _hideGridHighlighter: function () { + if (!this.gridHighlighterShown || !this.highlighters.CssGridHighlighter) { + return; + } + + let onHidden = this.highlighters.CssGridHighlighter.hide(); + if (onHidden) { + onHidden.then(null, e => console.error(e)); + } + + this.gridHighlighterShown = null; + this.emit("highlighter-hidden"); + }, + + /** + * Hide the currently shown hovered highlighter. + */ + _hideHoveredHighlighter: function () { + if (!this.hoveredHighlighterShown || + !this.highlighters[this.hoveredHighlighterShown]) { + return; + } + + // For some reason, the call to highlighter.hide doesn't always return a + // promise. This causes some tests to fail when trying to install a + // rejection handler on the result of the call. To avoid this, check + // whether the result is truthy before installing the handler. + let onHidden = this.highlighters[this.hoveredHighlighterShown].hide(); + if (onHidden) { + onHidden.then(null, e => console.error(e)); + } + + this.hoveredHighlighterShown = null; + this.emit("highlighter-hidden"); + }, + + /** + * Get a highlighter front given a type. It will only be initialized once. + * + * @param {String} type + * The highlighter type. One of this.highlighters. + * @return {Promise} that resolves to the highlighter + */ + _getHighlighter: function (type) { + let utils = this.highlighterUtils; + + if (this.highlighters[type]) { + return promise.resolve(this.highlighters[type]); + } + + return utils.getHighlighterByType(type).then(highlighter => { + this.highlighters[type] = highlighter; + return highlighter; + }); + }, + + /** + * Destroy this overlay instance, removing it from the view and destroying + * all initialized highlighters. + */ + destroy: function () { + this.removeFromView(); + + for (let type in this.highlighters) { + if (this.highlighters[type]) { + this.highlighters[type].finalize(); + this.highlighters[type] = null; + } + } + + this.highlighters = null; + + this.gridHighlighterShown = null; + this.hoveredHighlighterShown = null; + this.selectorHighlighterShown = null; + + this.highlighterUtils = null; + this.isRuleView = null; + this.view = null; + + this._isDestroyed = true; + } +}; + +module.exports = HighlightersOverlay; |