tenfourfox/devtools/client/styleinspector/style-inspector-menu.js
Cameron Kaiser c9b2922b70 hello FPR
2017-04-19 00:56:45 -07:00

558 lines
17 KiB
JavaScript

/* -*- 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;
}
}
};