--- a/devtools/client/inspector/rules/rules.js
+++ b/devtools/client/inspector/rules/rules.js
@@ -21,16 +21,17 @@ const ClassListPreviewer = require("devt
const {gDevTools} = require("devtools/client/framework/devtools");
const {getCssProperties} = require("devtools/shared/fronts/css-properties");
const {
VIEW_NODE_SELECTOR_TYPE,
VIEW_NODE_PROPERTY_TYPE,
VIEW_NODE_VALUE_TYPE,
VIEW_NODE_IMAGE_URL_TYPE,
VIEW_NODE_LOCATION_TYPE,
+ VIEW_NODE_SHAPE_POINT_TYPE,
} = require("devtools/client/inspector/shared/node-types");
const StyleInspectorMenu = require("devtools/client/inspector/shared/style-inspector-menu");
const TooltipsOverlay = require("devtools/client/inspector/shared/tooltips-overlay");
const {createChild, promiseWarn, debounce} = require("devtools/client/inspector/shared/utils");
const EventEmitter = require("devtools/shared/event-emitter");
const KeyShortcuts = require("devtools/client/shared/key-shortcuts");
const clipboardHelper = require("devtools/shared/platform/clipboard");
const AutocompletePopup = require("devtools/client/shared/autocomplete-popup");
@@ -43,16 +44,17 @@ const PREF_ENABLE_MDN_DOCS_TOOLTIP =
const FILTER_CHANGED_TIMEOUT = 150;
const PREF_ORIG_SOURCES = "devtools.styleeditor.source-maps-enabled";
// This is used to parse user input when filtering.
const FILTER_PROP_RE = /\s*([^:\s]*)\s*:\s*(.*?)\s*;?$/;
// This is used to parse the filter search value to see if the filter
// should be strict or not
const FILTER_STRICT_RE = /\s*`(.*?)`\s*$/;
+const INSET_POINT_TYPES = ["top", "right", "bottom", "left"];
/**
* Our model looks like this:
*
* ElementStyle:
* Responsible for keeping track of which properties are overridden.
* Maintains a list of Rule objects that apply to the element.
* Rule:
@@ -328,16 +330,29 @@ CssRuleView.prototype = {
property: getPropertyNameAndValue(node).name,
value: node.textContent,
enabled: prop.enabled,
overridden: prop.overridden,
pseudoElement: prop.rule.pseudoElement,
sheetHref: prop.rule.domRule.href,
textProperty: prop
};
+ } else if (classes.contains("ruleview-shape-point") && prop) {
+ type = VIEW_NODE_SHAPE_POINT_TYPE;
+ value = {
+ property: getPropertyNameAndValue(node).name,
+ value: node.textContent,
+ enabled: prop.enabled,
+ overridden: prop.overridden,
+ pseudoElement: prop.rule.pseudoElement,
+ sheetHref: prop.rule.domRule.href,
+ textProperty: prop,
+ toggleActive: getShapeToggleActive(node),
+ point: getShapePoint(node)
+ };
} else if (classes.contains("theme-link") &&
!classes.contains("ruleview-rule-source") && prop) {
type = VIEW_NODE_IMAGE_URL_TYPE;
value = {
property: getPropertyNameAndValue(node).name,
value: node.parentNode.textContent,
url: node.href,
enabled: prop.enabled,
@@ -1534,16 +1549,62 @@ function getPropertyNameAndValue(node) {
name: node.querySelector(".ruleview-propertyname").textContent,
value: node.querySelector(".ruleview-propertyvalue").textContent
};
}
node = node.parentNode;
}
}
+/**
+ * Walk up the DOM from a given node until a parent property holder is found,
+ * and return an active shape toggle if one exists.
+ *
+ * @param {DOMNode} node
+ * The node to start from
+ * @returns {DOMNode} The active shape toggle node, if one exists.
+ */
+function getShapeToggleActive(node) {
+ while (true) {
+ if (!node || !node.classList) {
+ return null;
+ }
+ // Check first for ruleview-computed since it's the deepest
+ if (node.classList.contains("ruleview-computed") ||
+ node.classList.contains("ruleview-property")) {
+ return node.querySelector(".ruleview-shape.active");
+ }
+ node = node.parentNode;
+ }
+}
+
+/**
+ * Get the point associated with a shape point node.
+ *
+ * @param {DOMNode} node
+ * A shape point node
+ * @returns {String} The point associated with the given node.
+ */
+function getShapePoint(node) {
+ let classList = node.classList;
+ let point = node.dataset.point;
+ // Inset points use classes instead of data because a single span can represent
+ // multiple points.
+ let insetClasses = [];
+ classList.forEach(className => {
+ if (INSET_POINT_TYPES.includes(className)) {
+ insetClasses.push(className);
+ }
+ });
+ if (insetClasses.length > 0) {
+ point = insetClasses.join(",");
+ }
+ return point;
+}
+
function RuleViewTool(inspector, window) {
this.inspector = inspector;
this.document = window.document;
this.view = new CssRuleView(this.inspector, this.document);
this.clearUserProperties = this.clearUserProperties.bind(this);
this.refresh = this.refresh.bind(this);
--- a/devtools/client/inspector/rules/views/text-property-editor.js
+++ b/devtools/client/inspector/rules/views/text-property-editor.js
@@ -22,16 +22,17 @@ const Services = require("Services");
const HTML_NS = "http://www.w3.org/1999/xhtml";
const SHARED_SWATCH_CLASS = "ruleview-swatch";
const COLOR_SWATCH_CLASS = "ruleview-colorswatch";
const BEZIER_SWATCH_CLASS = "ruleview-bezierswatch";
const FILTER_SWATCH_CLASS = "ruleview-filterswatch";
const ANGLE_SWATCH_CLASS = "ruleview-angleswatch";
+const INSET_POINT_TYPES = ["top", "right", "bottom", "left"];
/*
* An actionable element is an element which on click triggers a specific action
* (e.g. shows a color tooltip, opens a link, …).
*/
const ACTIONABLE_ELEMENTS_SELECTORS = [
`.${COLOR_SWATCH_CLASS}`,
`.${BEZIER_SWATCH_CLASS}`,
@@ -73,16 +74,17 @@ function TextPropertyEditor(ruleEditor,
this._onNameDone = this._onNameDone.bind(this);
this._onValueDone = this._onValueDone.bind(this);
this._onSwatchCommit = this._onSwatchCommit.bind(this);
this._onSwatchPreview = this._onSwatchPreview.bind(this);
this._onSwatchRevert = this._onSwatchRevert.bind(this);
this._onValidate = this.ruleView.debounce(this._previewValue, 10, this);
this.update = this.update.bind(this);
this.updatePropertyState = this.updatePropertyState.bind(this);
+ this._onHoverShapePoint = this._onHoverShapePoint.bind(this);
this._create();
this.update();
}
TextPropertyEditor.prototype = {
/**
* Boolean indicating if the name or value is being currently edited.
@@ -295,16 +297,18 @@ TextPropertyEditor.prototype = {
contentType: InplaceEditor.CONTENT_TYPES.CSS_VALUE,
property: this.prop,
popup: this.popup,
multiline: true,
maxWidth: () => this.container.getBoundingClientRect().width,
cssProperties: this.cssProperties,
contextMenu: this.ruleView.inspector.onTextBoxContextMenu
});
+
+ this.ruleView.highlighters.on("hover-shape-point", this._onHoverShapePoint);
}
},
/**
* Get the path from which to resolve requests for this
* rule's stylesheet.
*
* @return {String} the stylesheet's href.
@@ -449,16 +453,17 @@ TextPropertyEditor.prototype = {
return s[0].toUpperCase() + s.slice(1);
}).join("");
shapeToggle.setAttribute("data-mode", mode);
let { highlighters, inspector } = this.ruleView;
if (highlighters.shapesHighlighterShown === inspector.selection.nodeFront &&
highlighters.state.shapes.options.mode === mode) {
shapeToggle.classList.add("active");
+ highlighters.highlightRuleViewShapePoint(highlighters.state.shapes.hoverPoint);
}
}
// Now that we have updated the property's value, we might have a pending
// click on the value container. If we do, we have to trigger a click event
// on the right element.
if (this._hasPendingClick) {
this._hasPendingClick = false;
@@ -939,12 +944,73 @@ TextPropertyEditor.prototype = {
* Returns true if the property is a `display: [inline-]grid` declaration.
*
* @return {Boolean} true if the property is a `display: [inline-]grid` declaration.
*/
isDisplayGrid: function () {
return this.prop.name === "display" &&
(this.prop.value === "grid" ||
this.prop.value === "inline-grid");
- }
+ },
+
+ /**
+ * Highlight the given shape point in the rule view. Called when "hover-shape-point"
+ * event is emitted.
+ *
+ * @param {Event} event
+ * The "hover-shape-point" event.
+ * @param {String} point
+ * The point to highlight.
+ */
+ _onHoverShapePoint: function (event, point) {
+ // If there is no shape toggle, or it is not active, return.
+ let shapeToggle = this.valueSpan.querySelector(".ruleview-shape.active");
+ if (!shapeToggle) {
+ return;
+ }
+
+ let view = this.ruleView;
+ let { highlighters } = view;
+ let ruleViewEl = view.element;
+ let selector = `.ruleview-shape-point.active`;
+ for (let pointNode of ruleViewEl.querySelectorAll(selector)) {
+ this._toggleShapePointActive(pointNode, false);
+ }
+
+ if (typeof point === "string") {
+ if (point.includes(",")) {
+ point = point.split(",")[0];
+ }
+ // Because one inset value can represent multiple points, inset points use classes
+ // instead of data.
+ selector = (INSET_POINT_TYPES.includes(point)) ?
+ `.ruleview-shape-point.${point}` :
+ `.ruleview-shape-point[data-point='${point}']`;
+ for (let pointNode of this.valueSpan.querySelectorAll(selector)) {
+ let nodeInfo = view.getNodeInfo(pointNode);
+ if (highlighters.isRuleViewShapePoint(nodeInfo)) {
+ this._toggleShapePointActive(pointNode, true);
+ }
+ }
+ }
+ },
+
+ /**
+ * Toggle the class "active" on the given shape point in the rule view if the current
+ * inspector selection is highlighted by the shapes highlighter.
+ *
+ * @param {NodeFront} node
+ * The NodeFront of the shape point to toggle
+ * @param {Boolean} active
+ * Whether the shape point should be active
+ */
+ _toggleShapePointActive: function (node, active) {
+ let { highlighters } = this.ruleView;
+ if (highlighters.inspector.selection.nodeFront !=
+ highlighters.shapesHighlighterShown) {
+ return;
+ }
+
+ node.classList.toggle("active", active);
+ },
};
exports.TextPropertyEditor = TextPropertyEditor;
--- a/devtools/client/inspector/shared/highlighters-overlay.js
+++ b/devtools/client/inspector/shared/highlighters-overlay.js
@@ -4,19 +4,23 @@
* 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 Services = require("Services");
const {Task} = require("devtools/shared/task");
const EventEmitter = require("devtools/shared/event-emitter");
-const { VIEW_NODE_VALUE_TYPE } = require("devtools/client/inspector/shared/node-types");
+const {
+ VIEW_NODE_VALUE_TYPE,
+ VIEW_NODE_SHAPE_POINT_TYPE
+} = require("devtools/client/inspector/shared/node-types");
const DEFAULT_GRID_COLOR = "#4B0082";
+const INSET_POINT_TYPES = ["top", "right", "bottom", "left"];
/**
* Highlighters overlay is a singleton managing all highlighters in the Inspector.
*
* @param {Inspector} inspector
* Inspector toolbox panel.
*/
function HighlightersOverlay(inspector) {
@@ -47,16 +51,17 @@ function HighlightersOverlay(inspector)
this.onMarkupMutation = this.onMarkupMutation.bind(this);
this.onMouseMove = this.onMouseMove.bind(this);
this.onMouseOut = this.onMouseOut.bind(this);
this.onWillNavigate = this.onWillNavigate.bind(this);
this.onNavigate = this.onNavigate.bind(this);
this.showGridHighlighter = this.showGridHighlighter.bind(this);
this.showShapesHighlighter = this.showShapesHighlighter.bind(this);
this._handleRejection = this._handleRejection.bind(this);
+ this._onHighlighterEvent = this._onHighlighterEvent.bind(this);
// Add inspector events, not specific to a given view.
this.inspector.on("markupmutation", this.onMarkupMutation);
this.inspector.target.on("navigate", this.onNavigate);
this.inspector.target.on("will-navigate", this.onWillNavigate);
EventEmitter.decorate(this);
}
@@ -176,16 +181,61 @@ HighlightersOverlay.prototype = {
yield this.highlighters.ShapesHighlighter.hide();
this.emit("shapes-highlighter-hidden", this.shapesHighlighterShown,
this.state.shapes.options);
this.shapesHighlighterShown = null;
this.state.shapes = {};
}),
/**
+ * Show the shapes highlighter for the given element, with the given point highlighted.
+ *
+ * @param {NodeFront} node
+ * The NodeFront of the element to highlight.
+ * @param {String} point
+ * The point to highlight in the shapes highlighter.
+ */
+ hoverPointShapesHighlighter: Task.async(function* (node, point) {
+ if (node == this.shapesHighlighterShown) {
+ let options = Object.assign({}, this.state.shapes.options);
+ options.hoverPoint = point;
+ yield this.showShapesHighlighter(node, options);
+ }
+ }),
+
+ /**
+ * Highlight the given shape point in the rule view.
+ *
+ * @param {String} point
+ * The point to highlight.
+ */
+ highlightRuleViewShapePoint: function (point) {
+ let view = this.inspector.getPanel("ruleview").view;
+ let ruleViewEl = view.element;
+ let selector = `.ruleview-shape-point.active`;
+ for (let pointNode of ruleViewEl.querySelectorAll(selector)) {
+ this._toggleShapePointActive(pointNode, false);
+ }
+
+ if (point !== null && point !== undefined) {
+ // Because one inset value can represent multiple points, inset points use classes
+ // instead of data.
+ selector = (INSET_POINT_TYPES.includes(point)) ?
+ `.ruleview-shape-point.${point}` :
+ `.ruleview-shape-point[data-point='${point}']`;
+ for (let pointNode of ruleViewEl.querySelectorAll(selector)) {
+ let nodeInfo = view.getNodeInfo(pointNode);
+ if (this.isRuleViewShapePoint(nodeInfo)) {
+ this._toggleShapePointActive(pointNode, true);
+ }
+ }
+ }
+ },
+
+ /**
* Toggle the grid highlighter for the given grid container element.
*
* @param {NodeFront} node
* The NodeFront of the grid container element to highlight.
* @param {Object} options
* Object used for passing options to the grid highlighter.
* @param. {String|null} trigger
* String name matching "grid" or "rule" to indicate where the
@@ -315,45 +365,62 @@ HighlightersOverlay.prototype = {
yield this.highlighters.GeometryEditorHighlighter.hide();
this.emit("geometry-editor-highlighter-hidden");
this.geometryEditorHighlighterShown = null;
}),
/**
+ * Handle events emitted by the highlighter.
+ *
+ * @param {Object} data
+ * The data object sent in the event.
+ */
+ _onHighlighterEvent: function (data) {
+ if (data.type === "shape-hover-on") {
+ this.state.shapes.hoverPoint = data.point;
+ this.emit("hover-shape-point", data.point);
+ } else if (data.type === "shape-hover-off") {
+ this.state.shapes.hoverPoint = null;
+ this.emit("hover-shape-point", null);
+ }
+ this.emit("highlighter-event-handled");
+ },
+
+ /**
* Restore the saved highlighter states.
* @param {String} name
* The name of the highlighter to be restored
- * @param {String} selector
- * The selector of the node that was previously highlighted
- * @param {Object} options
- * The options previously supplied to the highlighter
- * @param {String} url
- * The URL of the page the highlighter was active on
+ * @param {Object} state
+ * The state of the highlighter to be restored
* @param {Function} showFunction
* The function that shows the highlighter
* @return {Promise} that resolves when the highlighter state was restored, and the
* expected highlighters are displayed.
*/
- restoreState: Task.async(function* (name, {selector, options, url}, showFunction) {
+ restoreState: Task.async(function* (name, state, showFunction) {
+ let { selector, options, url } = state;
if (!selector || url !== this.inspector.target.url) {
// Bail out if no selector was saved, or if we are on a different page.
this.emit(`${name}-state-restored`, { restored: false });
return;
}
// Wait for the new root to be ready in the inspector.
yield this.onInspectorNewRoot;
let walker = this.inspector.walker;
let rootNode = yield walker.getRootNode();
let nodeFront = yield walker.querySelector(rootNode, selector);
if (nodeFront) {
+ if (options.hoverPoint) {
+ options.hoverPoint = null;
+ }
yield showFunction(nodeFront, options);
this.emit(`${name}-state-restored`, { restored: true });
}
this.emit(`${name}-state-restored`, { restored: false });
}),
/**
@@ -377,16 +444,17 @@ HighlightersOverlay.prototype = {
} catch (e) {
// Ignore any error
}
if (!highlighter) {
return null;
}
+ highlighter.on("highlighter-event", this._onHighlighterEvent);
this.highlighters[type] = highlighter;
return highlighter;
}),
_handleRejection: function (error) {
if (!this.destroyed) {
console.error(error);
}
@@ -411,16 +479,33 @@ HighlightersOverlay.prototype = {
let ruleViewEl = this.inspector.getPanel("ruleview").view.element;
for (let icon of ruleViewEl.querySelectorAll(selector)) {
icon.classList.toggle("active", active);
}
},
/**
+ * Toggle the class "active" on the given shape point in the rule view if the current
+ * inspector selection is highlighted by the shapes highlighter.
+ *
+ * @param {NodeFront} node
+ * The NodeFront of the shape point to toggle
+ * @param {Boolean} active
+ * Whether the shape point should be active
+ */
+ _toggleShapePointActive: function (node, active) {
+ if (this.inspector.selection.nodeFront != this.shapesHighlighterShown) {
+ return;
+ }
+
+ node.classList.toggle("active", active);
+ },
+
+ /**
* Hide the currently shown hovered highlighter.
*/
_hideHoveredHighlighter: function () {
if (!this.hoveredHighlighterShown ||
!this.highlighters[this.hoveredHighlighterShown]) {
return;
}
@@ -482,16 +567,32 @@ HighlightersOverlay.prototype = {
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 highlightable shape point in the rule-view.
+ *
+ * @param {Object} nodeInfo
+ * @return {Boolean}
+ */
+ isRuleViewShapePoint: function (nodeInfo) {
+ let isShape = nodeInfo.type === VIEW_NODE_SHAPE_POINT_TYPE &&
+ (nodeInfo.value.property === "clip-path" ||
+ nodeInfo.value.property === "shape-outside");
+ let isEnabled = nodeInfo.value.enabled &&
+ !nodeInfo.value.overridden &&
+ !nodeInfo.value.pseudoElement;
+ return this.isRuleView && isShape && isEnabled && nodeInfo.value.toggleActive;
+ },
+
onClick: function (event) {
if (this._isRuleViewDisplayGrid(event.target)) {
event.stopPropagation();
let { store } = this.inspector;
let { grids, highlighterSettings } = store.getState();
let grid = grids.find(g => g.nodeFront == this.inspector.selection.nodeFront);
@@ -521,16 +622,23 @@ HighlightersOverlay.prototype = {
let view = this.isRuleView ?
this.inspector.getPanel("ruleview").view :
this.inspector.getPanel("computedview").computedView;
let nodeInfo = view.getNodeInfo(event.target);
if (!nodeInfo) {
return;
}
+ if (this.isRuleViewShapePoint(nodeInfo)) {
+ let { point } = nodeInfo.value;
+ this.hoverPointShapesHighlighter(this.inspector.selection.nodeFront, point);
+ this.emit("hover-shape-point", point);
+ return;
+ }
+
// Choose the type of highlighter required for the hovered node.
let type;
if (this._isRuleViewTransform(nodeInfo) ||
this._isComputedViewTransform(nodeInfo)) {
type = "CssTransformHighlighter";
}
if (type) {
@@ -549,16 +657,24 @@ HighlightersOverlay.prototype = {
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.
+ let view = this.isRuleView ?
+ this.inspector.getPanel("ruleview").view :
+ this.inspector.getPanel("computedview").computedView;
+ let nodeInfo = view.getNodeInfo(this._lastHovered);
+ if (nodeInfo && this.isRuleViewShapePoint(nodeInfo)) {
+ this.hoverPointShapesHighlighter(this.inspector.selection.nodeFront, null);
+ this.emit("hover-shape-point", null);
+ }
this._lastHovered = null;
this._hideHoveredHighlighter();
},
/**
* Handler function for "markupmutation" events. Hides the grid/shapes highlighter
* if the grid/shapes container is no longer in the DOM tree.
*/
@@ -625,16 +741,19 @@ HighlightersOverlay.prototype = {
/**
* Destroy this overlay instance, removing it from the view and destroying
* all initialized highlighters.
*/
destroy: function () {
for (let type in this.highlighters) {
if (this.highlighters[type]) {
+ if (this.highlighters[type].off) {
+ this.highlighters[type].off("highlighter-event", this._onHighlighterEvent);
+ }
this.highlighters[type].finalize();
this.highlighters[type] = null;
}
}
// Remove inspector events.
this.inspector.off("markupmutation", this.onMarkupMutation);
this.inspector.target.off("navigate", this.onNavigate);
--- a/devtools/client/inspector/shared/node-types.js
+++ b/devtools/client/inspector/shared/node-types.js
@@ -10,8 +10,9 @@
* Types of nodes used in the rule and omputed view.
*/
exports.VIEW_NODE_SELECTOR_TYPE = 1;
exports.VIEW_NODE_PROPERTY_TYPE = 2;
exports.VIEW_NODE_VALUE_TYPE = 3;
exports.VIEW_NODE_IMAGE_URL_TYPE = 4;
exports.VIEW_NODE_LOCATION_TYPE = 5;
+exports.VIEW_NODE_SHAPE_POINT_TYPE = 6;
--- a/devtools/client/inspector/test/browser.ini
+++ b/devtools/client/inspector/test/browser.ini
@@ -74,16 +74,17 @@ skip-if = os == "mac" # Full keyboard na
[browser_inspector_highlighter-cancel.js]
[browser_inspector_highlighter-comments.js]
[browser_inspector_highlighter-cssgrid_01.js]
[browser_inspector_highlighter-cssgrid_02.js]
[browser_inspector_highlighter-cssshape_01.js]
[browser_inspector_highlighter-cssshape_02.js]
[browser_inspector_highlighter-cssshape_03.js]
[browser_inspector_highlighter-cssshape_04.js]
+[browser_inspector_highlighter-cssshape_05.js]
[browser_inspector_highlighter-csstransform_01.js]
[browser_inspector_highlighter-csstransform_02.js]
[browser_inspector_highlighter-embed.js]
[browser_inspector_highlighter-eyedropper-clipboard.js]
subsuite = clipboard
skip-if = (os == 'linux' && bits == 32 && debug) # bug 1328915, disable linux32 debug devtools for timeouts
[browser_inspector_highlighter-eyedropper-csp.js]
[browser_inspector_highlighter-eyedropper-events.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_highlighter-cssshape_05.js
@@ -0,0 +1,110 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test hovering over shape points in the rule-view and shapes highlighter.
+
+const TEST_URL = URL_ROOT + "doc_inspector_highlighter_cssshapes.html";
+
+const HIGHLIGHTER_TYPE = "ShapesHighlighter";
+const CSS_SHAPES_ENABLED_PREF = "devtools.inspector.shapesHighlighter.enabled";
+
+add_task(function* () {
+ yield pushPref(CSS_SHAPES_ENABLED_PREF, true);
+ let env = yield openInspectorForURL(TEST_URL);
+ let helper = yield getHighlighterHelperFor(HIGHLIGHTER_TYPE)(env);
+ let { testActor, inspector } = env;
+ let view = selectRuleView(inspector);
+ let highlighters = view.highlighters;
+
+ yield highlightFromRuleView(inspector, view, highlighters, testActor);
+ yield highlightFromHighlighter(view, highlighters, testActor, helper);
+});
+
+function* highlightFromRuleView(inspector, view, highlighters, testActor) {
+ yield selectNode("#polygon", inspector);
+ yield toggleShapesHighlighter(view, highlighters, "#polygon", "clip-path", true);
+ let container = getRuleViewProperty(view, "#polygon", "clip-path").valueSpan;
+ let shapesToggle = container.querySelector(".ruleview-shape");
+
+ let highlighterFront = highlighters.highlighters[HIGHLIGHTER_TYPE];
+ let markerHidden = yield testActor.getHighlighterNodeAttribute(
+ "shapes-marker-hover", "hidden", highlighterFront);
+ ok(markerHidden, "Hover marker on highlighter is not visible");
+
+ info("Hover over point 0 in rule view");
+ let pointSpan = container.querySelector(".ruleview-shape-point[data-point='0']");
+ let onHighlighterShown = highlighters.once("shapes-highlighter-shown");
+ EventUtils.synthesizeMouseAtCenter(pointSpan, {type: "mousemove"}, view.styleWindow);
+ yield onHighlighterShown;
+
+ ok(pointSpan.classList.contains("active"), "Hovered span is active");
+ is(highlighters.state.shapes.options.hoverPoint, "0",
+ "Hovered point is saved to state");
+
+ markerHidden = yield testActor.getHighlighterNodeAttribute(
+ "shapes-marker-hover", "hidden", highlighterFront);
+ ok(!markerHidden, "Marker on highlighter is visible");
+
+ info("Move mouse off point");
+ onHighlighterShown = highlighters.once("shapes-highlighter-shown");
+ EventUtils.synthesizeMouseAtCenter(shapesToggle, {type: "mousemove"}, view.styleWindow);
+ yield onHighlighterShown;
+
+ ok(!pointSpan.classList.contains("active"), "Hovered span is no longer active");
+ is(highlighters.state.shapes.options.hoverPoint, null, "Hovered point is null");
+
+ markerHidden = yield testActor.getHighlighterNodeAttribute(
+ "shapes-marker-hover", "hidden", highlighterFront);
+ ok(markerHidden, "Marker on highlighter is not visible");
+
+ info("Hide shapes highlighter");
+ yield toggleShapesHighlighter(view, highlighters, "#polygon", "clip-path", false);
+}
+
+function* highlightFromHighlighter(view, highlighters, testActor, helper) {
+ let highlighterFront = highlighters.highlighters[HIGHLIGHTER_TYPE];
+ let { mouse } = helper;
+
+ yield toggleShapesHighlighter(view, highlighters, "#polygon", "clip-path", true);
+ let container = getRuleViewProperty(view, "#polygon", "clip-path").valueSpan;
+
+ info("Hover over first point in highlighter");
+ let onEventHandled = highlighters.once("highlighter-event-handled");
+ yield mouse.move(0, 0);
+ yield onEventHandled;
+ let markerHidden = yield testActor.getHighlighterNodeAttribute(
+ "shapes-marker-hover", "hidden", highlighterFront);
+ ok(!markerHidden, "Marker on highlighter is visible");
+
+ let pointSpan = container.querySelector(".ruleview-shape-point[data-point='0']");
+ ok(pointSpan.classList.contains("active"), "Span for point 0 is active");
+ is(highlighters.state.shapes.hoverPoint, "0", "Hovered point is saved to state");
+
+ info("Check that point is still highlighted after moving it");
+ yield mouse.down(0, 0);
+ yield mouse.move(10, 10);
+ yield mouse.up(10, 10);
+ markerHidden = yield testActor.getHighlighterNodeAttribute(
+ "shapes-marker-hover", "hidden", highlighterFront);
+ ok(!markerHidden, "Marker on highlighter is visible after moving point");
+
+ container = getRuleViewProperty(view, "element", "clip-path").valueSpan;
+ pointSpan = container.querySelector(".ruleview-shape-point[data-point='0']");
+ ok(pointSpan.classList.contains("active"),
+ "Span for point 0 is active after moving point");
+ is(highlighters.state.shapes.hoverPoint, "0",
+ "Hovered point is saved to state after moving point");
+
+ info("Move mouse off point");
+ onEventHandled = highlighters.once("highlighter-event-handled");
+ yield mouse.move(100, 100);
+ yield onEventHandled;
+ markerHidden = yield testActor.getHighlighterNodeAttribute(
+ "shapes-marker-hover", "hidden", highlighterFront);
+ ok(markerHidden, "Marker on highlighter is no longer visible");
+ ok(!pointSpan.classList.contains("active"), "Span for point 0 is no longer active");
+ is(highlighters.state.shapes.hoverPoint, null, "Hovered point is null");
+}
--- a/devtools/client/inspector/test/head.js
+++ b/devtools/client/inspector/test/head.js
@@ -798,8 +798,38 @@ function* getDisplayedNodeTextContent(se
yield inspector.markup.expandNode(container.node);
yield waitForMultipleChildrenUpdates(inspector);
if (container) {
let textContainer = container.elt.querySelector("pre");
return textContainer.textContent;
}
return null;
}
+
+/**
+ * Toggle the shapes highlighter by simulating a click on the toggle
+ * in the rules view with the given selector and property
+ *
+ * @param {CssRuleView} view
+ * The instance of the rule-view panel
+ * @param {Object} highlighters
+ * The highlighters instance of the rule-view panel
+ * @param {String} selector
+ * The selector in the rule-view to look for the property in
+ * @param {String} property
+ * The name of the property
+ * @param {Boolean} show
+ * If true, the shapes highlighter is being shown. If false, it is being hidden
+ */
+function* toggleShapesHighlighter(view, highlighters, selector, property, show) {
+ info("Toggle shapes highlighter");
+ let container = getRuleViewProperty(view, selector, property).valueSpan;
+ let shapesToggle = container.querySelector(".ruleview-shape");
+ if (show) {
+ let onHighlighterShown = highlighters.once("shapes-highlighter-shown");
+ shapesToggle.click();
+ yield onHighlighterShown;
+ } else {
+ let onHighlighterHidden = highlighters.once("shapes-highlighter-hidden");
+ shapesToggle.click();
+ yield onHighlighterHidden;
+ }
+}
--- a/devtools/client/shared/output-parser.js
+++ b/devtools/client/shared/output-parser.js
@@ -10,16 +10,17 @@ const {getCSSLexer} = require("devtools/
const EventEmitter = require("devtools/shared/event-emitter");
const {
ANGLE_TAKING_FUNCTIONS,
BASIC_SHAPE_FUNCTIONS,
BEZIER_KEYWORDS,
COLOR_TAKING_FUNCTIONS,
CSS_TYPES
} = require("devtools/shared/css/properties-db");
+const {appendText} = require("devtools/client/inspector/shared/utils");
const Services = require("Services");
const HTML_NS = "http://www.w3.org/1999/xhtml";
const CSS_GRID_ENABLED_PREF = "layout.css.grid.enabled";
const CSS_SHAPES_ENABLED_PREF = "devtools.inspector.shapesHighlighter.enabled";
/**
* This module is used to process text for output by developer tools. This means
@@ -355,29 +356,546 @@ OutputParser.prototype = {
let value = this._createNode("span", {});
value.textContent = grid;
container.appendChild(toggle);
container.appendChild(value);
this.parsed.push(container);
},
+ /**
+ * Append a CSS shapes highlighter toggle next to the value, and parse the value
+ * into spans, each containing a point that can be hovered over.
+ *
+ * @param {String} shape
+ * The shape text value to append
+ * @param {Object} options
+ * Options object. For valid options and default values see
+ * _mergeOptions()
+ */
_appendShape: function (shape, options) {
+ const shapeTypes = [{
+ prefix: "polygon(",
+ coordParser: this._addPolygonPointNodes.bind(this)
+ }, {
+ prefix: "circle(",
+ coordParser: this._addCirclePointNodes.bind(this)
+ }, {
+ prefix: "ellipse(",
+ coordParser: this._addEllipsePointNodes.bind(this)
+ }, {
+ prefix: "inset(",
+ coordParser: this._addInsetPointNodes.bind(this)
+ }];
+
let container = this._createNode("span", {});
let toggle = this._createNode("span", {
class: options.shapeClass
});
- let value = this._createNode("span", {});
- value.textContent = shape;
+ for (let { prefix, coordParser } of shapeTypes) {
+ if (shape.includes(prefix)) {
+ let coordsBegin = prefix.length;
+ let coordsEnd = shape.lastIndexOf(")");
+ let valContainer = this._createNode("span", {});
+
+ container.appendChild(toggle);
+
+ appendText(valContainer, shape.substring(0, coordsBegin));
+
+ let coordsString = shape.substring(coordsBegin, coordsEnd);
+ valContainer = coordParser(coordsString, valContainer);
+
+ appendText(valContainer, shape.substring(coordsEnd));
+ container.appendChild(valContainer);
+ }
+ }
+
+ this.parsed.push(container);
+ },
+
+ /**
+ * Parse the given polygon coordinates and create a span for each coordinate pair,
+ * adding it to the given container node.
+ *
+ * @param {String} coords
+ * The string of coordinate pairs.
+ * @param {Node} container
+ * The node to which spans containing points are added.
+ * @returns {Node} The container to which spans have been added.
+ */
+ _addPolygonPointNodes: function (coords, container) {
+ let tokenStream = getCSSLexer(coords);
+ let token = tokenStream.nextToken();
+ let coord = "";
+ let i = 0;
+ let depth = 0;
+ let isXCoord = true;
+ let fillRule = false;
+ let coordNode = this._createNode("span", {
+ class: "ruleview-shape-point",
+ "data-point": `${i}`,
+ });
+
+ while (token) {
+ if (token.tokenType === "symbol" && token.text === ",") {
+ // Comma separating coordinate pairs; add coordNode to container and reset vars
+ if (!isXCoord) {
+ // Y coord not added to coordNode yet
+ let node = this._createNode("span", {
+ class: "ruleview-shape-point",
+ "data-point": `${i}`,
+ "data-pair": (isXCoord) ? "x" : "y"
+ }, coord);
+ coordNode.appendChild(node);
+ coord = "";
+ isXCoord = !isXCoord;
+ }
+
+ if (fillRule) {
+ // If the last text added was a fill-rule, do not increment i.
+ fillRule = false;
+ } else {
+ container.appendChild(coordNode);
+ i++;
+ }
+ appendText(container, coords.substring(token.startOffset, token.endOffset));
+ coord = "";
+ depth = 0;
+ isXCoord = true;
+ coordNode = this._createNode("span", {
+ class: "ruleview-shape-point",
+ "data-point": `${i}`,
+ });
+ } else if (token.tokenType === "symbol" && token.text === "(") {
+ depth++;
+ coord += coords.substring(token.startOffset, token.endOffset);
+ } else if (token.tokenType === "symbol" && token.text === ")") {
+ depth--;
+ coord += coords.substring(token.startOffset, token.endOffset);
+ } else if (token.tokenType === "whitespace" && coord === "") {
+ // Whitespace at beginning of coord; add to container
+ appendText(container, coords.substring(token.startOffset, token.endOffset));
+ } else if (token.tokenType === "whitespace" && depth === 0) {
+ // Whitespace signifying end of coord
+ let node = this._createNode("span", {
+ class: "ruleview-shape-point",
+ "data-point": `${i}`,
+ "data-pair": (isXCoord) ? "x" : "y"
+ }, coord);
+ coordNode.appendChild(node);
+ appendText(coordNode, coords.substring(token.startOffset, token.endOffset));
+ coord = "";
+ isXCoord = !isXCoord;
+ } else if ((token.tokenType === "number" || token.tokenType === "dimension" ||
+ token.tokenType === "percentage" || token.tokenType === "function")) {
+ if (isXCoord && coord && depth === 0) {
+ // Whitespace is not necessary between x/y coords.
+ let node = this._createNode("span", {
+ class: "ruleview-shape-point",
+ "data-point": `${i}`,
+ "data-pair": "x"
+ }, coord);
+ coordNode.appendChild(node);
+ isXCoord = false;
+ coord = "";
+ }
+
+ coord += coords.substring(token.startOffset, token.endOffset);
+ if (token.tokenType === "function") {
+ depth++;
+ }
+ } else if (token.tokenType === "ident" &&
+ (token.text === "nonzero" || token.text === "evenodd")) {
+ // A fill-rule (nonzero or evenodd).
+ appendText(container, coords.substring(token.startOffset, token.endOffset));
+ fillRule = true;
+ } else {
+ coord += coords.substring(token.startOffset, token.endOffset);
+ }
+ token = tokenStream.nextToken();
+ }
+
+ // Add coords if any are left over
+ if (coord) {
+ let node = this._createNode("span", {
+ class: "ruleview-shape-point",
+ "data-point": `${i}`,
+ "data-pair": (isXCoord) ? "x" : "y"
+ }, coord);
+ coordNode.appendChild(node);
+ container.appendChild(coordNode);
+ }
+ return container;
+ },
+
+ /**
+ * Parse the given circle coordinates and populate the given container appropriately
+ * with a separate span for the center point.
+ *
+ * @param {String} coords
+ * The circle definition.
+ * @param {Node} container
+ * The node to which the definition is added.
+ * @returns {Node} The container to which the definition has been added.
+ */
+ _addCirclePointNodes: function (coords, container) {
+ let tokenStream = getCSSLexer(coords);
+ let token = tokenStream.nextToken();
+ let depth = 0;
+ let coord = "";
+ let point = "radius";
+ let centerNode = this._createNode("span", {
+ class: "ruleview-shape-point",
+ "data-point": "center"
+ });
+ while (token) {
+ if (token.tokenType === "symbol" && token.text === "(") {
+ depth++;
+ coord += coords.substring(token.startOffset, token.endOffset);
+ } else if (token.tokenType === "symbol" && token.text === ")") {
+ depth--;
+ coord += coords.substring(token.startOffset, token.endOffset);
+ } else if (token.tokenType === "whitespace" && coord === "") {
+ // Whitespace at beginning of coord; add to container
+ appendText(container, coords.substring(token.startOffset, token.endOffset));
+ } else if (token.tokenType === "whitespace" && point === "radius" && depth === 0) {
+ // Whitespace signifying end of radius
+ let node = this._createNode("span", {
+ class: "ruleview-shape-point",
+ "data-point": "radius"
+ }, coord);
+ container.appendChild(node);
+ appendText(container, coords.substring(token.startOffset, token.endOffset));
+ point = "cx";
+ coord = "";
+ depth = 0;
+ } else if (token.tokenType === "whitespace" && depth === 0) {
+ // Whitespace signifying end of cx/cy
+ let node = this._createNode("span", {
+ class: "ruleview-shape-point",
+ "data-point": "center",
+ "data-pair": (point === "cx") ? "x" : "y"
+ }, coord);
+ centerNode.appendChild(node);
+ appendText(centerNode, coords.substring(token.startOffset, token.endOffset));
+ point = (point === "cx") ? "cy" : "cx";
+ coord = "";
+ depth = 0;
+ } else if (token.tokenType === "ident" && token.text === "at") {
+ // "at"; Add radius to container if not already done so
+ if (point === "radius" && coord) {
+ let node = this._createNode("span", {
+ class: "ruleview-shape-point",
+ "data-point": "radius"
+ }, coord);
+ container.appendChild(node);
+ }
+ appendText(container, coords.substring(token.startOffset, token.endOffset));
+ point = "cx";
+ coord = "";
+ depth = 0;
+ } else if ((token.tokenType === "number" || token.tokenType === "dimension" ||
+ token.tokenType === "percentage" || token.tokenType === "function")) {
+ if (point === "cx" && coord && depth === 0) {
+ // Center coords don't require whitespace between x/y. So if current point is
+ // cx, we have the cx coord, and depth is 0, then this token is actually cy.
+ // Add cx to centerNode and set point to cy.
+ let node = this._createNode("span", {
+ class: "ruleview-shape-point",
+ "data-point": "center",
+ "data-pair": "x"
+ }, coord);
+ centerNode.appendChild(node);
+ point = "cy";
+ coord = "";
+ }
+
+ coord += coords.substring(token.startOffset, token.endOffset);
+ if (token.tokenType === "function") {
+ depth++;
+ }
+ } else {
+ coord += coords.substring(token.startOffset, token.endOffset);
+ }
+ token = tokenStream.nextToken();
+ }
+
+ // Add coords if any are left over.
+ if (coord) {
+ if (point === "radius") {
+ let node = this._createNode("span", {
+ class: "ruleview-shape-point",
+ "data-point": "radius"
+ }, coord);
+ container.appendChild(node);
+ } else {
+ let node = this._createNode("span", {
+ class: "ruleview-shape-point",
+ "data-point": "center",
+ "data-pair": (point === "cx") ? "x" : "y"
+ }, coord);
+ centerNode.appendChild(node);
+ }
+ }
- container.appendChild(toggle);
- container.appendChild(value);
- this.parsed.push(container);
+ if (centerNode.textContent) {
+ container.appendChild(centerNode);
+ }
+ return container;
+ },
+
+ /**
+ * Parse the given ellipse coordinates and populate the given container appropriately
+ * with a separate span for each point
+ *
+ * @param {String} coords
+ * The ellipse definition.
+ * @param {Node} container
+ * The node to which the definition is added.
+ * @returns {Node} The container to which the definition has been added.
+ */
+ _addEllipsePointNodes: function (coords, container) {
+ let tokenStream = getCSSLexer(coords);
+ let token = tokenStream.nextToken();
+ let depth = 0;
+ let coord = "";
+ let point = "rx";
+ let centerNode = this._createNode("span", {
+ class: "ruleview-shape-point",
+ "data-point": "center"
+ });
+ while (token) {
+ if (token.tokenType === "symbol" && token.text === "(") {
+ depth++;
+ coord += coords.substring(token.startOffset, token.endOffset);
+ } else if (token.tokenType === "symbol" && token.text === ")") {
+ depth--;
+ coord += coords.substring(token.startOffset, token.endOffset);
+ } else if (token.tokenType === "whitespace" && coord === "") {
+ // Whitespace at beginning of coord; add to container
+ appendText(container, coords.substring(token.startOffset, token.endOffset));
+ } else if (token.tokenType === "whitespace" && depth === 0) {
+ if (point === "rx" || point === "ry") {
+ // Whitespace signifying end of rx/ry
+ let node = this._createNode("span", {
+ class: "ruleview-shape-point",
+ "data-point": point,
+ }, coord);
+ container.appendChild(node);
+ appendText(container, coords.substring(token.startOffset, token.endOffset));
+ point = (point === "rx") ? "ry" : "cx";
+ coord = "";
+ depth = 0;
+ } else {
+ // Whitespace signifying end of cx/cy
+ let node = this._createNode("span", {
+ class: "ruleview-shape-point",
+ "data-point": "center",
+ "data-pair": (point === "cx") ? "x" : "y"
+ }, coord);
+ centerNode.appendChild(node);
+ appendText(centerNode, coords.substring(token.startOffset, token.endOffset));
+ point = (point === "cx") ? "cy" : "cx";
+ coord = "";
+ depth = 0;
+ }
+ } else if (token.tokenType === "ident" && token.text === "at") {
+ // "at"; Add radius to container if not already done so
+ if (point === "ry" && coord) {
+ let node = this._createNode("span", {
+ class: "ruleview-shape-point",
+ "data-point": "ry"
+ }, coord);
+ container.appendChild(node);
+ }
+ appendText(container, coords.substring(token.startOffset, token.endOffset));
+ point = "cx";
+ coord = "";
+ depth = 0;
+ } else if ((token.tokenType === "number" || token.tokenType === "dimension" ||
+ token.tokenType === "percentage" || token.tokenType === "function")) {
+ if (point === "rx" && coord && depth === 0) {
+ // Radius coords don't require whitespace between x/y.
+ let node = this._createNode("span", {
+ class: "ruleview-shape-point",
+ "data-point": "rx",
+ }, coord);
+ container.appendChild(node);
+ point = "ry";
+ coord = "";
+ }
+ if (point === "cx" && coord && depth === 0) {
+ // Center coords don't require whitespace between x/y.
+ let node = this._createNode("span", {
+ class: "ruleview-shape-point",
+ "data-point": "center",
+ "data-pair": "x"
+ }, coord);
+ centerNode.appendChild(node);
+ point = "cy";
+ coord = "";
+ }
+
+ coord += coords.substring(token.startOffset, token.endOffset);
+ if (token.tokenType === "function") {
+ depth++;
+ }
+ } else {
+ coord += coords.substring(token.startOffset, token.endOffset);
+ }
+ token = tokenStream.nextToken();
+ }
+
+ // Add coords if any are left over.
+ if (coord) {
+ if (point === "rx" || point === "ry") {
+ let node = this._createNode("span", {
+ class: "ruleview-shape-point",
+ "data-point": point
+ }, coord);
+ container.appendChild(node);
+ } else {
+ let node = this._createNode("span", {
+ class: "ruleview-shape-point",
+ "data-point": "center",
+ "data-pair": (point === "cx") ? "x" : "y"
+ }, coord);
+ centerNode.appendChild(node);
+ }
+ }
+
+ if (centerNode.textContent) {
+ container.appendChild(centerNode);
+ }
+ return container;
+ },
+
+ /**
+ * Parse the given inset coordinates and populate the given container appropriately.
+ *
+ * @param {String} coords
+ * The inset definition.
+ * @param {Node} container
+ * The node to which the definition is added.
+ * @returns {Node} The container to which the definition has been added.
+ */
+ _addInsetPointNodes: function (coords, container) {
+ const insetPoints = ["top", "right", "bottom", "left"];
+ let tokenStream = getCSSLexer(coords);
+ let token = tokenStream.nextToken();
+ let depth = 0;
+ let coord = "";
+ let i = 0;
+ let round = false;
+ // nodes is an array containing all the coordinate spans. otherText is an array of
+ // arrays, each containing the text that should be inserted into container before
+ // the node with the same index. i.e. all elements of otherText[i] is inserted
+ // into container before nodes[i].
+ let nodes = [];
+ let otherText = [[]];
+
+ while (token) {
+ if (round) {
+ // Everything that comes after "round" should just be plain text
+ otherText[i].push(coords.substring(token.startOffset, token.endOffset));
+ } else if (token.tokenType === "symbol" && token.text === "(") {
+ depth++;
+ coord += coords.substring(token.startOffset, token.endOffset);
+ } else if (token.tokenType === "symbol" && token.text === ")") {
+ depth--;
+ coord += coords.substring(token.startOffset, token.endOffset);
+ } else if (token.tokenType === "whitespace" && coord === "") {
+ // Whitespace at beginning of coord; add to container
+ otherText[i].push(coords.substring(token.startOffset, token.endOffset));
+ } else if (token.tokenType === "whitespace" && depth === 0) {
+ // Whitespace signifying end of coord; create node and push to nodes
+ let node = this._createNode("span", {
+ class: "ruleview-shape-point"
+ }, coord);
+ nodes.push(node);
+ i++;
+ coord = "";
+ otherText[i] = [coords.substring(token.startOffset, token.endOffset)];
+ depth = 0;
+ } else if ((token.tokenType === "number" || token.tokenType === "dimension" ||
+ token.tokenType === "percentage" || token.tokenType === "function")) {
+ if (coord && depth === 0) {
+ // Inset coords don't require whitespace between each coord.
+ let node = this._createNode("span", {
+ class: "ruleview-shape-point",
+ }, coord);
+ nodes.push(node);
+ i++;
+ coord = "";
+ otherText[i] = [];
+ }
+
+ coord += coords.substring(token.startOffset, token.endOffset);
+ if (token.tokenType === "function") {
+ depth++;
+ }
+ } else if (token.tokenType === "ident" && token.text === "round") {
+ if (coord && depth === 0) {
+ // Whitespace is not necessary before "round"; create a new node for the coord
+ let node = this._createNode("span", {
+ class: "ruleview-shape-point",
+ }, coord);
+ nodes.push(node);
+ i++;
+ coord = "";
+ otherText[i] = [];
+ }
+ round = true;
+ otherText[i].push(coords.substring(token.startOffset, token.endOffset));
+ } else {
+ coord += coords.substring(token.startOffset, token.endOffset);
+ }
+ token = tokenStream.nextToken();
+ }
+
+ // Take care of any leftover text
+ if (coord) {
+ if (round) {
+ otherText[i].push(coord);
+ } else {
+ let node = this._createNode("span", {
+ class: "ruleview-shape-point",
+ }, coord);
+ nodes.push(node);
+ }
+ }
+
+ // insetPoints contains the 4 different possible inset points in the order they are
+ // defined. By taking the modulo of the index in insetPoints with the number of nodes,
+ // we can get which node represents each point (e.g. if there is only 1 node, it
+ // represents all 4 points). The exception is "left" when there are 3 nodes. In that
+ // case, it is nodes[1] that represents the left point rather than nodes[0].
+ for (let j = 0; j < 4; j++) {
+ let point = insetPoints[j];
+ let nodeIndex = (point === "left" && nodes.length === 3) ? 1 : j % nodes.length;
+ nodes[nodeIndex].classList.add(point);
+ }
+
+ nodes.forEach((node, j, array) => {
+ for (let text of otherText[j]) {
+ appendText(container, text);
+ }
+ container.appendChild(node);
+ });
+
+ // Add text that comes after the last node, if any exists
+ if (otherText[nodes.length]) {
+ for (let text of otherText[nodes.length]) {
+ appendText(container, text);
+ }
+ }
+
+ return container;
},
/**
* Append a angle value to the output
*
* @param {String} angle
* angle to append
* @param {Object} options
--- a/devtools/client/shared/test/browser_outputparser.js
+++ b/devtools/client/shared/test/browser_outputparser.js
@@ -1,15 +1,16 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const OutputParser = require("devtools/client/shared/output-parser");
const {initCssProperties, getCssProperties} = require("devtools/shared/fronts/css-properties");
+const CSS_SHAPES_ENABLED_PREF = "devtools.inspector.shapesHighlighter.enabled";
add_task(function* () {
yield addTab("about:blank");
yield performTest();
gBrowser.removeCurrentTab();
});
function* performTest() {
@@ -22,16 +23,17 @@ function* performTest() {
let cssProperties = getCssProperties(toolbox);
let parser = new OutputParser(doc, cssProperties);
testParseCssProperty(doc, parser);
testParseCssVar(doc, parser);
testParseURL(doc, parser);
testParseFilter(doc, parser);
testParseAngle(doc, parser);
+ testParseShape(doc, parser);
host.destroy();
}
// Class name used in color swatch.
var COLOR_TEST_CLASS = "test-class";
// Create a new CSS color-parsing test. |name| is the name of the CSS
@@ -288,8 +290,126 @@ function testParseAngle(doc, parser) {
frag = parser.parseCssProperty("background-image",
"linear-gradient(90deg, red, blue", {
angleSwatchClass: "test-angleswatch"
});
swatchCount = frag.querySelectorAll(".test-angleswatch").length;
is(swatchCount, 1, "angle swatch was created");
}
+
+function testParseShape(doc, parser) {
+ info("Test shape parsing");
+ pushPref(CSS_SHAPES_ENABLED_PREF, true);
+ const tests = [
+ {
+ desc: "Polygon shape",
+ definition: "polygon(evenodd, 0px 0px, 10%200px,30%30% , calc(250px - 10px) 0 ,\n "
+ + "12em var(--variable), 100% 100%) margin-box",
+ spanCount: 18
+ },
+ {
+ desc: "Invalid polygon shape",
+ definition: "polygon(0px 0px 100px 20px, 20% 20%)",
+ spanCount: 0
+ },
+ {
+ desc: "Circle shape with all arguments",
+ definition: "circle(25% at\n 30% 200px) border-box",
+ spanCount: 4
+ },
+ {
+ desc: "Circle shape with only one center",
+ definition: "circle(25em at 40%)",
+ spanCount: 3
+ },
+ {
+ desc: "Circle shape with no radius",
+ definition: "circle(at 30% 40%)",
+ spanCount: 3
+ },
+ {
+ desc: "Circle shape with no center",
+ definition: "circle(12em)",
+ spanCount: 1
+ },
+ {
+ desc: "Circle shape with no arguments",
+ definition: "circle()",
+ spanCount: 0
+ },
+ {
+ desc: "Circle shape with no space before at",
+ definition: "circle(25%at 30% 30%)",
+ spanCount: 4
+ },
+ {
+ desc: "Invalid circle shape",
+ definition: "circle(25%at30%30%)",
+ spanCount: 0
+ },
+ {
+ desc: "Ellipse shape with all arguments",
+ definition: "ellipse(200px 10em at 25% 120px) content-box",
+ spanCount: 5
+ },
+ {
+ desc: "Ellipse shape with only one center",
+ definition: "ellipse(200px 10% at 120px)",
+ spanCount: 4
+ },
+ {
+ desc: "Ellipse shape with no radius",
+ definition: "ellipse(at 25% 120px)",
+ spanCount: 3
+ },
+ {
+ desc: "Ellipse shape with no center",
+ definition: "ellipse(200px\n10em)",
+ spanCount: 2
+ },
+ {
+ desc: "Ellipse shape with no arguments",
+ definition: "ellipse()",
+ spanCount: 0
+ },
+ {
+ desc: "Invalid ellipse shape",
+ definition: "ellipse(200px100px at 30$ 20%)",
+ spanCount: 0
+ },
+ {
+ desc: "Inset shape with 4 arguments",
+ definition: "inset(200px 100px\n 30%15%)",
+ spanCount: 4
+ },
+ {
+ desc: "Inset shape with 3 arguments",
+ definition: "inset(200px 100px 15%)",
+ spanCount: 3
+ },
+ {
+ desc: "Inset shape with 2 arguments",
+ definition: "inset(200px 100px)",
+ spanCount: 2
+ },
+ {
+ desc: "Inset shape with 1 argument",
+ definition: "inset(200px)",
+ spanCount: 1
+ },
+ {
+ desc: "Inset shape with 0 arguments",
+ definition: "inset()",
+ spanCount: 0
+ }
+ ];
+
+ for (let {desc, definition, spanCount} of tests) {
+ info(desc);
+ let frag = parser.parseCssProperty("clip-path", definition, {
+ shapeClass: "ruleview-shape"
+ });
+ let spans = frag.querySelectorAll(".ruleview-shape-point");
+ is(spans.length, spanCount, desc + " span count");
+ is(frag.textContent, definition, desc + " text content");
+ }
+}
--- a/devtools/client/themes/rules.css
+++ b/devtools/client/themes/rules.css
@@ -472,16 +472,20 @@
}
.ruleview-shape {
background: url("chrome://devtools/skin/images/tool-shadereditor.svg");
border-radius: 0;
background-size: 1em;
}
+.ruleview-shape-point.active {
+ background-color: var(--rule-highlight-background-color);
+}
+
.ruleview-colorswatch::before {
content: '';
background-color: #eee;
background-image: linear-gradient(45deg, #ccc 25%, transparent 25%, transparent 75%, #ccc 75%, #ccc),
linear-gradient(45deg, #ccc 25%, transparent 25%, transparent 75%, #ccc 75%, #ccc);
background-size: 12px 12px;
background-position: 0 0, 6px 6px;
position: absolute;
--- a/devtools/server/actors/highlighters.css
+++ b/devtools/server/actors/highlighters.css
@@ -608,8 +608,12 @@
stroke: var(--highlighter-guide-color);
shape-rendering: geometricPrecision;
vector-effect: non-scaling-stroke;
}
:-moz-native-anonymous .shapes-markers {
fill: var(--highlighter-marker-color);
}
+
+:-moz-native-anonymous .shapes-marker-hover {
+ fill: var(--highlighter-guide-color);
+}
--- a/devtools/server/actors/highlighters.js
+++ b/devtools/server/actors/highlighters.js
@@ -454,16 +454,19 @@ exports.CustomHighlighterActor = protoco
}
// The assumption is that all custom highlighters need the canvasframe
// container to append their elements, so if this is a XUL window, bail out.
if (!isXUL(this._inspector.tabActor.window)) {
this._highlighterEnv = new HighlighterEnvironment();
this._highlighterEnv.initFromTabActor(inspector.tabActor);
this._highlighter = new constructor(this._highlighterEnv);
+ if (this._highlighter.on) {
+ this._highlighter.on("highlighter-event", this._onHighlighterEvent.bind(this));
+ }
} else {
throw new Error("Custom " + typeName +
"highlighter cannot be created in a XUL window");
}
},
get conn() {
return this._inspector && this._inspector.conn;
@@ -507,21 +510,31 @@ exports.CustomHighlighterActor = protoco
*/
hide: function () {
if (this._highlighter) {
this._highlighter.hide();
}
},
/**
+ * Upon receiving an event from the highlighter, forward it to the client.
+ */
+ _onHighlighterEvent: function (type, data) {
+ events.emit(this, "highlighter-event", data);
+ },
+
+ /**
* Kill this actor. This method is called automatically just before the actor
* is destroyed.
*/
finalize: function () {
if (this._highlighter) {
+ if (this._highlighter.off) {
+ this._highlighter.off("highlighter-event", this._onHighlighterEvent.bind(this));
+ }
this._highlighter.destroy();
this._highlighter = null;
}
if (this._highlighterEnv) {
this._highlighterEnv.destroy();
this._highlighterEnv = null;
}
--- a/devtools/server/actors/highlighters/shapes.js
+++ b/devtools/server/actors/highlighters/shapes.js
@@ -10,37 +10,41 @@ const { setIgnoreLayoutChanges, getCurre
const { AutoRefreshHighlighter } = require("./auto-refresh");
const {
getDistance,
clickedOnEllipseEdge,
distanceToLine,
projection,
clickedOnPoint
} = require("devtools/server/actors/utils/shapes-geometry-utils");
+const EventEmitter = require("devtools/shared/event-emitter");
const BASE_MARKER_SIZE = 10;
// the width of the area around highlighter lines that can be clicked, in px
const LINE_CLICK_WIDTH = 5;
const DOM_EVENTS = ["mousedown", "mousemove", "mouseup", "dblclick"];
const _dragging = Symbol("shapes/dragging");
/**
* The ShapesHighlighter draws an outline shapes in the page.
* The idea is to have something that is able to wrap complex shapes for css properties
* such as shape-outside/inside, clip-path but also SVG elements.
*/
class ShapesHighlighter extends AutoRefreshHighlighter {
constructor(highlighterEnv) {
super(highlighterEnv);
+ EventEmitter.decorate(this);
this.ID_CLASS_PREFIX = "shapes-";
this.referenceBox = "border";
this.useStrokeBox = false;
this.geometryBox = "";
+ this.hoveredPoint = null;
+ this.fillRule = "";
this.markup = new CanvasFrameAnonymousContentHelper(this.highlighterEnv,
this._buildMarkup.bind(this));
this.onPageHide = this.onPageHide.bind(this);
let { pageListenerTarget } = this.highlighterEnv;
DOM_EVENTS.forEach(event => pageListenerTarget.addEventListener(event, this));
pageListenerTarget.addEventListener("pagehide", this.onPageHide);
@@ -117,16 +121,27 @@ class ShapesHighlighter extends AutoRefr
parent: mainSvg,
attributes: {
"id": "markers",
"class": "markers",
},
prefix: this.ID_CLASS_PREFIX
});
+ createSVGNode(this.win, {
+ nodeType: "path",
+ parent: mainSvg,
+ attributes: {
+ "id": "marker-hover",
+ "class": "marker-hover",
+ "hidden": true
+ },
+ prefix: this.ID_CLASS_PREFIX
+ });
+
return container;
}
get currentDimensions() {
let { top, left, width, height } = this.currentQuads[this.referenceBox][0].bounds;
// If an SVG element has a stroke, currentQuads will return the stroke bounding box.
// However, clip-path always uses the object bounding box unless "stroke-box" is
@@ -196,16 +211,17 @@ class ShapesHighlighter extends AutoRefr
if (this.property === "shape-outside") {
this.currentNode.style.setProperty("width", this[_dragging].origWidth);
}
this[_dragging] = null;
}
break;
case "mousemove":
if (!this[_dragging]) {
+ this._handleMouseMoveNotDragging(pageX, pageY);
return;
}
event.stopPropagation();
event.preventDefault();
let { point } = this[_dragging];
if (this.shapeType === "polygon") {
this._handlePolygonMove(pageX, pageY);
@@ -215,17 +231,17 @@ class ShapesHighlighter extends AutoRefr
this._handleEllipseMove(point, pageX, pageY);
} else if (this.shapeType === "inset") {
this._handleInsetMove(point, pageX, pageY);
}
break;
case "dblclick":
if (this.shapeType === "polygon") {
let { percentX, percentY } = this.convertPageCoordsToPercent(pageX, pageY);
- let index = this.getPolygonClickedPoint(percentX, percentY);
+ let index = this.getPolygonPointAt(percentX, percentY);
if (index === -1) {
this.getPolygonClickedLine(percentX, percentY);
return;
}
this._deletePolygonPoint(index);
}
break;
@@ -235,17 +251,17 @@ class ShapesHighlighter extends AutoRefr
/**
* Handle a click when highlighting a polygon.
* @param {any} pageX the x coordinate of the click
* @param {any} pageY the y coordinate of the click
*/
_handlePolygonClick(pageX, pageY) {
let { width, height } = this.zoomAdjustedDimensions;
let { percentX, percentY } = this.convertPageCoordsToPercent(pageX, pageY);
- let point = this.getPolygonClickedPoint(percentX, percentY);
+ let point = this.getPolygonPointAt(percentX, percentY);
if (point === -1) {
return;
}
let [x, y] = this.coordUnits[point];
let xComputed = this.coordinates[point][0] / 100 * width;
let yComputed = this.coordinates[point][1] / 100 * height;
let unitX = getUnit(x);
@@ -268,66 +284,73 @@ class ShapesHighlighter extends AutoRefr
*/
_handlePolygonMove(pageX, pageY) {
let { point, unitX, unitY, valueX, valueY, ratioX, ratioY, x, y } = this[_dragging];
let deltaX = (pageX - x) * ratioX;
let deltaY = (pageY - y) * ratioY;
let newX = `${valueX + deltaX}${unitX}`;
let newY = `${valueY + deltaY}${unitY}`;
- let polygonDef = this.coordUnits.map((coords, i) => {
+ let polygonDef = (this.fillRule) ? `${this.fillRule}, ` : "";
+ polygonDef += this.coordUnits.map((coords, i) => {
return (i === point) ? `${newX} ${newY}` : `${coords[0]} ${coords[1]}`;
}).join(", ");
polygonDef = (this.geometryBox) ? `polygon(${polygonDef}) ${this.geometryBox}` :
`polygon(${polygonDef})`;
this.currentNode.style.setProperty(this.property, polygonDef, "important");
}
/**
* Set the inline style of the polygon, adding a new point.
* @param {Number} after the index of the point that the new point should be added after
* @param {Number} x the x coordinate of the new point
* @param {Number} y the y coordinate of the new point
*/
_addPolygonPoint(after, x, y) {
- let polygonDef = this.coordUnits.map((coords, i) => {
+ let polygonDef = (this.fillRule) ? `${this.fillRule}, ` : "";
+ polygonDef += this.coordUnits.map((coords, i) => {
return (i === after) ? `${coords[0]} ${coords[1]}, ${x}% ${y}%` :
`${coords[0]} ${coords[1]}`;
}).join(", ");
polygonDef = (this.geometryBox) ? `polygon(${polygonDef}) ${this.geometryBox}` :
`polygon(${polygonDef})`;
+ this.hoveredPoint = after + 1;
+ this._emitHoverEvent(this.hoveredPoint);
this.currentNode.style.setProperty(this.property, polygonDef, "important");
}
/**
* Set the inline style of the polygon, deleting the given point.
* @param {Number} point the index of the point to delete
*/
_deletePolygonPoint(point) {
let coordinates = this.coordUnits.slice();
coordinates.splice(point, 1);
- let polygonDef = coordinates.map((coords, i) => {
+ let polygonDef = (this.fillRule) ? `${this.fillRule}, ` : "";
+ polygonDef += coordinates.map((coords, i) => {
return `${coords[0]} ${coords[1]}`;
}).join(", ");
polygonDef = (this.geometryBox) ? `polygon(${polygonDef}) ${this.geometryBox}` :
`polygon(${polygonDef})`;
+ this.hoveredPoint = null;
+ this._emitHoverEvent(this.hoveredPoint);
this.currentNode.style.setProperty(this.property, polygonDef, "important");
}
/**
* Handle a click when highlighting a circle.
* @param {any} pageX the x coordinate of the click
* @param {any} pageY the y coordinate of the click
*/
_handleCircleClick(pageX, pageY) {
let { width, height } = this.zoomAdjustedDimensions;
let { percentX, percentY } = this.convertPageCoordsToPercent(pageX, pageY);
- let point = this.getCircleClickedPoint(percentX, percentY);
+ let point = this.getCirclePointAt(percentX, percentY);
if (!point) {
return;
}
if (point === "center") {
let { cx, cy } = this.coordUnits;
let cxComputed = this.coordinates.cx / 100 * width;
let cyComputed = this.coordinates.cy / 100 * height;
@@ -398,17 +421,17 @@ class ShapesHighlighter extends AutoRefr
/**
* Handle a click when highlighting an ellipse.
* @param {any} pageX the x coordinate of the click
* @param {any} pageY the y coordinate of the click
*/
_handleEllipseClick(pageX, pageY) {
let { width, height } = this.zoomAdjustedDimensions;
let { percentX, percentY } = this.convertPageCoordsToPercent(pageX, pageY);
- let point = this.getEllipseClickedPoint(percentX, percentY);
+ let point = this.getEllipsePointAt(percentX, percentY);
if (!point) {
return;
}
if (point === "center") {
let { cx, cy } = this.coordUnits;
let cxComputed = this.coordinates.cx / 100 * width;
let cyComputed = this.coordinates.cy / 100 * height;
@@ -497,17 +520,17 @@ class ShapesHighlighter extends AutoRefr
/**
* Handle a click when highlighting an inset.
* @param {any} pageX the x coordinate of the click
* @param {any} pageY the y coordinate of the click
*/
_handleInsetClick(pageX, pageY) {
let { width, height } = this.zoomAdjustedDimensions;
let { percentX, percentY } = this.convertPageCoordsToPercent(pageX, pageY);
- let point = this.getInsetClickedPoint(percentX, percentY);
+ let point = this.getInsetPointAt(percentX, percentY);
if (!point) {
return;
}
let value = this.coordUnits[point];
let size = (point === "left" || point === "right") ? width : height;
let computedValue = this.coordinates[point] / 100 * size;
let unit = getUnit(value);
@@ -550,16 +573,134 @@ class ShapesHighlighter extends AutoRefr
`inset(${top} ${right} ${bottom} ${left} round ${round})` :
`inset(${top} ${right} ${bottom} ${left})`;
insetDef += (this.geometryBox) ? this.geometryBox : "";
this.currentNode.style.setProperty(this.property, insetDef, "important");
}
+ _handleMouseMoveNotDragging(pageX, pageY) {
+ let { percentX, percentY } = this.convertPageCoordsToPercent(pageX, pageY);
+ if (this.shapeType === "polygon") {
+ let point = this.getPolygonPointAt(percentX, percentY);
+ let oldHoveredPoint = this.hoveredPoint;
+ this.hoveredPoint = (point !== -1) ? point : null;
+ if (this.hoveredPoint !== oldHoveredPoint) {
+ this._emitHoverEvent(this.hoveredPoint);
+ }
+ this._handleMarkerHover(point);
+ } else if (this.shapeType === "circle") {
+ let point = this.getCirclePointAt(percentX, percentY);
+ let oldHoveredPoint = this.hoveredPoint;
+ this.hoveredPoint = point ? point : null;
+ if (this.hoveredPoint !== oldHoveredPoint) {
+ this._emitHoverEvent(this.hoveredPoint);
+ }
+ this._handleMarkerHover(point);
+ } else if (this.shapeType === "ellipse") {
+ let point = this.getEllipsePointAt(percentX, percentY);
+ let oldHoveredPoint = this.hoveredPoint;
+ this.hoveredPoint = point ? point : null;
+ if (this.hoveredPoint !== oldHoveredPoint) {
+ this._emitHoverEvent(this.hoveredPoint);
+ }
+ this._handleMarkerHover(point);
+ } else if (this.shapeType === "inset") {
+ let point = this.getInsetPointAt(percentX, percentY);
+ let oldHoveredPoint = this.hoveredPoint;
+ this.hoveredPoint = point ? point : null;
+ if (this.hoveredPoint !== oldHoveredPoint) {
+ this._emitHoverEvent(this.hoveredPoint);
+ }
+ this._handleMarkerHover(point);
+ }
+ }
+
+ _handleMarkerHover(point) {
+ // Hide hover marker for now, will be shown if point is a valid hover target
+ this.getElement("marker-hover").setAttribute("hidden", true);
+ if (point === null || point === undefined) {
+ return;
+ }
+
+ if (this.shapeType === "polygon") {
+ if (point === -1) {
+ return;
+ }
+ this._drawHoverMarker([this.coordinates[point]]);
+ } else if (this.shapeType === "circle") {
+ let { cx, cy, rx } = this.coordinates;
+ if (point === "radius") {
+ this._drawHoverMarker([[cx + rx, cy]]);
+ } else if (point === "center") {
+ this._drawHoverMarker([[cx, cy]]);
+ }
+ } else if (this.shapeType === "ellipse") {
+ if (point === "center") {
+ let { cx, cy } = this.coordinates;
+ this._drawHoverMarker([[cx, cy]]);
+ } else if (point === "rx") {
+ let { cx, cy, rx } = this.coordinates;
+ this._drawHoverMarker([[cx + rx, cy]]);
+ } else if (point === "ry") {
+ let { cx, cy, ry } = this.coordinates;
+ this._drawHoverMarker([[cx, cy + ry]]);
+ }
+ } else if (this.shapeType === "inset") {
+ if (!point) {
+ return;
+ }
+
+ let { top, right, bottom, left } = this.coordinates;
+ let centerX = (left + (100 - right)) / 2;
+ let centerY = (top + (100 - bottom)) / 2;
+ let points = point.split(",");
+ let coords = points.map(side => {
+ if (side === "top") {
+ return [centerX, top];
+ } else if (side === "right") {
+ return [100 - right, centerY];
+ } else if (side === "bottom") {
+ return [centerX, 100 - bottom];
+ } else if (side === "left") {
+ return [left, centerY];
+ }
+ return null;
+ });
+
+ this._drawHoverMarker(coords);
+ }
+ }
+
+ _drawHoverMarker(points) {
+ let { width, height } = this.zoomAdjustedDimensions;
+ let zoom = getCurrentZoom(this.win);
+ let path = points.map(([x, y]) => {
+ return getCirclePath(x, y, width, height, zoom);
+ }).join(" ");
+
+ let markerHover = this.getElement("marker-hover");
+ markerHover.setAttribute("d", path);
+ markerHover.removeAttribute("hidden");
+ }
+
+ _emitHoverEvent(point) {
+ if (point === null || point === undefined) {
+ this.emit("highlighter-event", {
+ type: "shape-hover-off"
+ });
+ } else {
+ this.emit("highlighter-event", {
+ type: "shape-hover-on",
+ point: point.toString()
+ });
+ }
+ }
+
/**
* Convert the given coordinates on the page to percentages relative to the current
* element.
* @param {Number} pageX the x coordinate on the page
* @param {Number} pageY the y coordinate on the page
* @returns {Object} object of form {percentX, percentY}, which are the x/y coords
* in percentages relative to the element.
*/
@@ -587,23 +728,23 @@ class ShapesHighlighter extends AutoRefr
x = x * width / 100;
y = y * height / 100;
x += left;
y += top;
return { x, y };
}
/**
- * Get the id of the point clicked on the polygon highlighter.
+ * Get the id of the point on the polygon highlighter at the given coordinate.
* @param {Number} pageX the x coordinate on the page, in % relative to the element
* @param {Number} pageY the y coordinate on the page, in % relative to the element
* @returns {Number} the index of the point that was clicked on in this.coordinates,
* or -1 if none of the points were clicked on.
*/
- getPolygonClickedPoint(pageX, pageY) {
+ getPolygonPointAt(pageX, pageY) {
let { coordinates } = this;
let { width, height } = this.zoomAdjustedDimensions;
let zoom = getCurrentZoom(this.win);
let clickRadiusX = BASE_MARKER_SIZE / zoom * 100 / width;
let clickRadiusY = BASE_MARKER_SIZE / zoom * 100 / height;
for (let [index, coord] of coordinates.entries()) {
let [x, y] = coord;
@@ -642,23 +783,23 @@ class ShapesHighlighter extends AutoRefr
let [newX, newY] = projection(x1, y1, x2, y2, pageX, pageY);
this._addPolygonPoint(i, newX, newY);
return;
}
}
}
/**
- * Check if the center point or radius of the circle highlighter was clicked
+ * Check if the center point or radius of the circle highlighter is at given coords
* @param {Number} pageX the x coordinate on the page, in % relative to the element
* @param {Number} pageY the y coordinate on the page, in % relative to the element
* @returns {String} "center" if the center point was clicked, "radius" if the radius
* was clicked, "" if neither was clicked.
*/
- getCircleClickedPoint(pageX, pageY) {
+ getCirclePointAt(pageX, pageY) {
let { cx, cy, rx, ry } = this.coordinates;
let { width, height } = this.zoomAdjustedDimensions;
let zoom = getCurrentZoom(this.win);
let clickRadiusX = BASE_MARKER_SIZE / zoom * 100 / width;
let clickRadiusY = BASE_MARKER_SIZE / zoom * 100 / height;
if (clickedOnPoint(pageX, pageY, cx, cy, clickRadiusX, clickRadiusY)) {
return "center";
@@ -670,24 +811,24 @@ class ShapesHighlighter extends AutoRefr
clickedOnPoint(pageX, pageY, cx + rx, cy, clickRadiusX, clickRadiusY)) {
return "radius";
}
return "";
}
/**
- * Check if the center point or rx/ry points of the ellipse highlighter was clicked
+ * Check if the center or rx/ry points of the ellipse highlighter is at given point
* @param {Number} pageX the x coordinate on the page, in % relative to the element
* @param {Number} pageY the y coordinate on the page, in % relative to the element
* @returns {String} "center" if the center point was clicked, "rx" if the x-radius
* point was clicked, "ry" if the y-radius point was clicked,
* "" if none was clicked.
*/
- getEllipseClickedPoint(pageX, pageY) {
+ getEllipsePointAt(pageX, pageY) {
let { cx, cy, rx, ry } = this.coordinates;
let { width, height } = this.zoomAdjustedDimensions;
let zoom = getCurrentZoom(this.win);
let clickRadiusX = BASE_MARKER_SIZE / zoom * 100 / width;
let clickRadiusY = BASE_MARKER_SIZE / zoom * 100 / height;
if (clickedOnPoint(pageX, pageY, cx, cy, clickRadiusX, clickRadiusY)) {
return "center";
@@ -700,23 +841,23 @@ class ShapesHighlighter extends AutoRefr
if (clickedOnPoint(pageX, pageY, cx, cy + ry, clickRadiusX, clickRadiusY)) {
return "ry";
}
return "";
}
/**
- * Check if the edges of the inset highlighter was clicked
+ * Check if the edges of the inset highlighter is at given coords
* @param {Number} pageX the x coordinate on the page, in % relative to the element
* @param {Number} pageY the y coordinate on the page, in % relative to the element
* @returns {String} "top", "left", "right", or "bottom" if any of those edges were
* clicked. "" if none were clicked.
*/
- getInsetClickedPoint(pageX, pageY) {
+ getInsetPointAt(pageX, pageY) {
let { top, left, right, bottom } = this.coordinates;
let zoom = getCurrentZoom(this.win);
let { width, height } = this.zoomAdjustedDimensions;
let clickWidthX = LINE_CLICK_WIDTH * 100 / width;
let clickWidthY = LINE_CLICK_WIDTH * 100 / height;
let clickRadiusX = BASE_MARKER_SIZE / zoom * 100 / width;
let clickRadiusY = BASE_MARKER_SIZE / zoom * 100 / height;
let centerX = (left + (100 - right)) / 2;
@@ -808,33 +949,44 @@ class ShapesHighlighter extends AutoRefr
* Parses the definition of the CSS polygon() function and returns its points,
* converted to percentages.
* @param {String} definition the arguments of the polygon() function
* @returns {Array} an array of the points of the polygon, with all values
* evaluated and converted to percentages
*/
polygonPoints(definition) {
this.coordUnits = this.polygonRawPoints();
- return definition.split(", ").map(coords => {
+ let splitDef = definition.split(", ");
+ if (splitDef[0] === "evenodd" || splitDef[0] === "nonzero") {
+ splitDef.shift();
+ }
+ return splitDef.map(coords => {
return splitCoords(coords).map(this.convertCoordsToPercent.bind(this));
});
}
/**
* Parse the raw (non-computed) definition of the CSS polygon.
* @returns {Array} an array of the points of the polygon, with units preserved.
*/
polygonRawPoints() {
let definition = getDefinedShapeProperties(this.currentNode, this.property);
if (definition === this.rawDefinition) {
return this.coordUnits;
}
this.rawDefinition = definition;
definition = definition.substring(8, definition.lastIndexOf(")"));
- return definition.split(", ").map(coords => {
+ let splitDef = definition.split(", ");
+ if (splitDef[0].includes("evenodd") || splitDef[0].includes("nonzero")) {
+ this.fillRule = splitDef[0].trim();
+ splitDef.shift();
+ } else {
+ this.fillRule = "";
+ }
+ return splitDef.map(coords => {
return splitCoords(coords).map(coord => {
// Undo the insertion of that was done in splitCoords.
return coord.replace(/\u00a0/g, " ");
});
});
}
/**
@@ -1071,16 +1223,17 @@ class ShapesHighlighter extends AutoRefr
this.getElement("polygon").hasAttribute("hidden") &&
this.getElement("rect").hasAttribute("hidden");
}
/**
* Show the highlighter on a given node
*/
_show() {
+ this.hoveredPoint = this.options.hoverPoint;
return this._update();
}
/**
* The AutoRefreshHighlighter's _hasMoved method returns true only if the element's
* quads have changed. Override it so it also returns true if the element's shape has
* changed (which can happen when you change a CSS properties for instance).
*/
@@ -1147,16 +1300,18 @@ class ShapesHighlighter extends AutoRefr
} else if (this.shapeType === "circle") {
this._updateCircleShape(width, height, zoom);
} else if (this.shapeType === "ellipse") {
this._updateEllipseShape(width, height, zoom);
} else if (this.shapeType === "inset") {
this._updateInsetShape(width, height, zoom);
}
+ this._handleMarkerHover(this.hoveredPoint);
+
let { width: winWidth, height: winHeight } = this._winDimensions;
root.removeAttribute("hidden");
root.setAttribute("style",
`position:absolute; width:${winWidth}px;height:${winHeight}px; overflow:hidden`);
setIgnoreLayoutChanges(false, this.highlighterEnv.window.document.documentElement);
return true;
--- a/devtools/shared/specs/highlighters.js
+++ b/devtools/shared/specs/highlighters.js
@@ -33,16 +33,23 @@ const highlighterSpec = generateActorSpe
}
});
exports.highlighterSpec = highlighterSpec;
const customHighlighterSpec = generateActorSpec({
typeName: "customhighlighter",
+ events: {
+ "highlighter-event": {
+ type: "highlighter-event",
+ data: Arg(0, "json")
+ }
+ },
+
methods: {
release: {
release: true
},
show: {
request: {
node: Arg(0, "domnode"),
options: Arg(1, "nullable:json")