diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
commit | 6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /devtools/client/shared/widgets/ShapesInContextEditor.js | |
parent | Initial commit. (diff) | |
download | thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.tar.xz thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.zip |
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'devtools/client/shared/widgets/ShapesInContextEditor.js')
-rw-r--r-- | devtools/client/shared/widgets/ShapesInContextEditor.js | 347 |
1 files changed, 347 insertions, 0 deletions
diff --git a/devtools/client/shared/widgets/ShapesInContextEditor.js b/devtools/client/shared/widgets/ShapesInContextEditor.js new file mode 100644 index 0000000000..1d794bd81f --- /dev/null +++ b/devtools/client/shared/widgets/ShapesInContextEditor.js @@ -0,0 +1,347 @@ +/* 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 and clip-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.classList.remove("active"); + } + 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(".ruleview-shapeswatch"); + if (this.swatch) { + this.swatch.classList.add("active"); + } + } + + /** + * 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 = ".ruleview-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; |