diff options
Diffstat (limited to 'devtools/client/inspector/shared/style-inspector-menu.js')
-rw-r--r-- | devtools/client/inspector/shared/style-inspector-menu.js | 510 |
1 files changed, 510 insertions, 0 deletions
diff --git a/devtools/client/inspector/shared/style-inspector-menu.js b/devtools/client/inspector/shared/style-inspector-menu.js new file mode 100644 index 000000000..975074609 --- /dev/null +++ b/devtools/client/inspector/shared/style-inspector-menu.js @@ -0,0 +1,510 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* 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 {PREF_ORIG_SOURCES} = require("devtools/client/styleeditor/utils"); +const Services = require("Services"); +const {Task} = require("devtools/shared/task"); + +const Menu = require("devtools/client/framework/menu"); +const MenuItem = require("devtools/client/framework/menu-item"); + +const { + VIEW_NODE_SELECTOR_TYPE, + VIEW_NODE_PROPERTY_TYPE, + VIEW_NODE_VALUE_TYPE, + VIEW_NODE_IMAGE_URL_TYPE, + VIEW_NODE_LOCATION_TYPE, +} = require("devtools/client/inspector/shared/node-types"); +const clipboardHelper = require("devtools/shared/platform/clipboard"); + +const STYLE_INSPECTOR_PROPERTIES = "devtools/shared/locales/styleinspector.properties"; +const {LocalizationHelper} = require("devtools/shared/l10n"); +const STYLE_INSPECTOR_L10N = new LocalizationHelper(STYLE_INSPECTOR_PROPERTIES); + +const PREF_ENABLE_MDN_DOCS_TOOLTIP = + "devtools.inspector.mdnDocsTooltip.enabled"; + +/** + * Style inspector context menu + * + * @param {RuleView|ComputedView} view + * RuleView or ComputedView instance controlling this menu + * @param {Object} options + * Option menu configuration + */ +function StyleInspectorMenu(view, options) { + this.view = view; + this.inspector = this.view.inspector; + this.styleDocument = this.view.styleDocument; + this.styleWindow = this.view.styleWindow; + + this.isRuleView = options.isRuleView; + + this._onAddNewRule = this._onAddNewRule.bind(this); + this._onCopy = this._onCopy.bind(this); + this._onCopyColor = this._onCopyColor.bind(this); + this._onCopyImageDataUrl = this._onCopyImageDataUrl.bind(this); + this._onCopyLocation = this._onCopyLocation.bind(this); + this._onCopyPropertyDeclaration = this._onCopyPropertyDeclaration.bind(this); + this._onCopyPropertyName = this._onCopyPropertyName.bind(this); + this._onCopyPropertyValue = this._onCopyPropertyValue.bind(this); + this._onCopyRule = this._onCopyRule.bind(this); + this._onCopySelector = this._onCopySelector.bind(this); + this._onCopyUrl = this._onCopyUrl.bind(this); + this._onSelectAll = this._onSelectAll.bind(this); + this._onShowMdnDocs = this._onShowMdnDocs.bind(this); + this._onToggleOrigSources = this._onToggleOrigSources.bind(this); +} + +module.exports = StyleInspectorMenu; + +StyleInspectorMenu.prototype = { + /** + * Display the style inspector context menu + */ + show: function (event) { + try { + this._openMenu({ + target: event.explicitOriginalTarget, + screenX: event.screenX, + screenY: event.screenY, + }); + } catch (e) { + console.error(e); + } + }, + + _openMenu: function ({ target, screenX = 0, screenY = 0 } = { }) { + // In the sidebar we do not have this.styleDocument.popupNode + // so we need to save the node ourselves. + this.styleDocument.popupNode = target; + this.styleWindow.focus(); + + let menu = new Menu(); + + let menuitemCopy = new MenuItem({ + label: STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copy"), + accesskey: STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copy.accessKey"), + click: () => { + this._onCopy(); + }, + disabled: !this._hasTextSelected(), + }); + let menuitemCopyLocation = new MenuItem({ + label: STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copyLocation"), + click: () => { + this._onCopyLocation(); + }, + visible: false, + }); + let menuitemCopyRule = new MenuItem({ + label: STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copyRule"), + click: () => { + this._onCopyRule(); + }, + visible: this.isRuleView, + }); + let copyColorAccessKey = "styleinspector.contextmenu.copyColor.accessKey"; + let menuitemCopyColor = new MenuItem({ + label: STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copyColor"), + accesskey: STYLE_INSPECTOR_L10N.getStr(copyColorAccessKey), + click: () => { + this._onCopyColor(); + }, + visible: this._isColorPopup(), + }); + let copyUrlAccessKey = "styleinspector.contextmenu.copyUrl.accessKey"; + let menuitemCopyUrl = new MenuItem({ + label: STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copyUrl"), + accesskey: STYLE_INSPECTOR_L10N.getStr(copyUrlAccessKey), + click: () => { + this._onCopyUrl(); + }, + visible: this._isImageUrl(), + }); + let copyImageAccessKey = "styleinspector.contextmenu.copyImageDataUrl.accessKey"; + let menuitemCopyImageDataUrl = new MenuItem({ + label: STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copyImageDataUrl"), + accesskey: STYLE_INSPECTOR_L10N.getStr(copyImageAccessKey), + click: () => { + this._onCopyImageDataUrl(); + }, + visible: this._isImageUrl(), + }); + let copyPropDeclarationLabel = "styleinspector.contextmenu.copyPropertyDeclaration"; + let menuitemCopyPropertyDeclaration = new MenuItem({ + label: STYLE_INSPECTOR_L10N.getStr(copyPropDeclarationLabel), + click: () => { + this._onCopyPropertyDeclaration(); + }, + visible: false, + }); + let menuitemCopyPropertyName = new MenuItem({ + label: STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copyPropertyName"), + click: () => { + this._onCopyPropertyName(); + }, + visible: false, + }); + let menuitemCopyPropertyValue = new MenuItem({ + label: STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copyPropertyValue"), + click: () => { + this._onCopyPropertyValue(); + }, + visible: false, + }); + let menuitemCopySelector = new MenuItem({ + label: STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copySelector"), + click: () => { + this._onCopySelector(); + }, + visible: false, + }); + + this._clickedNodeInfo = this._getClickedNodeInfo(); + if (this.isRuleView && this._clickedNodeInfo) { + switch (this._clickedNodeInfo.type) { + case VIEW_NODE_PROPERTY_TYPE : + menuitemCopyPropertyDeclaration.visible = true; + menuitemCopyPropertyName.visible = true; + break; + case VIEW_NODE_VALUE_TYPE : + menuitemCopyPropertyDeclaration.visible = true; + menuitemCopyPropertyValue.visible = true; + break; + case VIEW_NODE_SELECTOR_TYPE : + menuitemCopySelector.visible = true; + break; + case VIEW_NODE_LOCATION_TYPE : + menuitemCopyLocation.visible = true; + break; + } + } + + menu.append(menuitemCopy); + menu.append(menuitemCopyLocation); + menu.append(menuitemCopyRule); + menu.append(menuitemCopyColor); + menu.append(menuitemCopyUrl); + menu.append(menuitemCopyImageDataUrl); + menu.append(menuitemCopyPropertyDeclaration); + menu.append(menuitemCopyPropertyName); + menu.append(menuitemCopyPropertyValue); + menu.append(menuitemCopySelector); + + menu.append(new MenuItem({ + type: "separator", + })); + + // Select All + let selectAllAccessKey = "styleinspector.contextmenu.selectAll.accessKey"; + let menuitemSelectAll = new MenuItem({ + label: STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.selectAll"), + accesskey: STYLE_INSPECTOR_L10N.getStr(selectAllAccessKey), + click: () => { + this._onSelectAll(); + }, + }); + menu.append(menuitemSelectAll); + + menu.append(new MenuItem({ + type: "separator", + })); + + // Add new rule + let addRuleAccessKey = "styleinspector.contextmenu.addNewRule.accessKey"; + let menuitemAddRule = new MenuItem({ + label: STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.addNewRule"), + accesskey: STYLE_INSPECTOR_L10N.getStr(addRuleAccessKey), + click: () => { + this._onAddNewRule(); + }, + visible: this.isRuleView, + disabled: !this.isRuleView || + this.inspector.selection.isAnonymousNode(), + }); + menu.append(menuitemAddRule); + + // Show MDN Docs + let mdnDocsAccessKey = "styleinspector.contextmenu.showMdnDocs.accessKey"; + let menuitemShowMdnDocs = new MenuItem({ + label: STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.showMdnDocs"), + accesskey: STYLE_INSPECTOR_L10N.getStr(mdnDocsAccessKey), + click: () => { + this._onShowMdnDocs(); + }, + visible: (Services.prefs.getBoolPref(PREF_ENABLE_MDN_DOCS_TOOLTIP) && + this._isPropertyName()), + }); + menu.append(menuitemShowMdnDocs); + + // Show Original Sources + let sourcesAccessKey = "styleinspector.contextmenu.toggleOrigSources.accessKey"; + let menuitemSources = new MenuItem({ + label: STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.toggleOrigSources"), + accesskey: STYLE_INSPECTOR_L10N.getStr(sourcesAccessKey), + click: () => { + this._onToggleOrigSources(); + }, + type: "checkbox", + checked: Services.prefs.getBoolPref(PREF_ORIG_SOURCES), + }); + menu.append(menuitemSources); + + menu.popup(screenX, screenY, this.inspector._toolbox); + return menu; + }, + + _hasTextSelected: function () { + let hasTextSelected; + let selection = this.styleWindow.getSelection(); + + let node = this._getClickedNode(); + if (node.nodeName == "input" || node.nodeName == "textarea") { + let { selectionStart, selectionEnd } = node; + hasTextSelected = isFinite(selectionStart) && isFinite(selectionEnd) + && selectionStart !== selectionEnd; + } else { + hasTextSelected = selection.toString() && !selection.isCollapsed; + } + + return hasTextSelected; + }, + + /** + * Get the type of the currently clicked node + */ + _getClickedNodeInfo: function () { + let node = this._getClickedNode(); + return this.view.getNodeInfo(node); + }, + + /** + * A helper that determines if the popup was opened with a click to a color + * value and saves the color to this._colorToCopy. + * + * @return {Boolean} + * true if click on color opened the popup, false otherwise. + */ + _isColorPopup: function () { + this._colorToCopy = ""; + + let container = this._getClickedNode(); + if (!container) { + return false; + } + + let isColorNode = el => el.dataset && "color" in el.dataset; + + while (!isColorNode(container)) { + container = container.parentNode; + if (!container) { + return false; + } + } + + this._colorToCopy = container.dataset.color; + return true; + }, + + _isPropertyName: function () { + let nodeInfo = this._getClickedNodeInfo(); + if (!nodeInfo) { + return false; + } + return nodeInfo.type == VIEW_NODE_PROPERTY_TYPE; + }, + + /** + * Check if the current node (clicked node) is an image URL + * + * @return {Boolean} true if the node is an image url + */ + _isImageUrl: function () { + let nodeInfo = this._getClickedNodeInfo(); + if (!nodeInfo) { + return false; + } + return nodeInfo.type == VIEW_NODE_IMAGE_URL_TYPE; + }, + + /** + * Get the DOM Node container for the current popupNode. + * If popupNode is a textNode, return the parent node, otherwise return + * popupNode itself. + * + * @return {DOMNode} + */ + _getClickedNode: function () { + let container = null; + let node = this.styleDocument.popupNode; + + if (node) { + let isTextNode = node.nodeType == node.TEXT_NODE; + container = isTextNode ? node.parentElement : node; + } + + return container; + }, + + /** + * Select all text. + */ + _onSelectAll: function () { + let selection = this.styleWindow.getSelection(); + selection.selectAllChildren(this.view.element); + }, + + /** + * Copy the most recently selected color value to clipboard. + */ + _onCopy: function () { + this.view.copySelection(this.styleDocument.popupNode); + }, + + /** + * Copy the most recently selected color value to clipboard. + */ + _onCopyColor: function () { + clipboardHelper.copyString(this._colorToCopy); + }, + + /* + * Retrieve the url for the selected image and copy it to the clipboard + */ + _onCopyUrl: function () { + if (!this._clickedNodeInfo) { + return; + } + + clipboardHelper.copyString(this._clickedNodeInfo.value.url); + }, + + /** + * Retrieve the image data for the selected image url and copy it to the + * clipboard + */ + _onCopyImageDataUrl: Task.async(function* () { + if (!this._clickedNodeInfo) { + return; + } + + let message; + try { + let inspectorFront = this.inspector.inspector; + let imageUrl = this._clickedNodeInfo.value.url; + let data = yield inspectorFront.getImageDataFromURL(imageUrl); + message = yield data.data.string(); + } catch (e) { + message = + STYLE_INSPECTOR_L10N.getStr("styleinspector.copyImageDataUrlError"); + } + + clipboardHelper.copyString(message); + }), + + /** + * Show docs from MDN for a CSS property. + */ + _onShowMdnDocs: function () { + let cssPropertyName = this.styleDocument.popupNode.textContent; + let anchor = this.styleDocument.popupNode.parentNode; + let cssDocsTooltip = this.view.tooltips.cssDocs; + cssDocsTooltip.show(anchor, cssPropertyName); + }, + + /** + * Add a new rule to the current element. + */ + _onAddNewRule: function () { + this.view._onAddRule(); + }, + + /** + * Copy the rule source location of the current clicked node. + */ + _onCopyLocation: function () { + if (!this._clickedNodeInfo) { + return; + } + + clipboardHelper.copyString(this._clickedNodeInfo.value); + }, + + /** + * Copy the rule property declaration of the current clicked node. + */ + _onCopyPropertyDeclaration: function () { + if (!this._clickedNodeInfo) { + return; + } + + let textProp = this._clickedNodeInfo.value.textProperty; + clipboardHelper.copyString(textProp.stringifyProperty()); + }, + + /** + * Copy the rule property name of the current clicked node. + */ + _onCopyPropertyName: function () { + if (!this._clickedNodeInfo) { + return; + } + + clipboardHelper.copyString(this._clickedNodeInfo.value.property); + }, + + /** + * Copy the rule property value of the current clicked node. + */ + _onCopyPropertyValue: function () { + if (!this._clickedNodeInfo) { + return; + } + + clipboardHelper.copyString(this._clickedNodeInfo.value.value); + }, + + /** + * Copy the rule of the current clicked node. + */ + _onCopyRule: function () { + let ruleEditor = + this.styleDocument.popupNode.parentNode.offsetParent._ruleEditor; + let rule = ruleEditor.rule; + clipboardHelper.copyString(rule.stringifyRule()); + }, + + /** + * Copy the rule selector of the current clicked node. + */ + _onCopySelector: function () { + if (!this._clickedNodeInfo) { + return; + } + + clipboardHelper.copyString(this._clickedNodeInfo.value); + }, + + /** + * Toggle the original sources pref. + */ + _onToggleOrigSources: function () { + let isEnabled = Services.prefs.getBoolPref(PREF_ORIG_SOURCES); + Services.prefs.setBoolPref(PREF_ORIG_SOURCES, !isEnabled); + }, + + destroy: function () { + this.popupNode = null; + this.styleDocument.popupNode = null; + this.view = null; + this.inspector = null; + this.styleDocument = null; + this.styleWindow = null; + } +}; |