347 lines
12 KiB
JavaScript
347 lines
12 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 EventEmitter = require("resource://devtools/shared/event-emitter.js");
|
|
const { debounce } = require("resource://devtools/shared/debounce.js");
|
|
|
|
/**
|
|
* The ShapesInContextEditor:
|
|
* - communicates with the ShapesHighlighter actor from the server;
|
|
* - listens to events for shape change and hover point coming from the shape-highlighter;
|
|
* - writes shape value changes to the CSS declaration it was triggered from;
|
|
* - synchronises highlighting coordinate points on mouse over between the shapes
|
|
* highlighter and the shape value shown in the Rule view.
|
|
*
|
|
* It is instantiated once in HighlightersOverlay by calls to .getInContextEditor().
|
|
*/
|
|
class ShapesInContextEditor {
|
|
constructor(highlighter, inspector, state) {
|
|
EventEmitter.decorate(this);
|
|
|
|
this.inspector = inspector;
|
|
this.highlighter = highlighter;
|
|
// Refence to the NodeFront currently being highlighted.
|
|
this.highlighterTargetNode = null;
|
|
this.highligherEventHandlers = {};
|
|
this.highligherEventHandlers["shape-change"] = this.onShapeChange;
|
|
this.highligherEventHandlers["shape-hover-on"] = this.onShapeHover;
|
|
this.highligherEventHandlers["shape-hover-off"] = this.onShapeHover;
|
|
// Mode for shapes highlighter: shape-outside or clip-path. Used to discern
|
|
// when toggling the highlighter on the same node for different CSS properties.
|
|
this.mode = null;
|
|
// Reference to Rule view used to listen for changes
|
|
this.ruleView = this.inspector.getPanel("ruleview").view;
|
|
// Reference of |state| from HighlightersOverlay.
|
|
this.state = state;
|
|
// Reference to DOM node of the toggle icon for shapes highlighter.
|
|
this.swatch = null;
|
|
|
|
// Commit triggers expensive DOM changes in TextPropertyEditor.update()
|
|
// so we debounce it.
|
|
this.commit = debounce(this.commit, 200, this);
|
|
this.onHighlighterEvent = this.onHighlighterEvent.bind(this);
|
|
this.onNodeFrontChanged = this.onNodeFrontChanged.bind(this);
|
|
this.onShapeValueUpdated = this.onShapeValueUpdated.bind(this);
|
|
this.onRuleViewChanged = this.onRuleViewChanged.bind(this);
|
|
|
|
this.highlighter.on("highlighter-event", this.onHighlighterEvent);
|
|
this.ruleView.on("ruleview-changed", this.onRuleViewChanged);
|
|
}
|
|
|
|
/**
|
|
* Get the reference to the TextProperty where shape changes should be written.
|
|
*
|
|
* We can't rely on the TextProperty to be consistent while changing the value of an
|
|
* inline style because the fix for Bug 1467076 forces a full rebuild of TextProperties
|
|
* for the inline style's mock-CSS Rule in the Rule view.
|
|
*
|
|
* On |toggle()|, we store the target TextProperty index, property name and parent rule.
|
|
* Here, we use that index and property name to attempt to re-identify the correct
|
|
* TextProperty in the rule.
|
|
*
|
|
* @return {TextProperty|null}
|
|
*/
|
|
get textProperty() {
|
|
if (!this.rule || !this.rule.textProps) {
|
|
return null;
|
|
}
|
|
|
|
const textProp = this.rule.textProps[this.textPropIndex];
|
|
return textProp && textProp.name === this.textPropName ? textProp : null;
|
|
}
|
|
|
|
/**
|
|
* Called when the element style changes from the Rule view.
|
|
* If the TextProperty we're acting on isn't enabled anymore or overridden,
|
|
* turn off the shapes highlighter.
|
|
*/
|
|
async onRuleViewChanged() {
|
|
if (
|
|
this.textProperty &&
|
|
(!this.textProperty.enabled || this.textProperty.overridden)
|
|
) {
|
|
await this.hide();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Toggle the shapes highlighter for the given element.
|
|
*
|
|
* @param {NodeFront} node
|
|
* The NodeFront of the element with a shape to highlight.
|
|
* @param {Object} options
|
|
* Object used for passing options to the shapes highlighter.
|
|
*/
|
|
async toggle(node, options, prop) {
|
|
// Same target node, same mode -> hide and exit OR switch to toggle transform mode.
|
|
if (node == this.highlighterTargetNode && this.mode === options.mode) {
|
|
if (!options.transformMode) {
|
|
await this.hide();
|
|
return;
|
|
}
|
|
|
|
options.transformMode = !this.state.shapes.options.transformMode;
|
|
}
|
|
|
|
// Same target node, dfferent modes -> toggle between shape-outside, clip-path and offset-path.
|
|
// Hide highlighter for previous property, but continue and show for other property.
|
|
if (node == this.highlighterTargetNode && this.mode !== options.mode) {
|
|
await this.hide();
|
|
}
|
|
|
|
// Save the target TextProperty's parent rule, index and property name for later
|
|
// re-identification of the TextProperty. @see |get textProperty()|.
|
|
this.rule = prop.rule;
|
|
this.textPropIndex = this.rule.textProps.indexOf(prop);
|
|
this.textPropName = prop.name;
|
|
|
|
this.findSwatch();
|
|
await this.show(node, options);
|
|
}
|
|
|
|
/**
|
|
* Show the shapes highlighter for the given element.
|
|
*
|
|
* @param {NodeFront} node
|
|
* The NodeFront of the element with a shape to highlight.
|
|
* @param {Object} options
|
|
* Object used for passing options to the shapes highlighter.
|
|
*/
|
|
async show(node, options) {
|
|
const isShown = await this.highlighter.show(node, options);
|
|
if (!isShown) {
|
|
return;
|
|
}
|
|
|
|
this.inspector.selection.on("detached-front", this.onNodeFrontChanged);
|
|
this.inspector.selection.on("new-node-front", this.onNodeFrontChanged);
|
|
this.ruleView.on("property-value-updated", this.onShapeValueUpdated);
|
|
this.highlighterTargetNode = node;
|
|
this.mode = options.mode;
|
|
this.emit("show", { node, options });
|
|
}
|
|
|
|
/**
|
|
* Hide the shapes highlighter.
|
|
*/
|
|
async hide() {
|
|
try {
|
|
await this.highlighter.hide();
|
|
} catch (err) {
|
|
// silent error
|
|
}
|
|
|
|
// Stop if the panel has been destroyed during the call to hide.
|
|
if (this.destroyed) {
|
|
return;
|
|
}
|
|
|
|
if (this.swatch) {
|
|
this.swatch.setAttribute("aria-pressed", false);
|
|
}
|
|
this.swatch = null;
|
|
this.rule = null;
|
|
this.textPropIndex = -1;
|
|
this.textPropName = null;
|
|
|
|
this.emit("hide", { node: this.highlighterTargetNode });
|
|
this.inspector.selection.off("detached-front", this.onNodeFrontChanged);
|
|
this.inspector.selection.off("new-node-front", this.onNodeFrontChanged);
|
|
this.ruleView.off("property-value-updated", this.onShapeValueUpdated);
|
|
this.highlighterTargetNode = null;
|
|
}
|
|
|
|
/**
|
|
* Identify the swatch (aka toggle icon) DOM node from the TextPropertyEditor of the
|
|
* TextProperty we're working with. Whenever the TextPropertyEditor is updated (i.e.
|
|
* when committing the shape value to the Rule view), it rebuilds its DOM and the old
|
|
* swatch reference becomes invalid. Call this method to identify the current swatch.
|
|
*/
|
|
findSwatch() {
|
|
if (!this.textProperty) {
|
|
return;
|
|
}
|
|
|
|
const valueSpan = this.textProperty.editor.valueSpan;
|
|
this.swatch = valueSpan.querySelector(".inspector-shapeswatch");
|
|
if (this.swatch) {
|
|
this.swatch.setAttribute("aria-pressed", true);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle events emitted by the highlighter.
|
|
* Find any callback assigned to the event type and call it with the given data object.
|
|
*
|
|
* @param {Object} data
|
|
* The data object sent in the event.
|
|
*/
|
|
onHighlighterEvent(data) {
|
|
const handler = this.highligherEventHandlers[data.type];
|
|
if (!handler || typeof handler !== "function") {
|
|
return;
|
|
}
|
|
handler.call(this, data);
|
|
this.inspector.highlighters.emit("highlighter-event-handled");
|
|
}
|
|
|
|
/**
|
|
* Clean up when node selection changes because Rule view and TextPropertyEditor
|
|
* instances are not automatically destroyed when selection changes.
|
|
*/
|
|
async onNodeFrontChanged() {
|
|
try {
|
|
await this.hide();
|
|
} catch (err) {
|
|
// Silent error.
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handler for "shape-change" event from the shapes highlighter.
|
|
*
|
|
* @param {Object} data
|
|
* Data associated with the "shape-change" event.
|
|
* Contains:
|
|
* - {String} value: the new shape value.
|
|
* - {String} type: the event type ("shape-change").
|
|
*/
|
|
onShapeChange(data) {
|
|
this.preview(data.value);
|
|
this.commit(data.value);
|
|
}
|
|
|
|
/**
|
|
* Handler for "shape-hover-on" and "shape-hover-off" events from the shapes highlighter.
|
|
* Called when the mouse moves over or off of a coordinate point inside the shapes
|
|
* highlighter. Marks/unmarks the corresponding coordinate node in the shape value
|
|
* from the Rule view.
|
|
*
|
|
* @param {Object} data
|
|
* Data associated with the "shape-hover" event.
|
|
* Contains:
|
|
* - {String|null} point: coordinate to highlight or null if nothing to highlight
|
|
* - {String} type: the event type ("shape-hover-on" or "shape-hover-on").
|
|
*/
|
|
onShapeHover(data) {
|
|
const shapeValueEl = this.swatch && this.swatch.nextSibling;
|
|
if (!shapeValueEl) {
|
|
return;
|
|
}
|
|
|
|
const pointSelector = ".inspector-shape-point";
|
|
// First, unmark all highlighted coordinate nodes from Rule view
|
|
for (const node of shapeValueEl.querySelectorAll(
|
|
`${pointSelector}.active`
|
|
)) {
|
|
node.classList.remove("active");
|
|
}
|
|
|
|
// Exit if there's no coordinate to highlight.
|
|
if (typeof data.point !== "string") {
|
|
return;
|
|
}
|
|
|
|
const point = data.point.includes(",")
|
|
? data.point.split(",")[0]
|
|
: data.point;
|
|
|
|
/**
|
|
* Build selector for coordinate nodes in shape value that must be highlighted.
|
|
* Coordinate values for inset() use class names instead of data attributes because
|
|
* a single node may represent multiple coordinates in shorthand notation.
|
|
* Example: inset(50px); The node wrapping 50px represents all four inset coordinates.
|
|
*/
|
|
const INSET_POINT_TYPES = ["top", "right", "bottom", "left"];
|
|
const selector = INSET_POINT_TYPES.includes(point)
|
|
? `${pointSelector}.${point}`
|
|
: `${pointSelector}[data-point='${point}']`;
|
|
|
|
for (const node of shapeValueEl.querySelectorAll(selector)) {
|
|
node.classList.add("active");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handler for "property-value-updated" event triggered by the Rule view.
|
|
* Called after the shape value has been written to the element's style and the Rule
|
|
* view updated. Emits an event on HighlightersOverlay that is expected by
|
|
* tests in order to check if the shape value has been correctly applied.
|
|
*/
|
|
async onShapeValueUpdated() {
|
|
if (this.textProperty) {
|
|
// When TextPropertyEditor updates, it replaces the previous swatch DOM node.
|
|
// Find and store the new one.
|
|
this.findSwatch();
|
|
this.inspector.highlighters.emit("shapes-highlighter-changes-applied");
|
|
} else {
|
|
await this.hide();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Preview a shape value on the element without committing the changes to the Rule view.
|
|
*
|
|
* @param {String} value
|
|
* The shape value to set the current property to
|
|
*/
|
|
preview(value) {
|
|
if (!this.textProperty) {
|
|
return;
|
|
}
|
|
// Update the element's style to see live results.
|
|
this.textProperty.rule.previewPropertyValue(this.textProperty, value);
|
|
// Update the text of CSS value in the Rule view. This makes it inert.
|
|
// When commit() is called, the value is reparsed and its DOM structure rebuilt.
|
|
this.swatch.nextSibling.textContent = value;
|
|
}
|
|
|
|
/**
|
|
* Commit a shape value change which triggers an expensive operation that rebuilds
|
|
* part of the DOM of the TextPropertyEditor. Called in a debounced manner; see
|
|
* constructor.
|
|
*
|
|
* @param {String} value
|
|
* The shape value for the current property
|
|
*/
|
|
commit(value) {
|
|
if (!this.textProperty) {
|
|
return;
|
|
}
|
|
|
|
this.textProperty.setValue(value);
|
|
}
|
|
|
|
destroy() {
|
|
this.highlighter.off("highlighter-event", this.onHighlighterEvent);
|
|
this.ruleView.off("ruleview-changed", this.onRuleViewChanged);
|
|
this.highligherEventHandlers = {};
|
|
|
|
this.destroyed = true;
|
|
}
|
|
}
|
|
|
|
module.exports = ShapesInContextEditor;
|