tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

ShapesInContextEditor.js (12103B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 "use strict";
      6 
      7 const EventEmitter = require("resource://devtools/shared/event-emitter.js");
      8 const { debounce } = require("resource://devtools/shared/debounce.js");
      9 
     10 /**
     11 * The ShapesInContextEditor:
     12 * - communicates with the ShapesHighlighter actor from the server;
     13 * - listens to events for shape change and hover point coming from the shape-highlighter;
     14 * - writes shape value changes to the CSS declaration it was triggered from;
     15 * - synchronises highlighting coordinate points on mouse over between the shapes
     16 *   highlighter and the shape value shown in the Rule view.
     17 *
     18 * It is instantiated once in HighlightersOverlay by calls to .getInContextEditor().
     19 */
     20 class ShapesInContextEditor {
     21  constructor(highlighter, inspector, state) {
     22    EventEmitter.decorate(this);
     23 
     24    this.inspector = inspector;
     25    this.highlighter = highlighter;
     26    // Refence to the NodeFront currently being highlighted.
     27    this.highlighterTargetNode = null;
     28    this.highligherEventHandlers = {};
     29    this.highligherEventHandlers["shape-change"] = this.onShapeChange;
     30    this.highligherEventHandlers["shape-hover-on"] = this.onShapeHover;
     31    this.highligherEventHandlers["shape-hover-off"] = this.onShapeHover;
     32    // Mode for shapes highlighter: shape-outside or clip-path. Used to discern
     33    // when toggling the highlighter on the same node for different CSS properties.
     34    this.mode = null;
     35    // Reference to Rule view used to listen for changes
     36    this.ruleView = this.inspector.getPanel("ruleview").view;
     37    // Reference of |state| from HighlightersOverlay.
     38    this.state = state;
     39    // Reference to DOM node of the toggle icon for shapes highlighter.
     40    this.swatch = null;
     41 
     42    // Commit triggers expensive DOM changes in TextPropertyEditor.update()
     43    // so we debounce it.
     44    this.commit = debounce(this.commit, 200, this);
     45    this.onHighlighterEvent = this.onHighlighterEvent.bind(this);
     46    this.onNodeFrontChanged = this.onNodeFrontChanged.bind(this);
     47    this.onShapeValueUpdated = this.onShapeValueUpdated.bind(this);
     48    this.onRuleViewChanged = this.onRuleViewChanged.bind(this);
     49 
     50    this.highlighter.on("highlighter-event", this.onHighlighterEvent);
     51    this.ruleView.on("ruleview-changed", this.onRuleViewChanged);
     52  }
     53 
     54  /**
     55   * Get the reference to the TextProperty where shape changes should be written.
     56   *
     57   * We can't rely on the TextProperty to be consistent while changing the value of an
     58   * inline style because the fix for Bug 1467076 forces a full rebuild of TextProperties
     59   * for the inline style's mock-CSS Rule in the Rule view.
     60   *
     61   * On |toggle()|, we store the target TextProperty index, property name and parent rule.
     62   * Here, we use that index and property name to attempt to re-identify the correct
     63   * TextProperty in the rule.
     64   *
     65   * @return {TextProperty|null}
     66   */
     67  get textProperty() {
     68    if (!this.rule || !this.rule.textProps) {
     69      return null;
     70    }
     71 
     72    const textProp = this.rule.textProps[this.textPropIndex];
     73    return textProp && textProp.name === this.textPropName ? textProp : null;
     74  }
     75 
     76  /**
     77   * Called when the element style changes from the Rule view.
     78   * If the TextProperty we're acting on isn't enabled anymore or overridden,
     79   * turn off the shapes highlighter.
     80   */
     81  async onRuleViewChanged() {
     82    if (
     83      this.textProperty &&
     84      (!this.textProperty.enabled || this.textProperty.overridden)
     85    ) {
     86      await this.hide();
     87    }
     88  }
     89 
     90  /**
     91   * Toggle the shapes highlighter for the given element.
     92   *
     93   * @param {NodeFront} node
     94   *        The NodeFront of the element with a shape to highlight.
     95   * @param {object} options
     96   *        Object used for passing options to the shapes highlighter.
     97   */
     98  async toggle(node, options, prop) {
     99    // Same target node, same mode -> hide and exit OR switch to toggle transform mode.
    100    if (node == this.highlighterTargetNode && this.mode === options.mode) {
    101      if (!options.transformMode) {
    102        await this.hide();
    103        return;
    104      }
    105 
    106      options.transformMode = !this.state.shapes.options.transformMode;
    107    }
    108 
    109    // Same target node, dfferent modes -> toggle between shape-outside, clip-path and offset-path.
    110    // Hide highlighter for previous property, but continue and show for other property.
    111    if (node == this.highlighterTargetNode && this.mode !== options.mode) {
    112      await this.hide();
    113    }
    114 
    115    // Save the target TextProperty's parent rule, index and property name for later
    116    // re-identification of the TextProperty. @see |get textProperty()|.
    117    this.rule = prop.rule;
    118    this.textPropIndex = this.rule.textProps.indexOf(prop);
    119    this.textPropName = prop.name;
    120 
    121    this.findSwatch();
    122    await this.show(node, options);
    123  }
    124 
    125  /**
    126   * Show the shapes highlighter for the given element.
    127   *
    128   * @param {NodeFront} node
    129   *        The NodeFront of the element with a shape to highlight.
    130   * @param {object} options
    131   *        Object used for passing options to the shapes highlighter.
    132   */
    133  async show(node, options) {
    134    const isShown = await this.highlighter.show(node, options);
    135    if (!isShown) {
    136      return;
    137    }
    138 
    139    this.inspector.selection.on("detached-front", this.onNodeFrontChanged);
    140    this.inspector.selection.on("new-node-front", this.onNodeFrontChanged);
    141    this.ruleView.on("property-value-updated", this.onShapeValueUpdated);
    142    this.highlighterTargetNode = node;
    143    this.mode = options.mode;
    144    this.emit("show", { node, options });
    145  }
    146 
    147  /**
    148   * Hide the shapes highlighter.
    149   */
    150  async hide() {
    151    try {
    152      await this.highlighter.hide();
    153    } catch (err) {
    154      // silent error
    155    }
    156 
    157    // Stop if the panel has been destroyed during the call to hide.
    158    if (this.destroyed) {
    159      return;
    160    }
    161 
    162    if (this.swatch) {
    163      this.swatch.setAttribute("aria-pressed", false);
    164    }
    165    this.swatch = null;
    166    this.rule = null;
    167    this.textPropIndex = -1;
    168    this.textPropName = null;
    169 
    170    this.emit("hide", { node: this.highlighterTargetNode });
    171    this.inspector.selection.off("detached-front", this.onNodeFrontChanged);
    172    this.inspector.selection.off("new-node-front", this.onNodeFrontChanged);
    173    this.ruleView.off("property-value-updated", this.onShapeValueUpdated);
    174    this.highlighterTargetNode = null;
    175  }
    176 
    177  /**
    178   * Identify the swatch (aka toggle icon) DOM node from the TextPropertyEditor of the
    179   * TextProperty we're working with. Whenever the TextPropertyEditor is updated (i.e.
    180   * when committing the shape value to the Rule view), it rebuilds its DOM and the old
    181   * swatch reference becomes invalid. Call this method to identify the current swatch.
    182   */
    183  findSwatch() {
    184    if (!this.textProperty) {
    185      return;
    186    }
    187 
    188    const valueSpan = this.textProperty.editor.valueSpan;
    189    this.swatch = valueSpan.querySelector(".inspector-shapeswatch");
    190    if (this.swatch) {
    191      this.swatch.setAttribute("aria-pressed", true);
    192    }
    193  }
    194 
    195  /**
    196   * Handle events emitted by the highlighter.
    197   * Find any callback assigned to the event type and call it with the given data object.
    198   *
    199   * @param {object} data
    200   *        The data object sent in the event.
    201   */
    202  onHighlighterEvent(data) {
    203    const handler = this.highligherEventHandlers[data.type];
    204    if (!handler || typeof handler !== "function") {
    205      return;
    206    }
    207    handler.call(this, data);
    208    this.inspector.highlighters.emit("highlighter-event-handled");
    209  }
    210 
    211  /**
    212   * Clean up when node selection changes because Rule view and TextPropertyEditor
    213   * instances are not automatically destroyed when selection changes.
    214   */
    215  async onNodeFrontChanged() {
    216    try {
    217      await this.hide();
    218    } catch (err) {
    219      // Silent error.
    220    }
    221  }
    222 
    223  /**
    224   * Handler for "shape-change" event from the shapes highlighter.
    225   *
    226   * @param  {object} data
    227   *         Data associated with the "shape-change" event.
    228   *         Contains:
    229   *         - {String} value: the new shape value.
    230   *         - {String} type: the event type ("shape-change").
    231   */
    232  onShapeChange(data) {
    233    this.preview(data.value);
    234    this.commit(data.value);
    235  }
    236 
    237  /**
    238   * Handler for "shape-hover-on" and "shape-hover-off" events from the shapes highlighter.
    239   * Called when the mouse moves over or off of a coordinate point inside the shapes
    240   * highlighter. Marks/unmarks the corresponding coordinate node in the shape value
    241   * from the Rule view.
    242   *
    243   * @param  {object} data
    244   *         Data associated with the "shape-hover" event.
    245   *         Contains:
    246   *         - {String|null} point: coordinate to highlight or null if nothing to highlight
    247   *         - {String} type: the event type ("shape-hover-on" or "shape-hover-on").
    248   */
    249  onShapeHover(data) {
    250    const shapeValueEl = this.swatch && this.swatch.nextSibling;
    251    if (!shapeValueEl) {
    252      return;
    253    }
    254 
    255    const pointSelector = ".inspector-shape-point";
    256    // First, unmark all highlighted coordinate nodes from Rule view
    257    for (const node of shapeValueEl.querySelectorAll(
    258      `${pointSelector}.active`
    259    )) {
    260      node.classList.remove("active");
    261    }
    262 
    263    // Exit if there's no coordinate to highlight.
    264    if (typeof data.point !== "string") {
    265      return;
    266    }
    267 
    268    const point = data.point.includes(",")
    269      ? data.point.split(",")[0]
    270      : data.point;
    271 
    272    /**
    273     * Build selector for coordinate nodes in shape value that must be highlighted.
    274     * Coordinate values for inset() use class names instead of data attributes because
    275     * a single node may represent multiple coordinates in shorthand notation.
    276     * Example: inset(50px); The node wrapping 50px represents all four inset coordinates.
    277     */
    278    const INSET_POINT_TYPES = ["top", "right", "bottom", "left"];
    279    const selector = INSET_POINT_TYPES.includes(point)
    280      ? `${pointSelector}.${point}`
    281      : `${pointSelector}[data-point='${point}']`;
    282 
    283    for (const node of shapeValueEl.querySelectorAll(selector)) {
    284      node.classList.add("active");
    285    }
    286  }
    287 
    288  /**
    289   * Handler for "property-value-updated" event triggered by the Rule view.
    290   * Called after the shape value has been written to the element's style and the Rule
    291   * view updated. Emits an event on HighlightersOverlay that is expected by
    292   * tests in order to check if the shape value has been correctly applied.
    293   */
    294  async onShapeValueUpdated() {
    295    if (this.textProperty) {
    296      // When TextPropertyEditor updates, it replaces the previous swatch DOM node.
    297      // Find and store the new one.
    298      this.findSwatch();
    299      this.inspector.highlighters.emit("shapes-highlighter-changes-applied");
    300    } else {
    301      await this.hide();
    302    }
    303  }
    304 
    305  /**
    306   * Preview a shape value on the element without committing the changes to the Rule view.
    307   *
    308   * @param {string} value
    309   *        The shape value to set the current property to
    310   */
    311  preview(value) {
    312    if (!this.textProperty) {
    313      return;
    314    }
    315    // Update the element's style to see live results.
    316    this.textProperty.rule.previewPropertyValue(this.textProperty, value);
    317    // Update the text of CSS value in the Rule view. This makes it inert.
    318    // When commit() is called, the value is reparsed and its DOM structure rebuilt.
    319    this.swatch.nextSibling.textContent = value;
    320  }
    321 
    322  /**
    323   * Commit a shape value change which triggers an expensive operation that rebuilds
    324   * part of the DOM of the TextPropertyEditor. Called in a debounced manner; see
    325   * constructor.
    326   *
    327   * @param {string} value
    328   *        The shape value for the current property
    329   */
    330  commit(value) {
    331    if (!this.textProperty) {
    332      return;
    333    }
    334 
    335    this.textProperty.setValue(value);
    336  }
    337 
    338  destroy() {
    339    this.highlighter.off("highlighter-event", this.onHighlighterEvent);
    340    this.ruleView.off("ruleview-changed", this.onRuleViewChanged);
    341    this.highligherEventHandlers = {};
    342 
    343    this.destroyed = true;
    344  }
    345 }
    346 
    347 module.exports = ShapesInContextEditor;