tor-browser

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

BoxModelMain.js (22483B)


      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 {
      8  createFactory,
      9  PureComponent,
     10 } = require("resource://devtools/client/shared/vendor/react.mjs");
     11 const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
     12 const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.mjs");
     13 const { KeyCodes } = require("resource://devtools/client/shared/keycodes.js");
     14 const { LocalizationHelper } = require("resource://devtools/shared/l10n.js");
     15 
     16 const BoxModelEditable = createFactory(
     17  require("resource://devtools/client/inspector/boxmodel/components/BoxModelEditable.js")
     18 );
     19 
     20 const Types = require("resource://devtools/client/inspector/boxmodel/types.js");
     21 
     22 const {
     23  highlightSelectedNode,
     24  unhighlightNode,
     25 } = require("resource://devtools/client/inspector/boxmodel/actions/box-model-highlighter.js");
     26 
     27 const SHARED_STRINGS_URI = "devtools/client/locales/shared.properties";
     28 const SHARED_L10N = new LocalizationHelper(SHARED_STRINGS_URI);
     29 
     30 class BoxModelMain extends PureComponent {
     31  static get propTypes() {
     32    return {
     33      boxModel: PropTypes.shape(Types.boxModel).isRequired,
     34      boxModelContainer: PropTypes.object,
     35      dispatch: PropTypes.func.isRequired,
     36      onShowBoxModelEditor: PropTypes.func.isRequired,
     37      onShowRulePreviewTooltip: PropTypes.func.isRequired,
     38    };
     39  }
     40 
     41  constructor(props) {
     42    super(props);
     43 
     44    this.state = {
     45      activeDescendant: null,
     46      focusable: false,
     47    };
     48 
     49    this.getActiveDescendant = this.getActiveDescendant.bind(this);
     50    this.getBorderOrPaddingValue = this.getBorderOrPaddingValue.bind(this);
     51    this.getContextBox = this.getContextBox.bind(this);
     52    this.getDisplayPosition = this.getDisplayPosition.bind(this);
     53    this.getHeightValue = this.getHeightValue.bind(this);
     54    this.getMarginValue = this.getMarginValue.bind(this);
     55    this.getPositionValue = this.getPositionValue.bind(this);
     56    this.getWidthValue = this.getWidthValue.bind(this);
     57    this.moveFocus = this.moveFocus.bind(this);
     58    this.onHighlightMouseOver = this.onHighlightMouseOver.bind(this);
     59    this.onKeyDown = this.onKeyDown.bind(this);
     60    this.onLevelClick = this.onLevelClick.bind(this);
     61    this.setActive = this.setActive.bind(this);
     62  }
     63 
     64  componentDidUpdate() {
     65    const displayPosition = this.getDisplayPosition();
     66    const isContentBox = this.getContextBox();
     67 
     68    this.layouts = {
     69      position: new Map([
     70        [KeyCodes.DOM_VK_ESCAPE, this.positionLayout],
     71        [KeyCodes.DOM_VK_DOWN, this.marginLayout],
     72        [KeyCodes.DOM_VK_RETURN, this.positionEditable],
     73        [KeyCodes.DOM_VK_UP, null],
     74        ["click", this.positionLayout],
     75      ]),
     76      margin: new Map([
     77        [KeyCodes.DOM_VK_ESCAPE, this.marginLayout],
     78        [KeyCodes.DOM_VK_DOWN, this.borderLayout],
     79        [KeyCodes.DOM_VK_RETURN, this.marginEditable],
     80        [KeyCodes.DOM_VK_UP, displayPosition ? this.positionLayout : null],
     81        ["click", this.marginLayout],
     82      ]),
     83      border: new Map([
     84        [KeyCodes.DOM_VK_ESCAPE, this.borderLayout],
     85        [KeyCodes.DOM_VK_DOWN, this.paddingLayout],
     86        [KeyCodes.DOM_VK_RETURN, this.borderEditable],
     87        [KeyCodes.DOM_VK_UP, this.marginLayout],
     88        ["click", this.borderLayout],
     89      ]),
     90      padding: new Map([
     91        [KeyCodes.DOM_VK_ESCAPE, this.paddingLayout],
     92        [KeyCodes.DOM_VK_DOWN, isContentBox ? this.contentLayout : null],
     93        [KeyCodes.DOM_VK_RETURN, this.paddingEditable],
     94        [KeyCodes.DOM_VK_UP, this.borderLayout],
     95        ["click", this.paddingLayout],
     96      ]),
     97      content: new Map([
     98        [KeyCodes.DOM_VK_ESCAPE, this.contentLayout],
     99        [KeyCodes.DOM_VK_DOWN, null],
    100        [KeyCodes.DOM_VK_RETURN, this.contentEditable],
    101        [KeyCodes.DOM_VK_UP, this.paddingLayout],
    102        ["click", this.contentLayout],
    103      ]),
    104    };
    105  }
    106 
    107  getActiveDescendant() {
    108    let { activeDescendant } = this.state;
    109 
    110    if (!activeDescendant) {
    111      const displayPosition = this.getDisplayPosition();
    112      const nextLayout = displayPosition
    113        ? this.positionLayout
    114        : this.marginLayout;
    115      activeDescendant = nextLayout.getAttribute("data-box");
    116      this.setActive(nextLayout);
    117    }
    118 
    119    return activeDescendant;
    120  }
    121 
    122  getBorderOrPaddingValue(property) {
    123    const { layout } = this.props.boxModel;
    124    return layout[property] ? parseFloat(layout[property]) : "-";
    125  }
    126 
    127  /**
    128   * Returns true if the layout box sizing is context box and false otherwise.
    129   */
    130  getContextBox() {
    131    const { layout } = this.props.boxModel;
    132    return layout["box-sizing"] == "content-box";
    133  }
    134 
    135  /**
    136   * Returns true if the position is displayed and false otherwise.
    137   */
    138  getDisplayPosition() {
    139    const { layout } = this.props.boxModel;
    140    return layout.position && layout.position != "static";
    141  }
    142 
    143  getHeightValue(property) {
    144    if (property == undefined) {
    145      return "-";
    146    }
    147 
    148    const { layout } = this.props.boxModel;
    149 
    150    property -=
    151      parseFloat(layout["border-top-width"]) +
    152      parseFloat(layout["border-bottom-width"]) +
    153      parseFloat(layout["padding-top"]) +
    154      parseFloat(layout["padding-bottom"]);
    155    property = parseFloat(property.toPrecision(6));
    156 
    157    return property >= 0 ? property : "auto";
    158  }
    159 
    160  getMarginValue(property, direction) {
    161    const { layout } = this.props.boxModel;
    162    const autoMargins = layout.autoMargins || {};
    163    let value = "-";
    164 
    165    if (direction in autoMargins) {
    166      value = autoMargins[direction];
    167    } else if (layout[property]) {
    168      const parsedValue = parseFloat(layout[property]);
    169 
    170      if (Number.isNaN(parsedValue)) {
    171        // Not a number. We use the raw string.
    172        // Useful for pseudo-elements with auto margins since they
    173        // don't appear in autoMargins.
    174        value = layout[property];
    175      } else {
    176        value = parsedValue;
    177      }
    178    }
    179 
    180    return value;
    181  }
    182 
    183  getPositionValue(property) {
    184    const { layout } = this.props.boxModel;
    185    let value = "-";
    186 
    187    if (!layout[property]) {
    188      return value;
    189    }
    190 
    191    const parsedValue = parseFloat(layout[property]);
    192 
    193    if (Number.isNaN(parsedValue)) {
    194      // Not a number. We use the raw string.
    195      value = layout[property];
    196    } else {
    197      value = parsedValue;
    198    }
    199 
    200    return value;
    201  }
    202 
    203  getWidthValue(property) {
    204    if (property == undefined) {
    205      return "-";
    206    }
    207 
    208    const { layout } = this.props.boxModel;
    209 
    210    property -=
    211      parseFloat(layout["border-left-width"]) +
    212      parseFloat(layout["border-right-width"]) +
    213      parseFloat(layout["padding-left"]) +
    214      parseFloat(layout["padding-right"]);
    215    property = parseFloat(property.toPrecision(6));
    216 
    217    return property >= 0 ? property : "auto";
    218  }
    219 
    220  /**
    221   * Move the focus to the next/previous editable element of the current layout.
    222   *
    223   * @param  {Element} target
    224   *         Node to be observed
    225   * @param  {boolean} shiftKey
    226   *         Determines if shiftKey was pressed
    227   */
    228  moveFocus({ target, shiftKey }) {
    229    const editBoxes = [
    230      ...this.positionLayout.querySelectorAll("[data-box].boxmodel-editable"),
    231    ];
    232    const editingMode = target.tagName === "input";
    233    // target.nextSibling is input field
    234    let position = editingMode
    235      ? editBoxes.indexOf(target.nextSibling)
    236      : editBoxes.indexOf(target);
    237 
    238    if (position === editBoxes.length - 1 && !shiftKey) {
    239      position = 0;
    240    } else if (position === 0 && shiftKey) {
    241      position = editBoxes.length - 1;
    242    } else {
    243      shiftKey ? position-- : position++;
    244    }
    245 
    246    const editBox = editBoxes[position];
    247    this.setActive(editBox);
    248    editBox.focus();
    249 
    250    if (editingMode) {
    251      editBox.click();
    252    }
    253  }
    254 
    255  /**
    256   * Active level set to current layout.
    257   *
    258   * @param  {Element} nextLayout
    259   *         Element of next layout that user has navigated to
    260   */
    261  setActive(nextLayout) {
    262    const { boxModelContainer } = this.props;
    263 
    264    // We set this attribute for testing purposes.
    265    if (boxModelContainer) {
    266      boxModelContainer.dataset.activeDescendantClassName =
    267        nextLayout.className;
    268    }
    269 
    270    this.setState({
    271      activeDescendant: nextLayout.getAttribute("data-box"),
    272    });
    273  }
    274 
    275  onHighlightMouseOver(event) {
    276    let region = event.target.getAttribute("data-box");
    277 
    278    if (!region) {
    279      let el = event.target;
    280 
    281      do {
    282        el = el.parentNode;
    283 
    284        if (el && el.getAttribute("data-box")) {
    285          region = el.getAttribute("data-box");
    286          break;
    287        }
    288      } while (el.parentNode);
    289 
    290      this.props.dispatch(unhighlightNode());
    291    }
    292 
    293    this.props.dispatch(
    294      highlightSelectedNode({
    295        region,
    296        showOnly: region,
    297        onlyRegionArea: true,
    298      })
    299    );
    300 
    301    event.preventDefault();
    302  }
    303 
    304  /**
    305   * Handle keyboard navigation and focus for box model layouts.
    306   *
    307   * Updates active layout on arrow key navigation
    308   * Focuses next layout's editboxes on enter key
    309   * Unfocuses current layout's editboxes when active layout changes
    310   * Controls tabbing between editBoxes
    311   *
    312   * @param  {Event} event
    313   *         The event triggered by a keypress on the box model
    314   */
    315  onKeyDown(event) {
    316    const { target, keyCode } = event;
    317    const isEditable = target._editable || target.editor;
    318 
    319    const level = this.getActiveDescendant();
    320    const editingMode = target.tagName === "input";
    321 
    322    switch (keyCode) {
    323      case KeyCodes.DOM_VK_RETURN:
    324        if (!isEditable) {
    325          this.setState({ focusable: true }, () => {
    326            const editableBox = this.layouts[level].get(keyCode);
    327            if (editableBox) {
    328              editableBox.boxModelEditable.focus();
    329            }
    330          });
    331        }
    332        break;
    333      case KeyCodes.DOM_VK_DOWN:
    334      case KeyCodes.DOM_VK_UP:
    335        if (!editingMode) {
    336          event.preventDefault();
    337          event.stopPropagation();
    338          this.setState({ focusable: false }, () => {
    339            const nextLayout = this.layouts[level].get(keyCode);
    340 
    341            if (!nextLayout) {
    342              return;
    343            }
    344 
    345            this.setActive(nextLayout);
    346 
    347            if (target?._editable) {
    348              target.blur();
    349            }
    350 
    351            this.props.boxModelContainer.focus();
    352          });
    353        }
    354        break;
    355      case KeyCodes.DOM_VK_TAB:
    356        if (isEditable) {
    357          event.preventDefault();
    358          this.moveFocus(event);
    359        }
    360        break;
    361      case KeyCodes.DOM_VK_ESCAPE:
    362        if (target._editable) {
    363          event.preventDefault();
    364          event.stopPropagation();
    365          this.setState({ focusable: false }, () => {
    366            this.props.boxModelContainer.focus();
    367          });
    368        }
    369        break;
    370      default:
    371        break;
    372    }
    373  }
    374 
    375  /**
    376   * Update active on mouse click.
    377   *
    378   * @param  {Event} event
    379   *         The event triggered by a mouse click on the box model
    380   */
    381  onLevelClick(event) {
    382    const { target } = event;
    383    const displayPosition = this.getDisplayPosition();
    384    const isContentBox = this.getContextBox();
    385 
    386    // Avoid switching the active descendant to the position or content layout
    387    // if those are not editable.
    388    if (
    389      (!displayPosition && target == this.positionLayout) ||
    390      (!isContentBox && target == this.contentLayout)
    391    ) {
    392      return;
    393    }
    394 
    395    const nextLayout =
    396      this.layouts[target.getAttribute("data-box")].get("click");
    397    this.setActive(nextLayout);
    398 
    399    if (target?._editable) {
    400      target.blur();
    401    }
    402  }
    403 
    404  render() {
    405    const {
    406      boxModel,
    407      dispatch,
    408      onShowBoxModelEditor,
    409      onShowRulePreviewTooltip,
    410    } = this.props;
    411    const { layout } = boxModel;
    412    let { height, width } = layout;
    413    const { activeDescendant: level, focusable } = this.state;
    414 
    415    const borderTop = this.getBorderOrPaddingValue("border-top-width");
    416    const borderRight = this.getBorderOrPaddingValue("border-right-width");
    417    const borderBottom = this.getBorderOrPaddingValue("border-bottom-width");
    418    const borderLeft = this.getBorderOrPaddingValue("border-left-width");
    419 
    420    const paddingTop = this.getBorderOrPaddingValue("padding-top");
    421    const paddingRight = this.getBorderOrPaddingValue("padding-right");
    422    const paddingBottom = this.getBorderOrPaddingValue("padding-bottom");
    423    const paddingLeft = this.getBorderOrPaddingValue("padding-left");
    424 
    425    const displayPosition = this.getDisplayPosition();
    426    const positionTop = this.getPositionValue("top");
    427    const positionRight = this.getPositionValue("right");
    428    const positionBottom = this.getPositionValue("bottom");
    429    const positionLeft = this.getPositionValue("left");
    430 
    431    const marginTop = this.getMarginValue("margin-top", "top");
    432    const marginRight = this.getMarginValue("margin-right", "right");
    433    const marginBottom = this.getMarginValue("margin-bottom", "bottom");
    434    const marginLeft = this.getMarginValue("margin-left", "left");
    435 
    436    height = this.getHeightValue(height);
    437    width = this.getWidthValue(width);
    438 
    439    const contentBox =
    440      layout["box-sizing"] == "content-box"
    441        ? dom.div(
    442            { className: "boxmodel-size" },
    443            BoxModelEditable({
    444              box: "content",
    445              focusable,
    446              level,
    447              property: "width",
    448              ref: editable => {
    449                this.contentEditable = editable;
    450              },
    451              textContent: width,
    452              onShowBoxModelEditor,
    453              onShowRulePreviewTooltip,
    454            }),
    455            dom.span({}, "\u00D7"),
    456            BoxModelEditable({
    457              box: "content",
    458              focusable,
    459              level,
    460              property: "height",
    461              textContent: height,
    462              onShowBoxModelEditor,
    463              onShowRulePreviewTooltip,
    464            })
    465          )
    466        : dom.p(
    467            {
    468              className: "boxmodel-size",
    469              id: "boxmodel-size-id",
    470            },
    471            dom.span(
    472              { title: "content" },
    473              SHARED_L10N.getFormatStr("dimensions", width, height)
    474            )
    475          );
    476 
    477    return dom.div(
    478      {
    479        className: "boxmodel-main devtools-monospace",
    480        "data-box": "position",
    481        ref: div => {
    482          this.positionLayout = div;
    483        },
    484        onClick: this.onLevelClick,
    485        onKeyDown: this.onKeyDown,
    486        onMouseOver: this.onHighlightMouseOver,
    487        onMouseOut: () => dispatch(unhighlightNode()),
    488      },
    489      displayPosition
    490        ? dom.span(
    491            {
    492              className: "boxmodel-legend",
    493              "data-box": "position",
    494              title: "position",
    495            },
    496            "position"
    497          )
    498        : null,
    499      dom.div(
    500        { className: "boxmodel-box" },
    501        dom.span(
    502          {
    503            className: "boxmodel-legend",
    504            "data-box": "margin",
    505            title: "margin",
    506            role: "region",
    507            "aria-level": "1", // margin, outermost box
    508            "aria-owns":
    509              "margin-top-id margin-right-id margin-bottom-id margin-left-id margins-div",
    510          },
    511          "margin"
    512        ),
    513        dom.div(
    514          {
    515            className: "boxmodel-margins",
    516            id: "margins-div",
    517            "data-box": "margin",
    518            title: "margin",
    519            ref: div => {
    520              this.marginLayout = div;
    521            },
    522          },
    523          dom.span(
    524            {
    525              className: "boxmodel-legend",
    526              "data-box": "border",
    527              title: "border",
    528              role: "region",
    529              "aria-level": "2", // margin -> border, second box
    530              "aria-owns":
    531                "border-top-width-id border-right-width-id border-bottom-width-id border-left-width-id borders-div",
    532            },
    533            "border"
    534          ),
    535          dom.div(
    536            {
    537              className: "boxmodel-borders",
    538              id: "borders-div",
    539              "data-box": "border",
    540              title: "border",
    541              ref: div => {
    542                this.borderLayout = div;
    543              },
    544            },
    545            dom.span(
    546              {
    547                className: "boxmodel-legend",
    548                "data-box": "padding",
    549                title: "padding",
    550                role: "region",
    551                "aria-level": "3", // margin -> border -> padding
    552                "aria-owns":
    553                  "padding-top-id padding-right-id padding-bottom-id padding-left-id padding-div",
    554              },
    555              "padding"
    556            ),
    557            dom.div(
    558              {
    559                className: "boxmodel-paddings",
    560                id: "padding-div",
    561                "data-box": "padding",
    562                title: "padding",
    563                "aria-owns": "boxmodel-contents-id",
    564                ref: div => {
    565                  this.paddingLayout = div;
    566                },
    567              },
    568              dom.div({
    569                className: "boxmodel-contents",
    570                id: "boxmodel-contents-id",
    571                "data-box": "content",
    572                title: "content",
    573                role: "region",
    574                "aria-level": "4", // margin -> border -> padding -> content
    575                "aria-label": SHARED_L10N.getFormatStr(
    576                  "boxModelSize.accessibleLabel",
    577                  width,
    578                  height
    579                ),
    580                "aria-owns": "boxmodel-size-id",
    581                ref: div => {
    582                  this.contentLayout = div;
    583                },
    584              })
    585            )
    586          )
    587        )
    588      ),
    589      displayPosition
    590        ? BoxModelEditable({
    591            box: "position",
    592            direction: "top",
    593            focusable,
    594            level,
    595            property: "position-top",
    596            ref: editable => {
    597              this.positionEditable = editable;
    598            },
    599            textContent: positionTop,
    600            onShowBoxModelEditor,
    601            onShowRulePreviewTooltip,
    602          })
    603        : null,
    604      displayPosition
    605        ? BoxModelEditable({
    606            box: "position",
    607            direction: "right",
    608            focusable,
    609            level,
    610            property: "position-right",
    611            textContent: positionRight,
    612            onShowBoxModelEditor,
    613            onShowRulePreviewTooltip,
    614          })
    615        : null,
    616      displayPosition
    617        ? BoxModelEditable({
    618            box: "position",
    619            direction: "bottom",
    620            focusable,
    621            level,
    622            property: "position-bottom",
    623            textContent: positionBottom,
    624            onShowBoxModelEditor,
    625            onShowRulePreviewTooltip,
    626          })
    627        : null,
    628      displayPosition
    629        ? BoxModelEditable({
    630            box: "position",
    631            direction: "left",
    632            focusable,
    633            level,
    634            property: "position-left",
    635            textContent: positionLeft,
    636            onShowBoxModelEditor,
    637            onShowRulePreviewTooltip,
    638          })
    639        : null,
    640      BoxModelEditable({
    641        box: "margin",
    642        direction: "top",
    643        focusable,
    644        level,
    645        property: "margin-top",
    646        ref: editable => {
    647          this.marginEditable = editable;
    648        },
    649        textContent: marginTop,
    650        onShowBoxModelEditor,
    651        onShowRulePreviewTooltip,
    652      }),
    653      BoxModelEditable({
    654        box: "margin",
    655        direction: "right",
    656        focusable,
    657        level,
    658        property: "margin-right",
    659        textContent: marginRight,
    660        onShowBoxModelEditor,
    661        onShowRulePreviewTooltip,
    662      }),
    663      BoxModelEditable({
    664        box: "margin",
    665        direction: "bottom",
    666        focusable,
    667        level,
    668        property: "margin-bottom",
    669        textContent: marginBottom,
    670        onShowBoxModelEditor,
    671        onShowRulePreviewTooltip,
    672      }),
    673      BoxModelEditable({
    674        box: "margin",
    675        direction: "left",
    676        focusable,
    677        level,
    678        property: "margin-left",
    679        textContent: marginLeft,
    680        onShowBoxModelEditor,
    681        onShowRulePreviewTooltip,
    682      }),
    683      BoxModelEditable({
    684        box: "border",
    685        direction: "top",
    686        focusable,
    687        level,
    688        property: "border-top-width",
    689        ref: editable => {
    690          this.borderEditable = editable;
    691        },
    692        textContent: borderTop,
    693        onShowBoxModelEditor,
    694        onShowRulePreviewTooltip,
    695      }),
    696      BoxModelEditable({
    697        box: "border",
    698        direction: "right",
    699        focusable,
    700        level,
    701        property: "border-right-width",
    702        textContent: borderRight,
    703        onShowBoxModelEditor,
    704        onShowRulePreviewTooltip,
    705      }),
    706      BoxModelEditable({
    707        box: "border",
    708        direction: "bottom",
    709        focusable,
    710        level,
    711        property: "border-bottom-width",
    712        textContent: borderBottom,
    713        onShowBoxModelEditor,
    714        onShowRulePreviewTooltip,
    715      }),
    716      BoxModelEditable({
    717        box: "border",
    718        direction: "left",
    719        focusable,
    720        level,
    721        property: "border-left-width",
    722        textContent: borderLeft,
    723        onShowBoxModelEditor,
    724        onShowRulePreviewTooltip,
    725      }),
    726      BoxModelEditable({
    727        box: "padding",
    728        direction: "top",
    729        focusable,
    730        level,
    731        property: "padding-top",
    732        ref: editable => {
    733          this.paddingEditable = editable;
    734        },
    735        textContent: paddingTop,
    736        onShowBoxModelEditor,
    737        onShowRulePreviewTooltip,
    738      }),
    739      BoxModelEditable({
    740        box: "padding",
    741        direction: "right",
    742        focusable,
    743        level,
    744        property: "padding-right",
    745        textContent: paddingRight,
    746        onShowBoxModelEditor,
    747        onShowRulePreviewTooltip,
    748      }),
    749      BoxModelEditable({
    750        box: "padding",
    751        direction: "bottom",
    752        focusable,
    753        level,
    754        property: "padding-bottom",
    755        textContent: paddingBottom,
    756        onShowBoxModelEditor,
    757        onShowRulePreviewTooltip,
    758      }),
    759      BoxModelEditable({
    760        box: "padding",
    761        direction: "left",
    762        focusable,
    763        level,
    764        property: "padding-left",
    765        textContent: paddingLeft,
    766        onShowBoxModelEditor,
    767        onShowRulePreviewTooltip,
    768      }),
    769      contentBox
    770    );
    771  }
    772 }
    773 
    774 module.exports = BoxModelMain;