tor-browser

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

style-inspector-menu.js (14236B)


      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  VIEW_NODE_SELECTOR_TYPE,
      9  VIEW_NODE_PROPERTY_TYPE,
     10  VIEW_NODE_VALUE_TYPE,
     11  VIEW_NODE_IMAGE_URL_TYPE,
     12  VIEW_NODE_LOCATION_TYPE,
     13 } = require("resource://devtools/client/inspector/shared/node-types.js");
     14 
     15 loader.lazyRequireGetter(
     16  this,
     17  "Menu",
     18  "resource://devtools/client/framework/menu.js"
     19 );
     20 loader.lazyRequireGetter(
     21  this,
     22  "MenuItem",
     23  "resource://devtools/client/framework/menu-item.js"
     24 );
     25 loader.lazyRequireGetter(
     26  this,
     27  "getRuleFromNode",
     28  "resource://devtools/client/inspector/rules/utils/utils.js",
     29  true
     30 );
     31 loader.lazyRequireGetter(
     32  this,
     33  "clipboardHelper",
     34  "resource://devtools/shared/platform/clipboard.js"
     35 );
     36 
     37 const STYLE_INSPECTOR_PROPERTIES =
     38  "devtools/shared/locales/styleinspector.properties";
     39 const { LocalizationHelper } = require("resource://devtools/shared/l10n.js");
     40 const STYLE_INSPECTOR_L10N = new LocalizationHelper(STYLE_INSPECTOR_PROPERTIES);
     41 
     42 const PREF_ORIG_SOURCES = "devtools.source-map.client-service.enabled";
     43 
     44 /**
     45 * Style inspector context menu
     46 */
     47 class StyleInspectorMenu {
     48  /**
     49   * @param {RuleView|ComputedView} view
     50   *        RuleView or ComputedView instance controlling this menu
     51   * @param {object} options
     52   *        Option menu configuration
     53   */
     54  constructor(view, { isRuleView = false } = {}) {
     55    this.view = view;
     56    this.inspector = this.view.inspector;
     57    this.styleWindow = this.view.styleWindow || this.view.doc.defaultView;
     58    this.isRuleView = isRuleView;
     59 
     60    this._onCopy = this._onCopy.bind(this);
     61    this._onCopyColor = this._onCopyColor.bind(this);
     62    this._onCopyImageDataUrl = this._onCopyImageDataUrl.bind(this);
     63    this._onCopyLocation = this._onCopyLocation.bind(this);
     64    this._onCopyDeclaration = this._onCopyDeclaration.bind(this);
     65    this._onCopyPropertyName = this._onCopyPropertyName.bind(this);
     66    this._onCopyPropertyValue = this._onCopyPropertyValue.bind(this);
     67    this._onCopyRule = this._onCopyRule.bind(this);
     68    this._onCopySelector = this._onCopySelector.bind(this);
     69    this._onCopyUrl = this._onCopyUrl.bind(this);
     70    this._onSelectAll = this._onSelectAll.bind(this);
     71    this._onToggleOrigSources = this._onToggleOrigSources.bind(this);
     72  }
     73  /**
     74   * Display the style inspector context menu
     75   */
     76  show(event) {
     77    try {
     78      this._openMenu({
     79        target: event.target,
     80        screenX: event.screenX,
     81        screenY: event.screenY,
     82      });
     83    } catch (e) {
     84      console.error(e);
     85    }
     86  }
     87 
     88  _openMenu({ target, screenX = 0, screenY = 0 } = {}) {
     89    this.currentTarget = target;
     90    this.styleWindow.focus();
     91 
     92    const menu = new Menu();
     93 
     94    const menuitemCopy = new MenuItem({
     95      label: STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copy"),
     96      accesskey: STYLE_INSPECTOR_L10N.getStr(
     97        "styleinspector.contextmenu.copy.accessKey"
     98      ),
     99      click: () => {
    100        this._onCopy();
    101      },
    102      disabled: !this._hasTextSelected(),
    103    });
    104    const menuitemCopyLocation = new MenuItem({
    105      label: STYLE_INSPECTOR_L10N.getStr(
    106        "styleinspector.contextmenu.copyLocation"
    107      ),
    108      click: () => {
    109        this._onCopyLocation();
    110      },
    111      visible: false,
    112    });
    113    const menuitemCopyRule = new MenuItem({
    114      label: STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copyRule"),
    115      click: () => {
    116        this._onCopyRule();
    117      },
    118      visible:
    119        // `_onCopyRule` calls `getRuleFromNode`, which retrieves the target closest
    120        // ancestor element with a data-rule-id attribute. If there's no such element,
    121        // we're not inside a rule and we shouldn't display this context menu item.
    122        this.isRuleView && !!target.closest(".ruleview-rule[data-rule-id]"),
    123    });
    124    const copyColorAccessKey = "styleinspector.contextmenu.copyColor.accessKey";
    125    const menuitemCopyColor = new MenuItem({
    126      label: STYLE_INSPECTOR_L10N.getStr(
    127        "styleinspector.contextmenu.copyColor"
    128      ),
    129      accesskey: STYLE_INSPECTOR_L10N.getStr(copyColorAccessKey),
    130      click: () => {
    131        this._onCopyColor();
    132      },
    133      visible: this._isColorPopup(),
    134    });
    135    const copyUrlAccessKey = "styleinspector.contextmenu.copyUrl.accessKey";
    136    const menuitemCopyUrl = new MenuItem({
    137      label: STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copyUrl"),
    138      accesskey: STYLE_INSPECTOR_L10N.getStr(copyUrlAccessKey),
    139      click: () => {
    140        this._onCopyUrl();
    141      },
    142      visible: this._isImageUrl(),
    143    });
    144    const copyImageAccessKey =
    145      "styleinspector.contextmenu.copyImageDataUrl.accessKey";
    146    const menuitemCopyImageDataUrl = new MenuItem({
    147      label: STYLE_INSPECTOR_L10N.getStr(
    148        "styleinspector.contextmenu.copyImageDataUrl"
    149      ),
    150      accesskey: STYLE_INSPECTOR_L10N.getStr(copyImageAccessKey),
    151      click: () => {
    152        this._onCopyImageDataUrl();
    153      },
    154      visible: this._isImageUrl(),
    155    });
    156    const copyDeclarationLabel = "styleinspector.contextmenu.copyDeclaration";
    157    const menuitemCopyDeclaration = new MenuItem({
    158      label: STYLE_INSPECTOR_L10N.getStr(copyDeclarationLabel),
    159      click: () => {
    160        this._onCopyDeclaration();
    161      },
    162      visible: false,
    163    });
    164    const menuitemCopyPropertyName = new MenuItem({
    165      label: STYLE_INSPECTOR_L10N.getStr(
    166        "styleinspector.contextmenu.copyPropertyName"
    167      ),
    168      click: () => {
    169        this._onCopyPropertyName();
    170      },
    171      visible: false,
    172    });
    173    const menuitemCopyPropertyValue = new MenuItem({
    174      label: STYLE_INSPECTOR_L10N.getStr(
    175        "styleinspector.contextmenu.copyPropertyValue"
    176      ),
    177      click: () => {
    178        this._onCopyPropertyValue();
    179      },
    180      visible: false,
    181    });
    182    const menuitemCopySelector = new MenuItem({
    183      label: STYLE_INSPECTOR_L10N.getStr(
    184        "styleinspector.contextmenu.copySelector"
    185      ),
    186      click: () => {
    187        this._onCopySelector();
    188      },
    189      visible: false,
    190    });
    191 
    192    this._clickedNodeInfo = this._getClickedNodeInfo();
    193    if (this.isRuleView && this._clickedNodeInfo) {
    194      switch (this._clickedNodeInfo.type) {
    195        case VIEW_NODE_PROPERTY_TYPE:
    196          menuitemCopyDeclaration.visible = true;
    197          menuitemCopyPropertyName.visible = true;
    198          break;
    199        case VIEW_NODE_VALUE_TYPE:
    200          menuitemCopyDeclaration.visible = true;
    201          menuitemCopyPropertyValue.visible = true;
    202          break;
    203        case VIEW_NODE_SELECTOR_TYPE:
    204          menuitemCopySelector.visible = true;
    205          break;
    206        case VIEW_NODE_LOCATION_TYPE:
    207          menuitemCopyLocation.visible = true;
    208          break;
    209      }
    210    }
    211 
    212    menu.append(menuitemCopy);
    213    menu.append(menuitemCopyLocation);
    214    menu.append(menuitemCopyRule);
    215    menu.append(menuitemCopyColor);
    216    menu.append(menuitemCopyUrl);
    217    menu.append(menuitemCopyImageDataUrl);
    218    menu.append(menuitemCopyDeclaration);
    219    menu.append(menuitemCopyPropertyName);
    220    menu.append(menuitemCopyPropertyValue);
    221    menu.append(menuitemCopySelector);
    222 
    223    menu.append(
    224      new MenuItem({
    225        type: "separator",
    226      })
    227    );
    228 
    229    // Select All
    230    const selectAllAccessKey = "styleinspector.contextmenu.selectAll.accessKey";
    231    const menuitemSelectAll = new MenuItem({
    232      label: STYLE_INSPECTOR_L10N.getStr(
    233        "styleinspector.contextmenu.selectAll"
    234      ),
    235      accesskey: STYLE_INSPECTOR_L10N.getStr(selectAllAccessKey),
    236      click: () => {
    237        this._onSelectAll();
    238      },
    239    });
    240    menu.append(menuitemSelectAll);
    241 
    242    menu.append(
    243      new MenuItem({
    244        type: "separator",
    245      })
    246    );
    247 
    248    // Add new rule
    249    const addRuleAccessKey = "styleinspector.contextmenu.addNewRule.accessKey";
    250    const menuitemAddRule = new MenuItem({
    251      label: STYLE_INSPECTOR_L10N.getStr(
    252        "styleinspector.contextmenu.addNewRule"
    253      ),
    254      accesskey: STYLE_INSPECTOR_L10N.getStr(addRuleAccessKey),
    255      click: () => this.view.addNewRule(),
    256      visible: this.isRuleView,
    257      disabled: !this.isRuleView || !this.view.canAddNewRuleForSelectedNode(),
    258    });
    259    menu.append(menuitemAddRule);
    260 
    261    // Show Original Sources
    262    const sourcesAccessKey =
    263      "styleinspector.contextmenu.toggleOrigSources.accessKey";
    264    const menuitemSources = new MenuItem({
    265      label: STYLE_INSPECTOR_L10N.getStr(
    266        "styleinspector.contextmenu.toggleOrigSources"
    267      ),
    268      accesskey: STYLE_INSPECTOR_L10N.getStr(sourcesAccessKey),
    269      click: () => {
    270        this._onToggleOrigSources();
    271      },
    272      type: "checkbox",
    273      checked: Services.prefs.getBoolPref(PREF_ORIG_SOURCES),
    274    });
    275    menu.append(menuitemSources);
    276 
    277    menu.popup(screenX, screenY, this.inspector.toolbox.doc);
    278    return menu;
    279  }
    280 
    281  _hasTextSelected() {
    282    let hasTextSelected;
    283    const selection = this.styleWindow.getSelection();
    284 
    285    const node = this._getClickedNode();
    286    if (node.nodeName == "input" || node.nodeName == "textarea") {
    287      const { selectionStart, selectionEnd } = node;
    288      hasTextSelected =
    289        isFinite(selectionStart) &&
    290        isFinite(selectionEnd) &&
    291        selectionStart !== selectionEnd;
    292    } else {
    293      hasTextSelected = selection.toString() && !selection.isCollapsed;
    294    }
    295 
    296    return hasTextSelected;
    297  }
    298 
    299  /**
    300   * Get the type of the currently clicked node
    301   */
    302  _getClickedNodeInfo() {
    303    const node = this._getClickedNode();
    304    return this.view.getNodeInfo(node);
    305  }
    306 
    307  /**
    308   * A helper that determines if the popup was opened with a click to a color
    309   * value and saves the color to this._colorToCopy.
    310   *
    311   * @return {boolean}
    312   *         true if click on color opened the popup, false otherwise.
    313   */
    314  _isColorPopup() {
    315    this._colorToCopy = "";
    316 
    317    const container = this._getClickedNode();
    318    if (!container) {
    319      return false;
    320    }
    321 
    322    const colorNode = container.closest("[data-color]");
    323    if (!colorNode) {
    324      return false;
    325    }
    326 
    327    this._colorToCopy = colorNode.dataset.color;
    328    return true;
    329  }
    330 
    331  /**
    332   * Check if the current node (clicked node) is an image URL
    333   *
    334   * @return {boolean} true if the node is an image url
    335   */
    336  _isImageUrl() {
    337    const nodeInfo = this._getClickedNodeInfo();
    338    if (!nodeInfo) {
    339      return false;
    340    }
    341    return nodeInfo.type == VIEW_NODE_IMAGE_URL_TYPE;
    342  }
    343 
    344  /**
    345   * Get the DOM Node container for the current target node.
    346   * If the target node is a text node, return the parent node, otherwise return
    347   * the target node itself.
    348   *
    349   * @return {DOMNode}
    350   */
    351  _getClickedNode() {
    352    const node = this.currentTarget;
    353 
    354    if (!node) {
    355      return null;
    356    }
    357 
    358    return node.nodeType === node.TEXT_NODE ? node.parentElement : node;
    359  }
    360 
    361  /**
    362   * Select all text.
    363   */
    364  _onSelectAll() {
    365    const selection = this.styleWindow.getSelection();
    366 
    367    if (this.isRuleView) {
    368      selection.selectAllChildren(
    369        this.currentTarget.closest("#ruleview-container-focusable")
    370      );
    371    } else {
    372      selection.selectAllChildren(this.view.element);
    373    }
    374  }
    375 
    376  /**
    377   * Copy the most recently selected color value to clipboard.
    378   */
    379  _onCopy() {
    380    this.view.copySelection(this.currentTarget);
    381  }
    382 
    383  /**
    384   * Copy the most recently selected color value to clipboard.
    385   */
    386  _onCopyColor() {
    387    clipboardHelper.copyString(this._colorToCopy);
    388  }
    389 
    390  /*
    391   * Retrieve the url for the selected image and copy it to the clipboard
    392   */
    393  _onCopyUrl() {
    394    if (!this._clickedNodeInfo) {
    395      return;
    396    }
    397 
    398    clipboardHelper.copyString(this._clickedNodeInfo.value.url);
    399  }
    400 
    401  /**
    402   * Retrieve the image data for the selected image url and copy it to the
    403   * clipboard
    404   */
    405  async _onCopyImageDataUrl() {
    406    if (!this._clickedNodeInfo) {
    407      return;
    408    }
    409 
    410    let message;
    411    try {
    412      const inspectorFront = this.inspector.inspectorFront;
    413      const imageUrl = this._clickedNodeInfo.value.url;
    414      const data = await inspectorFront.getImageDataFromURL(imageUrl);
    415      message = await data.data.string();
    416    } catch (e) {
    417      message = STYLE_INSPECTOR_L10N.getStr(
    418        "styleinspector.copyImageDataUrlError"
    419      );
    420    }
    421 
    422    clipboardHelper.copyString(message);
    423  }
    424 
    425  /**
    426   * Copy the rule source location of the current clicked node.
    427   */
    428  _onCopyLocation() {
    429    if (!this._clickedNodeInfo) {
    430      return;
    431    }
    432 
    433    clipboardHelper.copyString(this._clickedNodeInfo.value);
    434  }
    435 
    436  /**
    437   * Copy the CSS declaration of the current clicked node.
    438   */
    439  _onCopyDeclaration() {
    440    if (!this._clickedNodeInfo) {
    441      return;
    442    }
    443 
    444    const textProp = this._clickedNodeInfo.value.textProperty;
    445    clipboardHelper.copyString(textProp.stringifyProperty());
    446  }
    447 
    448  /**
    449   * Copy the rule property name of the current clicked node.
    450   */
    451  _onCopyPropertyName() {
    452    if (!this._clickedNodeInfo) {
    453      return;
    454    }
    455 
    456    clipboardHelper.copyString(this._clickedNodeInfo.value.property);
    457  }
    458 
    459  /**
    460   * Copy the rule property value of the current clicked node.
    461   */
    462  _onCopyPropertyValue() {
    463    if (!this._clickedNodeInfo) {
    464      return;
    465    }
    466 
    467    clipboardHelper.copyString(this._clickedNodeInfo.value.value);
    468  }
    469 
    470  /**
    471   * Copy the rule of the current clicked node.
    472   */
    473  _onCopyRule() {
    474    const node = this._getClickedNode();
    475    const rule = getRuleFromNode(node, this.view._elementStyle);
    476    if (!rule) {
    477      console.error("Can't copy rule, no rule found for node", node);
    478      return;
    479    }
    480    clipboardHelper.copyString(rule.stringifyRule());
    481  }
    482 
    483  /**
    484   * Copy the rule selector of the current clicked node.
    485   */
    486  _onCopySelector() {
    487    if (!this._clickedNodeInfo) {
    488      return;
    489    }
    490 
    491    clipboardHelper.copyString(this._clickedNodeInfo.value);
    492  }
    493 
    494  /**
    495   * Toggle the original sources pref.
    496   */
    497  _onToggleOrigSources() {
    498    const isEnabled = Services.prefs.getBoolPref(PREF_ORIG_SOURCES);
    499    Services.prefs.setBoolPref(PREF_ORIG_SOURCES, !isEnabled);
    500  }
    501 
    502  destroy() {
    503    this.currentTarget = null;
    504    this.view = null;
    505    this.inspector = null;
    506    this.styleWindow = null;
    507  }
    508 }
    509 
    510 module.exports = StyleInspectorMenu;