tor-browser

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

box-model.js (13176B)


      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 boxModelReducer = require("resource://devtools/client/inspector/boxmodel/reducers/box-model.js");
      8 const {
      9  updateGeometryEditorEnabled,
     10  updateLayout,
     11  updateOffsetParent,
     12 } = require("resource://devtools/client/inspector/boxmodel/actions/box-model.js");
     13 
     14 loader.lazyRequireGetter(
     15  this,
     16  "EditingSession",
     17  "resource://devtools/client/inspector/boxmodel/utils/editing-session.js"
     18 );
     19 loader.lazyRequireGetter(
     20  this,
     21  "InplaceEditor",
     22  "resource://devtools/client/shared/inplace-editor.js",
     23  true
     24 );
     25 loader.lazyRequireGetter(
     26  this,
     27  "RulePreviewTooltip",
     28  "resource://devtools/client/shared/widgets/tooltip/RulePreviewTooltip.js"
     29 );
     30 
     31 const NUMERIC = /^-?[\d\.]+$/;
     32 
     33 /**
     34 * A singleton instance of the box model controllers.
     35 */
     36 class BoxModel {
     37  /**
     38   * @param  {Inspector} inspector
     39   *         An instance of the Inspector currently loaded in the toolbox.
     40   * @param  {Window} window
     41   *         The document window of the toolbox.
     42   */
     43  constructor(inspector, window) {
     44    this.document = window.document;
     45    this.inspector = inspector;
     46    this.store = inspector.store;
     47 
     48    this.store.injectReducer("boxModel", boxModelReducer);
     49 
     50    this.updateBoxModel = this.updateBoxModel.bind(this);
     51 
     52    this.onHideGeometryEditor = this.onHideGeometryEditor.bind(this);
     53    this.onMarkupViewLeave = this.onMarkupViewLeave.bind(this);
     54    this.onMarkupViewNodeHover = this.onMarkupViewNodeHover.bind(this);
     55    this.onNewSelection = this.onNewSelection.bind(this);
     56    this.onShowBoxModelEditor = this.onShowBoxModelEditor.bind(this);
     57    this.onShowRulePreviewTooltip = this.onShowRulePreviewTooltip.bind(this);
     58    this.onSidebarSelect = this.onSidebarSelect.bind(this);
     59    this.onToggleGeometryEditor = this.onToggleGeometryEditor.bind(this);
     60 
     61    this.inspector.selection.on("new-node-front", this.onNewSelection);
     62    this.inspector.sidebar.on("select", this.onSidebarSelect);
     63  }
     64  /**
     65   * Destruction function called when the inspector is destroyed. Removes event listeners
     66   * and cleans up references.
     67   */
     68  destroy() {
     69    this.inspector.selection.off("new-node-front", this.onNewSelection);
     70    this.inspector.sidebar.off("select", this.onSidebarSelect);
     71 
     72    if (this._geometryEditorEventsAbortController) {
     73      this._geometryEditorEventsAbortController.abort();
     74      this._geometryEditorEventsAbortController = null;
     75    }
     76 
     77    if (this._tooltip) {
     78      this._tooltip.destroy();
     79    }
     80 
     81    this.untrackReflows();
     82 
     83    this.elementRules = null;
     84    this._highlighters = null;
     85    this._tooltip = null;
     86    this.document = null;
     87    this.inspector = null;
     88  }
     89 
     90  get highlighters() {
     91    if (!this._highlighters) {
     92      // highlighters is a lazy getter in the inspector.
     93      this._highlighters = this.inspector.highlighters;
     94    }
     95 
     96    return this._highlighters;
     97  }
     98 
     99  get rulePreviewTooltip() {
    100    if (!this._tooltip) {
    101      this._tooltip = new RulePreviewTooltip(this.inspector.toolbox.doc);
    102    }
    103 
    104    return this._tooltip;
    105  }
    106 
    107  /**
    108   * Returns an object containing the box model's handler functions used in the box
    109   * model's React component props.
    110   */
    111  getComponentProps() {
    112    return {
    113      onShowBoxModelEditor: this.onShowBoxModelEditor,
    114      onShowRulePreviewTooltip: this.onShowRulePreviewTooltip,
    115      onToggleGeometryEditor: this.onToggleGeometryEditor,
    116    };
    117  }
    118 
    119  /**
    120   * Returns true if the layout panel is visible, and false otherwise.
    121   */
    122  isPanelVisible() {
    123    return (
    124      this.inspector.toolbox &&
    125      this.inspector.sidebar &&
    126      this.inspector.toolbox.currentToolId === "inspector" &&
    127      this.inspector.sidebar.getCurrentTabID() === "layoutview"
    128    );
    129  }
    130 
    131  /**
    132   * Returns true if the layout panel is visible and the current element is valid to
    133   * be displayed in the view.
    134   */
    135  isPanelVisibleAndNodeValid() {
    136    return (
    137      this.isPanelVisible() &&
    138      this.inspector.selection.isConnected() &&
    139      this.inspector.selection.isElementNode()
    140    );
    141  }
    142 
    143  /**
    144   * Starts listening to reflows in the current tab.
    145   */
    146  trackReflows() {
    147    this.inspector.on("reflow-in-selected-target", this.updateBoxModel);
    148  }
    149 
    150  /**
    151   * Stops listening to reflows in the current tab.
    152   */
    153  untrackReflows() {
    154    this.inspector.off("reflow-in-selected-target", this.updateBoxModel);
    155  }
    156 
    157  /**
    158   * Updates the box model panel by dispatching the new layout data.
    159   *
    160   * @param  {string} reason
    161   *         Optional string describing the reason why the boxmodel is updated.
    162   */
    163  updateBoxModel(reason) {
    164    this._updateReasons = this._updateReasons || [];
    165    if (reason) {
    166      this._updateReasons.push(reason);
    167    }
    168 
    169    const lastRequest = async function () {
    170      if (
    171        !this.inspector ||
    172        !this.isPanelVisible() ||
    173        !this.inspector.selection.isConnected() ||
    174        !this.inspector.selection.isElementNode()
    175      ) {
    176        return null;
    177      }
    178 
    179      const { nodeFront } = this.inspector.selection;
    180      const inspectorFront = this.getCurrentInspectorFront();
    181      const { pageStyle } = inspectorFront;
    182 
    183      let layout = await pageStyle.getLayout(nodeFront, {
    184        autoMargins: true,
    185      });
    186 
    187      const styleEntries = await pageStyle.getApplied(nodeFront, {
    188        // We don't need styles applied to pseudo elements of the current node.
    189        skipPseudo: true,
    190      });
    191      this.elementRules = styleEntries.map(e => e.rule);
    192 
    193      // Update the layout properties with whether or not the element's position is
    194      // editable with the geometry editor.
    195      const isPositionEditable = await pageStyle.isPositionEditable(nodeFront);
    196 
    197      layout = Object.assign({}, layout, {
    198        isPositionEditable,
    199      });
    200 
    201      // Update the redux store with the latest offset parent DOM node
    202      const offsetParent =
    203        await inspectorFront.walker.getOffsetParent(nodeFront);
    204      this.store.dispatch(updateOffsetParent(offsetParent));
    205 
    206      // Update the redux store with the latest layout properties and update the box
    207      // model view.
    208      this.store.dispatch(updateLayout(layout));
    209 
    210      // If a subsequent request has been made, wait for that one instead.
    211      if (this._lastRequest != lastRequest) {
    212        return this._lastRequest;
    213      }
    214 
    215      this.inspector.emit("boxmodel-view-updated", this._updateReasons);
    216 
    217      this._lastRequest = null;
    218      this._updateReasons = [];
    219 
    220      return null;
    221    }
    222      .bind(this)()
    223      .catch(error => {
    224        // If we failed because we were being destroyed while waiting for a request, ignore.
    225        if (this.document) {
    226          console.error(error);
    227        }
    228      });
    229 
    230    this._lastRequest = lastRequest;
    231  }
    232 
    233  /**
    234   * Hides the geometry editor and updates the box moodel store with the new
    235   * geometry editor enabled state.
    236   */
    237  onHideGeometryEditor() {
    238    this.highlighters.hideGeometryEditor();
    239    this.store.dispatch(updateGeometryEditorEnabled(false));
    240 
    241    if (this._geometryEditorEventsAbortController) {
    242      this._geometryEditorEventsAbortController.abort();
    243      this._geometryEditorEventsAbortController = null;
    244    }
    245  }
    246 
    247  /**
    248   * Handler function that re-shows the geometry editor for an element that already
    249   * had the geometry editor enabled. This handler function is called on a "leave" event
    250   * on the markup view.
    251   */
    252  onMarkupViewLeave() {
    253    const state = this.store.getState();
    254    const enabled = state.boxModel.geometryEditorEnabled;
    255 
    256    if (!enabled) {
    257      return;
    258    }
    259 
    260    const nodeFront = this.inspector.selection.nodeFront;
    261    this.highlighters.showGeometryEditor(nodeFront);
    262  }
    263 
    264  /**
    265   * Handler function that temporarily hides the geomery editor when the
    266   * markup view has a "node-hover" event.
    267   */
    268  onMarkupViewNodeHover() {
    269    this.highlighters.hideGeometryEditor();
    270  }
    271 
    272  /**
    273   * Selection 'new-node-front' event handler.
    274   */
    275  onNewSelection() {
    276    if (!this.isPanelVisibleAndNodeValid()) {
    277      return;
    278    }
    279 
    280    if (
    281      this.inspector.selection.isConnected() &&
    282      this.inspector.selection.isElementNode()
    283    ) {
    284      this.trackReflows();
    285    }
    286 
    287    this.updateBoxModel("new-selection");
    288  }
    289 
    290  /**
    291   * Shows the RulePreviewTooltip when a box model editable value is hovered on the
    292   * box model panel.
    293   *
    294   * @param  {Element} target
    295   *         The target element.
    296   * @param  {string} property
    297   *         The name of the property.
    298   */
    299  onShowRulePreviewTooltip(target, property) {
    300    const { highlightProperty } = this.inspector.getPanel("ruleview").view;
    301    const isHighlighted = highlightProperty(property);
    302 
    303    // Only show the tooltip if the property is not highlighted.
    304    // TODO: In the future, use an associated ruleId for toggling the tooltip instead of
    305    // the Boolean returned from highlightProperty.
    306    if (!isHighlighted) {
    307      this.rulePreviewTooltip.show(target);
    308    }
    309  }
    310 
    311  /**
    312   * Shows the inplace editor when a box model editable value is clicked on the
    313   * box model panel.
    314   *
    315   * @param  {DOMNode} element
    316   *         The element that was clicked.
    317   * @param  {Event} event
    318   *         The event object.
    319   * @param  {string} property
    320   *         The name of the property.
    321   */
    322  onShowBoxModelEditor(element, event, property) {
    323    const session = new EditingSession({
    324      inspector: this.inspector,
    325      doc: this.document,
    326      elementRules: this.elementRules,
    327    });
    328    const initialValue = session.getProperty(property);
    329 
    330    const editor = new InplaceEditor(
    331      {
    332        element,
    333        initial: initialValue,
    334        contentType: InplaceEditor.CONTENT_TYPES.CSS_VALUE,
    335        property: {
    336          name: property,
    337        },
    338        start: self => {
    339          self.elt.parentNode.classList.add("boxmodel-editing");
    340        },
    341        change: value => {
    342          if (NUMERIC.test(value)) {
    343            value += "px";
    344          }
    345 
    346          const properties = [{ name: property, value }];
    347 
    348          if (property.substring(0, 7) == "border-") {
    349            const bprop = property.substring(0, property.length - 5) + "style";
    350            const style = session.getProperty(bprop);
    351            if (!style || style == "none" || style == "hidden") {
    352              properties.push({ name: bprop, value: "solid" });
    353            }
    354          }
    355 
    356          if (property.substring(0, 9) == "position-") {
    357            properties[0].name = property.substring(9);
    358          }
    359 
    360          session.setProperties(properties).catch(console.error);
    361        },
    362        done: (value, commit) => {
    363          editor.elt.parentNode.classList.remove("boxmodel-editing");
    364          if (!commit) {
    365            session.revert().then(() => {
    366              session.destroy();
    367            }, console.error);
    368            return;
    369          }
    370 
    371          this.updateBoxModel("editable-value-change");
    372        },
    373        cssProperties: this.inspector.cssProperties,
    374      },
    375      event
    376    );
    377  }
    378 
    379  /**
    380   * Handler for the inspector sidebar select event. Starts tracking reflows if the
    381   * layout panel is visible. Otherwise, stop tracking reflows. Finally, refresh the box
    382   * model view if it is visible.
    383   */
    384  onSidebarSelect() {
    385    if (!this.isPanelVisible()) {
    386      this.untrackReflows();
    387      return;
    388    }
    389 
    390    if (
    391      this.inspector.selection.isConnected() &&
    392      this.inspector.selection.isElementNode()
    393    ) {
    394      this.trackReflows();
    395    }
    396 
    397    this.updateBoxModel();
    398  }
    399 
    400  /**
    401   * Toggles on/off the geometry editor for the current element when the geometry editor
    402   * toggle button is clicked.
    403   */
    404  onToggleGeometryEditor() {
    405    const { markup, selection, toolbox } = this.inspector;
    406    const nodeFront = this.inspector.selection.nodeFront;
    407    const state = this.store.getState();
    408    const enabled = !state.boxModel.geometryEditorEnabled;
    409 
    410    this.highlighters.toggleGeometryHighlighter(nodeFront);
    411    this.store.dispatch(updateGeometryEditorEnabled(enabled));
    412 
    413    if (enabled) {
    414      this._geometryEditorEventsAbortController = new AbortController();
    415      const eventListenersConfig = {
    416        signal: this._geometryEditorEventsAbortController.signal,
    417      };
    418      // Hide completely the geometry editor if:
    419      // - the picker is clicked
    420      // - or if a new node is selected
    421      toolbox.nodePicker.on(
    422        "picker-started",
    423        this.onHideGeometryEditor,
    424        eventListenersConfig
    425      );
    426      selection.on(
    427        "new-node-front",
    428        this.onHideGeometryEditor,
    429        eventListenersConfig
    430      );
    431      // Temporarily hide the geometry editor
    432      markup.on("leave", this.onMarkupViewLeave, eventListenersConfig);
    433      markup.on("node-hover", this.onMarkupViewNodeHover, eventListenersConfig);
    434    } else if (this._geometryEditorEventsAbortController) {
    435      this._geometryEditorEventsAbortController.abort();
    436      this._geometryEditorEventsAbortController = null;
    437    }
    438  }
    439 
    440  getCurrentInspectorFront() {
    441    return this.inspector.selection.nodeFront.inspectorFront;
    442  }
    443 }
    444 
    445 module.exports = BoxModel;