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