/* -*- 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 {Cc, Ci, Cu} = require("chrome"); const {PREF_ORIG_SOURCES} = require("devtools/client/styleeditor/utils"); loader.lazyRequireGetter(this, "overlays", "devtools/client/styleinspector/style-inspector-overlays"); loader.lazyImporter(this, "Services", "resource://gre/modules/Services.jsm"); loader.lazyServiceGetter(this, "clipboardHelper", "@mozilla.org/widget/clipboardhelper;1", "nsIClipboardHelper"); loader.lazyGetter(this, "_strings", () => { return Services.strings .createBundle("chrome://devtools-shared/locale/styleinspector.properties"); }); const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; 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); this._updateMenuItems = this._updateMenuItems.bind(this); this._createContextMenu(); } module.exports = StyleInspectorMenu; StyleInspectorMenu.prototype = { /** * Display the style inspector context menu */ show: function(event) { try { // In the sidebar we do not have this.styleDocument.popupNode // so we need to save the node ourselves. this.styleDocument.popupNode = event.explicitOriginalTarget; this.styleWindow.focus(); this._menupopup.openPopupAtScreen(event.screenX, event.screenY, true); } catch(e) { console.error(e); } }, _createContextMenu: function() { this._menupopup = this.styleDocument.createElementNS(XUL_NS, "menupopup"); this._menupopup.addEventListener("popupshowing", this._updateMenuItems); this._menupopup.id = "computed-view-context-menu"; let parentDocument = this.styleWindow.parent.document; let popupset = parentDocument.documentElement.querySelector("popupset"); if (!popupset) { popupset = parentDocument.createElementNS(XUL_NS, "popupset"); parentDocument.documentElement.appendChild(popupset); } popupset.appendChild(this._menupopup); this._createContextMenuItems(); }, /** * Create all context menu items */ _createContextMenuItems: function() { this.menuitemCopy = this._createContextMenuItem({ label: "styleinspector.contextmenu.copy", accesskey: "styleinspector.contextmenu.copy.accessKey", command: this._onCopy }); this.menuitemCopyLocation = this._createContextMenuItem({ label: "styleinspector.contextmenu.copyLocation", command: this._onCopyLocation }); this.menuitemCopyRule = this._createContextMenuItem({ label: "styleinspector.contextmenu.copyRule", command: this._onCopyRule }); this.menuitemCopyColor = this._createContextMenuItem({ label: "styleinspector.contextmenu.copyColor", accesskey: "styleinspector.contextmenu.copyColor.accessKey", command: this._onCopyColor }); this.menuitemCopyUrl = this._createContextMenuItem({ label: "styleinspector.contextmenu.copyUrl", accesskey: "styleinspector.contextmenu.copyUrl.accessKey", command: this._onCopyUrl }); this.menuitemCopyImageDataUrl = this._createContextMenuItem({ label: "styleinspector.contextmenu.copyImageDataUrl", accesskey: "styleinspector.contextmenu.copyImageDataUrl.accessKey", command: this._onCopyImageDataUrl }); this.menuitemCopyPropertyDeclaration = this._createContextMenuItem({ label: "styleinspector.contextmenu.copyPropertyDeclaration", command: this._onCopyPropertyDeclaration }); this.menuitemCopyPropertyName = this._createContextMenuItem({ label: "styleinspector.contextmenu.copyPropertyName", command: this._onCopyPropertyName }); this.menuitemCopyPropertyValue = this._createContextMenuItem({ label: "styleinspector.contextmenu.copyPropertyValue", command: this._onCopyPropertyValue }); this.menuitemCopySelector = this._createContextMenuItem({ label: "styleinspector.contextmenu.copySelector", command: this._onCopySelector }); this._createMenuSeparator(); // Select All this.menuitemSelectAll = this._createContextMenuItem({ label: "styleinspector.contextmenu.selectAll", accesskey: "styleinspector.contextmenu.selectAll.accessKey", command: this._onSelectAll }); this._createMenuSeparator(); // Add new rule this.menuitemAddRule = this._createContextMenuItem({ label: "styleinspector.contextmenu.addNewRule", accesskey: "styleinspector.contextmenu.addNewRule.accessKey", command: this._onAddNewRule }); // Show MDN Docs this.menuitemShowMdnDocs = this._createContextMenuItem({ label: "styleinspector.contextmenu.showMdnDocs", accesskey: "styleinspector.contextmenu.showMdnDocs.accessKey", command: this._onShowMdnDocs }); // Show Original Sources this.menuitemSources = this._createContextMenuItem({ label: "styleinspector.contextmenu.toggleOrigSources", accesskey: "styleinspector.contextmenu.toggleOrigSources.accessKey", command: this._onToggleOrigSources, type: "checkbox" }); }, /** * Create a single context menu item based on the provided configuration * Returns the created menu item element */ _createContextMenuItem: function(attributes) { let ownerDocument = this._menupopup.ownerDocument; let item = ownerDocument.createElementNS(XUL_NS, "menuitem"); item.setAttribute("label", _strings.GetStringFromName(attributes.label)); if (attributes.accesskey) { item.setAttribute("accesskey", _strings.GetStringFromName(attributes.accesskey)); } item.addEventListener("command", attributes.command); if (attributes.type) { item.setAttribute("type", attributes.type); } this._menupopup.appendChild(item); return item; }, _createMenuSeparator: function() { let ownerDocument = this._menupopup.ownerDocument; let separator = ownerDocument.createElementNS(XUL_NS, "menuseparator"); this._menupopup.appendChild(separator); }, /** * Update the context menu. This means enabling or disabling menuitems as * appropriate. */ _updateMenuItems: function() { this._updateCopyMenuItems(); let showOrig = Services.prefs.getBoolPref(PREF_ORIG_SOURCES); this.menuitemSources.setAttribute("checked", showOrig); let enableMdnDocsTooltip = Services.prefs.getBoolPref(PREF_ENABLE_MDN_DOCS_TOOLTIP); this.menuitemShowMdnDocs.hidden = !(enableMdnDocsTooltip && this._isPropertyName()); this.menuitemAddRule.hidden = !this.isRuleView; this.menuitemAddRule.disabled = !this.isRuleView || this.inspector.selection.isAnonymousNode(); }, /** * Display the necessary copy context menu items depending on the clicked * node and selection in the rule view. */ _updateCopyMenuItems: function() { this.menuitemCopy.disabled = !this._hasTextSelected(); this.menuitemCopyColor.hidden = !this._isColorPopup(); this.menuitemCopyImageDataUrl.hidden = !this._isImageUrl(); this.menuitemCopyUrl.hidden = !this._isImageUrl(); this.menuitemCopyRule.hidden = !this.isRuleView; this.menuitemCopyLocation.hidden = true; this.menuitemCopyPropertyDeclaration.hidden = true; this.menuitemCopyPropertyName.hidden = true; this.menuitemCopyPropertyValue.hidden = true; this.menuitemCopySelector.hidden = true; this._clickedNodeInfo = this._getClickedNodeInfo(); if (this.isRuleView && this._clickedNodeInfo) { switch (this._clickedNodeInfo.type) { case overlays.VIEW_NODE_PROPERTY_TYPE : this.menuitemCopyPropertyDeclaration.hidden = false; this.menuitemCopyPropertyName.hidden = false; break; case overlays.VIEW_NODE_VALUE_TYPE : this.menuitemCopyPropertyDeclaration.hidden = false; this.menuitemCopyPropertyValue.hidden = false; break; case overlays.VIEW_NODE_SELECTOR_TYPE : this.menuitemCopySelector.hidden = false; break; case overlays.VIEW_NODE_LOCATION_TYPE : this.menuitemCopyLocation.hidden = false; break; } } }, _hasTextSelected: function() { let hasTextSelected; let selection = this.styleWindow.getSelection(); let node = this._getClickedNode(); if (node.nodeName == "input") { // input type="text" 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 == overlays.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 == overlays.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.styleDocument.documentElement); }, /** * 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 = _strings.GetStringFromName("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._removeContextMenuItems(); // Destroy the context menu. this._menupopup.removeEventListener("popupshowing", this._updateMenuItems); this._menupopup.parentNode.removeChild(this._menupopup); this._menupopup = null; this.popupNode = null; this.styleDocument.popupNode = null; this.view = null; this.inspector = null; this.styleDocument = null; this.styleWindow = null; }, _removeContextMenuItems: function() { this._removeContextMenuItem("menuitemAddRule", this._onAddNewRule); this._removeContextMenuItem("menuitemCopy", this._onCopy); this._removeContextMenuItem("menuitemCopyColor", this._onCopyColor); this._removeContextMenuItem("menuitemCopyImageDataUrl", this._onCopyImageDataUrl); this._removeContextMenuItem("menuitemCopyLocation", this._onCopyLocation); this._removeContextMenuItem("menuitemCopyPropertyDeclaration", this._onCopyPropertyDeclaration); this._removeContextMenuItem("menuitemCopyPropertyName", this._onCopyPropertyName); this._removeContextMenuItem("menuitemCopyPropertyValue", this._onCopyPropertyValue); this._removeContextMenuItem("menuitemCopyRule", this._onCopyRule); this._removeContextMenuItem("menuitemCopySelector", this._onCopySelector); this._removeContextMenuItem("menuitemCopyUrl", this._onCopyUrl); this._removeContextMenuItem("menuitemSelectAll", this._onSelectAll); this._removeContextMenuItem("menuitemShowMdnDocs", this._onShowMdnDocs); this._removeContextMenuItem("menuitemSources", this._onToggleOrigSources); }, _removeContextMenuItem: function(itemName, callback) { if (this[itemName]) { this[itemName].removeEventListener("command", callback); this[itemName] = null; } } };