style-inspector-menu.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510
  1. /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
  2. /* This Source Code Form is subject to the terms of the Mozilla Public
  3. * License, v. 2.0. If a copy of the MPL was not distributed with this
  4. * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
  5. "use strict";
  6. const {PREF_ORIG_SOURCES} = require("devtools/client/styleeditor/utils");
  7. const Services = require("Services");
  8. const {Task} = require("devtools/shared/task");
  9. const Menu = require("devtools/client/framework/menu");
  10. const MenuItem = require("devtools/client/framework/menu-item");
  11. const {
  12. VIEW_NODE_SELECTOR_TYPE,
  13. VIEW_NODE_PROPERTY_TYPE,
  14. VIEW_NODE_VALUE_TYPE,
  15. VIEW_NODE_IMAGE_URL_TYPE,
  16. VIEW_NODE_LOCATION_TYPE,
  17. } = require("devtools/client/inspector/shared/node-types");
  18. const clipboardHelper = require("devtools/shared/platform/clipboard");
  19. const STYLE_INSPECTOR_PROPERTIES = "devtools/shared/locales/styleinspector.properties";
  20. const {LocalizationHelper} = require("devtools/shared/l10n");
  21. const STYLE_INSPECTOR_L10N = new LocalizationHelper(STYLE_INSPECTOR_PROPERTIES);
  22. const PREF_ENABLE_MDN_DOCS_TOOLTIP =
  23. "devtools.inspector.mdnDocsTooltip.enabled";
  24. /**
  25. * Style inspector context menu
  26. *
  27. * @param {RuleView|ComputedView} view
  28. * RuleView or ComputedView instance controlling this menu
  29. * @param {Object} options
  30. * Option menu configuration
  31. */
  32. function StyleInspectorMenu(view, options) {
  33. this.view = view;
  34. this.inspector = this.view.inspector;
  35. this.styleDocument = this.view.styleDocument;
  36. this.styleWindow = this.view.styleWindow;
  37. this.isRuleView = options.isRuleView;
  38. this._onAddNewRule = this._onAddNewRule.bind(this);
  39. this._onCopy = this._onCopy.bind(this);
  40. this._onCopyColor = this._onCopyColor.bind(this);
  41. this._onCopyImageDataUrl = this._onCopyImageDataUrl.bind(this);
  42. this._onCopyLocation = this._onCopyLocation.bind(this);
  43. this._onCopyPropertyDeclaration = this._onCopyPropertyDeclaration.bind(this);
  44. this._onCopyPropertyName = this._onCopyPropertyName.bind(this);
  45. this._onCopyPropertyValue = this._onCopyPropertyValue.bind(this);
  46. this._onCopyRule = this._onCopyRule.bind(this);
  47. this._onCopySelector = this._onCopySelector.bind(this);
  48. this._onCopyUrl = this._onCopyUrl.bind(this);
  49. this._onSelectAll = this._onSelectAll.bind(this);
  50. this._onShowMdnDocs = this._onShowMdnDocs.bind(this);
  51. this._onToggleOrigSources = this._onToggleOrigSources.bind(this);
  52. }
  53. module.exports = StyleInspectorMenu;
  54. StyleInspectorMenu.prototype = {
  55. /**
  56. * Display the style inspector context menu
  57. */
  58. show: function (event) {
  59. try {
  60. this._openMenu({
  61. target: event.explicitOriginalTarget,
  62. screenX: event.screenX,
  63. screenY: event.screenY,
  64. });
  65. } catch (e) {
  66. console.error(e);
  67. }
  68. },
  69. _openMenu: function ({ target, screenX = 0, screenY = 0 } = { }) {
  70. // In the sidebar we do not have this.styleDocument.popupNode
  71. // so we need to save the node ourselves.
  72. this.styleDocument.popupNode = target;
  73. this.styleWindow.focus();
  74. let menu = new Menu();
  75. let menuitemCopy = new MenuItem({
  76. label: STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copy"),
  77. accesskey: STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copy.accessKey"),
  78. click: () => {
  79. this._onCopy();
  80. },
  81. disabled: !this._hasTextSelected(),
  82. });
  83. let menuitemCopyLocation = new MenuItem({
  84. label: STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copyLocation"),
  85. click: () => {
  86. this._onCopyLocation();
  87. },
  88. visible: false,
  89. });
  90. let menuitemCopyRule = new MenuItem({
  91. label: STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copyRule"),
  92. click: () => {
  93. this._onCopyRule();
  94. },
  95. visible: this.isRuleView,
  96. });
  97. let copyColorAccessKey = "styleinspector.contextmenu.copyColor.accessKey";
  98. let menuitemCopyColor = new MenuItem({
  99. label: STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copyColor"),
  100. accesskey: STYLE_INSPECTOR_L10N.getStr(copyColorAccessKey),
  101. click: () => {
  102. this._onCopyColor();
  103. },
  104. visible: this._isColorPopup(),
  105. });
  106. let copyUrlAccessKey = "styleinspector.contextmenu.copyUrl.accessKey";
  107. let menuitemCopyUrl = new MenuItem({
  108. label: STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copyUrl"),
  109. accesskey: STYLE_INSPECTOR_L10N.getStr(copyUrlAccessKey),
  110. click: () => {
  111. this._onCopyUrl();
  112. },
  113. visible: this._isImageUrl(),
  114. });
  115. let copyImageAccessKey = "styleinspector.contextmenu.copyImageDataUrl.accessKey";
  116. let menuitemCopyImageDataUrl = new MenuItem({
  117. label: STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copyImageDataUrl"),
  118. accesskey: STYLE_INSPECTOR_L10N.getStr(copyImageAccessKey),
  119. click: () => {
  120. this._onCopyImageDataUrl();
  121. },
  122. visible: this._isImageUrl(),
  123. });
  124. let copyPropDeclarationLabel = "styleinspector.contextmenu.copyPropertyDeclaration";
  125. let menuitemCopyPropertyDeclaration = new MenuItem({
  126. label: STYLE_INSPECTOR_L10N.getStr(copyPropDeclarationLabel),
  127. click: () => {
  128. this._onCopyPropertyDeclaration();
  129. },
  130. visible: false,
  131. });
  132. let menuitemCopyPropertyName = new MenuItem({
  133. label: STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copyPropertyName"),
  134. click: () => {
  135. this._onCopyPropertyName();
  136. },
  137. visible: false,
  138. });
  139. let menuitemCopyPropertyValue = new MenuItem({
  140. label: STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copyPropertyValue"),
  141. click: () => {
  142. this._onCopyPropertyValue();
  143. },
  144. visible: false,
  145. });
  146. let menuitemCopySelector = new MenuItem({
  147. label: STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copySelector"),
  148. click: () => {
  149. this._onCopySelector();
  150. },
  151. visible: false,
  152. });
  153. this._clickedNodeInfo = this._getClickedNodeInfo();
  154. if (this.isRuleView && this._clickedNodeInfo) {
  155. switch (this._clickedNodeInfo.type) {
  156. case VIEW_NODE_PROPERTY_TYPE :
  157. menuitemCopyPropertyDeclaration.visible = true;
  158. menuitemCopyPropertyName.visible = true;
  159. break;
  160. case VIEW_NODE_VALUE_TYPE :
  161. menuitemCopyPropertyDeclaration.visible = true;
  162. menuitemCopyPropertyValue.visible = true;
  163. break;
  164. case VIEW_NODE_SELECTOR_TYPE :
  165. menuitemCopySelector.visible = true;
  166. break;
  167. case VIEW_NODE_LOCATION_TYPE :
  168. menuitemCopyLocation.visible = true;
  169. break;
  170. }
  171. }
  172. menu.append(menuitemCopy);
  173. menu.append(menuitemCopyLocation);
  174. menu.append(menuitemCopyRule);
  175. menu.append(menuitemCopyColor);
  176. menu.append(menuitemCopyUrl);
  177. menu.append(menuitemCopyImageDataUrl);
  178. menu.append(menuitemCopyPropertyDeclaration);
  179. menu.append(menuitemCopyPropertyName);
  180. menu.append(menuitemCopyPropertyValue);
  181. menu.append(menuitemCopySelector);
  182. menu.append(new MenuItem({
  183. type: "separator",
  184. }));
  185. // Select All
  186. let selectAllAccessKey = "styleinspector.contextmenu.selectAll.accessKey";
  187. let menuitemSelectAll = new MenuItem({
  188. label: STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.selectAll"),
  189. accesskey: STYLE_INSPECTOR_L10N.getStr(selectAllAccessKey),
  190. click: () => {
  191. this._onSelectAll();
  192. },
  193. });
  194. menu.append(menuitemSelectAll);
  195. menu.append(new MenuItem({
  196. type: "separator",
  197. }));
  198. // Add new rule
  199. let addRuleAccessKey = "styleinspector.contextmenu.addNewRule.accessKey";
  200. let menuitemAddRule = new MenuItem({
  201. label: STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.addNewRule"),
  202. accesskey: STYLE_INSPECTOR_L10N.getStr(addRuleAccessKey),
  203. click: () => {
  204. this._onAddNewRule();
  205. },
  206. visible: this.isRuleView,
  207. disabled: !this.isRuleView ||
  208. this.inspector.selection.isAnonymousNode(),
  209. });
  210. menu.append(menuitemAddRule);
  211. // Show MDN Docs
  212. let mdnDocsAccessKey = "styleinspector.contextmenu.showMdnDocs.accessKey";
  213. let menuitemShowMdnDocs = new MenuItem({
  214. label: STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.showMdnDocs"),
  215. accesskey: STYLE_INSPECTOR_L10N.getStr(mdnDocsAccessKey),
  216. click: () => {
  217. this._onShowMdnDocs();
  218. },
  219. visible: (Services.prefs.getBoolPref(PREF_ENABLE_MDN_DOCS_TOOLTIP) &&
  220. this._isPropertyName()),
  221. });
  222. menu.append(menuitemShowMdnDocs);
  223. // Show Original Sources
  224. let sourcesAccessKey = "styleinspector.contextmenu.toggleOrigSources.accessKey";
  225. let menuitemSources = new MenuItem({
  226. label: STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.toggleOrigSources"),
  227. accesskey: STYLE_INSPECTOR_L10N.getStr(sourcesAccessKey),
  228. click: () => {
  229. this._onToggleOrigSources();
  230. },
  231. type: "checkbox",
  232. checked: Services.prefs.getBoolPref(PREF_ORIG_SOURCES),
  233. });
  234. menu.append(menuitemSources);
  235. menu.popup(screenX, screenY, this.inspector._toolbox);
  236. return menu;
  237. },
  238. _hasTextSelected: function () {
  239. let hasTextSelected;
  240. let selection = this.styleWindow.getSelection();
  241. let node = this._getClickedNode();
  242. if (node.nodeName == "input" || node.nodeName == "textarea") {
  243. let { selectionStart, selectionEnd } = node;
  244. hasTextSelected = isFinite(selectionStart) && isFinite(selectionEnd)
  245. && selectionStart !== selectionEnd;
  246. } else {
  247. hasTextSelected = selection.toString() && !selection.isCollapsed;
  248. }
  249. return hasTextSelected;
  250. },
  251. /**
  252. * Get the type of the currently clicked node
  253. */
  254. _getClickedNodeInfo: function () {
  255. let node = this._getClickedNode();
  256. return this.view.getNodeInfo(node);
  257. },
  258. /**
  259. * A helper that determines if the popup was opened with a click to a color
  260. * value and saves the color to this._colorToCopy.
  261. *
  262. * @return {Boolean}
  263. * true if click on color opened the popup, false otherwise.
  264. */
  265. _isColorPopup: function () {
  266. this._colorToCopy = "";
  267. let container = this._getClickedNode();
  268. if (!container) {
  269. return false;
  270. }
  271. let isColorNode = el => el.dataset && "color" in el.dataset;
  272. while (!isColorNode(container)) {
  273. container = container.parentNode;
  274. if (!container) {
  275. return false;
  276. }
  277. }
  278. this._colorToCopy = container.dataset.color;
  279. return true;
  280. },
  281. _isPropertyName: function () {
  282. let nodeInfo = this._getClickedNodeInfo();
  283. if (!nodeInfo) {
  284. return false;
  285. }
  286. return nodeInfo.type == VIEW_NODE_PROPERTY_TYPE;
  287. },
  288. /**
  289. * Check if the current node (clicked node) is an image URL
  290. *
  291. * @return {Boolean} true if the node is an image url
  292. */
  293. _isImageUrl: function () {
  294. let nodeInfo = this._getClickedNodeInfo();
  295. if (!nodeInfo) {
  296. return false;
  297. }
  298. return nodeInfo.type == VIEW_NODE_IMAGE_URL_TYPE;
  299. },
  300. /**
  301. * Get the DOM Node container for the current popupNode.
  302. * If popupNode is a textNode, return the parent node, otherwise return
  303. * popupNode itself.
  304. *
  305. * @return {DOMNode}
  306. */
  307. _getClickedNode: function () {
  308. let container = null;
  309. let node = this.styleDocument.popupNode;
  310. if (node) {
  311. let isTextNode = node.nodeType == node.TEXT_NODE;
  312. container = isTextNode ? node.parentElement : node;
  313. }
  314. return container;
  315. },
  316. /**
  317. * Select all text.
  318. */
  319. _onSelectAll: function () {
  320. let selection = this.styleWindow.getSelection();
  321. selection.selectAllChildren(this.view.element);
  322. },
  323. /**
  324. * Copy the most recently selected color value to clipboard.
  325. */
  326. _onCopy: function () {
  327. this.view.copySelection(this.styleDocument.popupNode);
  328. },
  329. /**
  330. * Copy the most recently selected color value to clipboard.
  331. */
  332. _onCopyColor: function () {
  333. clipboardHelper.copyString(this._colorToCopy);
  334. },
  335. /*
  336. * Retrieve the url for the selected image and copy it to the clipboard
  337. */
  338. _onCopyUrl: function () {
  339. if (!this._clickedNodeInfo) {
  340. return;
  341. }
  342. clipboardHelper.copyString(this._clickedNodeInfo.value.url);
  343. },
  344. /**
  345. * Retrieve the image data for the selected image url and copy it to the
  346. * clipboard
  347. */
  348. _onCopyImageDataUrl: Task.async(function* () {
  349. if (!this._clickedNodeInfo) {
  350. return;
  351. }
  352. let message;
  353. try {
  354. let inspectorFront = this.inspector.inspector;
  355. let imageUrl = this._clickedNodeInfo.value.url;
  356. let data = yield inspectorFront.getImageDataFromURL(imageUrl);
  357. message = yield data.data.string();
  358. } catch (e) {
  359. message =
  360. STYLE_INSPECTOR_L10N.getStr("styleinspector.copyImageDataUrlError");
  361. }
  362. clipboardHelper.copyString(message);
  363. }),
  364. /**
  365. * Show docs from MDN for a CSS property.
  366. */
  367. _onShowMdnDocs: function () {
  368. let cssPropertyName = this.styleDocument.popupNode.textContent;
  369. let anchor = this.styleDocument.popupNode.parentNode;
  370. let cssDocsTooltip = this.view.tooltips.cssDocs;
  371. cssDocsTooltip.show(anchor, cssPropertyName);
  372. },
  373. /**
  374. * Add a new rule to the current element.
  375. */
  376. _onAddNewRule: function () {
  377. this.view._onAddRule();
  378. },
  379. /**
  380. * Copy the rule source location of the current clicked node.
  381. */
  382. _onCopyLocation: function () {
  383. if (!this._clickedNodeInfo) {
  384. return;
  385. }
  386. clipboardHelper.copyString(this._clickedNodeInfo.value);
  387. },
  388. /**
  389. * Copy the rule property declaration of the current clicked node.
  390. */
  391. _onCopyPropertyDeclaration: function () {
  392. if (!this._clickedNodeInfo) {
  393. return;
  394. }
  395. let textProp = this._clickedNodeInfo.value.textProperty;
  396. clipboardHelper.copyString(textProp.stringifyProperty());
  397. },
  398. /**
  399. * Copy the rule property name of the current clicked node.
  400. */
  401. _onCopyPropertyName: function () {
  402. if (!this._clickedNodeInfo) {
  403. return;
  404. }
  405. clipboardHelper.copyString(this._clickedNodeInfo.value.property);
  406. },
  407. /**
  408. * Copy the rule property value of the current clicked node.
  409. */
  410. _onCopyPropertyValue: function () {
  411. if (!this._clickedNodeInfo) {
  412. return;
  413. }
  414. clipboardHelper.copyString(this._clickedNodeInfo.value.value);
  415. },
  416. /**
  417. * Copy the rule of the current clicked node.
  418. */
  419. _onCopyRule: function () {
  420. let ruleEditor =
  421. this.styleDocument.popupNode.parentNode.offsetParent._ruleEditor;
  422. let rule = ruleEditor.rule;
  423. clipboardHelper.copyString(rule.stringifyRule());
  424. },
  425. /**
  426. * Copy the rule selector of the current clicked node.
  427. */
  428. _onCopySelector: function () {
  429. if (!this._clickedNodeInfo) {
  430. return;
  431. }
  432. clipboardHelper.copyString(this._clickedNodeInfo.value);
  433. },
  434. /**
  435. * Toggle the original sources pref.
  436. */
  437. _onToggleOrigSources: function () {
  438. let isEnabled = Services.prefs.getBoolPref(PREF_ORIG_SOURCES);
  439. Services.prefs.setBoolPref(PREF_ORIG_SOURCES, !isEnabled);
  440. },
  441. destroy: function () {
  442. this.popupNode = null;
  443. this.styleDocument.popupNode = null;
  444. this.view = null;
  445. this.inspector = null;
  446. this.styleDocument = null;
  447. this.styleWindow = null;
  448. }
  449. };