index.js 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  1. 'use strict';
  2. const electron = require('electron');
  3. const {download} = require('electron-dl');
  4. const isDev = require('electron-is-dev');
  5. const webContents = win => win.webContents || win.getWebContents();
  6. function create(win, opts) {
  7. webContents(win).on('context-menu', (e, props) => {
  8. if (typeof opts.shouldShowMenu === 'function' && opts.shouldShowMenu(e, props) === false) {
  9. return;
  10. }
  11. const editFlags = props.editFlags;
  12. const hasText = props.selectionText.trim().length > 0;
  13. const can = type => editFlags[`can${type}`] && hasText;
  14. let menuTpl = [{
  15. type: 'separator'
  16. }, {
  17. id: 'cut',
  18. label: 'Cut',
  19. // Needed because of macOS limitation:
  20. // https://github.com/electron/electron/issues/5860
  21. role: can('Cut') ? 'cut' : '',
  22. enabled: can('Cut'),
  23. visible: props.isEditable
  24. }, {
  25. id: 'copy',
  26. label: 'Copy',
  27. role: can('Copy') ? 'copy' : '',
  28. enabled: can('Copy'),
  29. visible: props.isEditable || hasText
  30. }, {
  31. id: 'paste',
  32. label: 'Paste',
  33. role: editFlags.canPaste ? 'paste' : '',
  34. enabled: editFlags.canPaste,
  35. visible: props.isEditable
  36. }, {
  37. type: 'separator'
  38. }];
  39. if (props.mediaType === 'image') {
  40. menuTpl = [{
  41. type: 'separator'
  42. }, {
  43. id: 'save',
  44. label: 'Save Image',
  45. click() {
  46. download(win, props.srcURL);
  47. }
  48. }, {
  49. type: 'separator'
  50. }];
  51. }
  52. if (props.linkURL && props.mediaType === 'none') {
  53. menuTpl = [{
  54. type: 'separator'
  55. }, {
  56. id: 'copyLink',
  57. label: 'Copy Link',
  58. click() {
  59. if (process.platform === 'darwin') {
  60. electron.clipboard.writeBookmark(props.linkText, props.linkURL);
  61. } else {
  62. electron.clipboard.writeText(props.linkURL);
  63. }
  64. }
  65. }, {
  66. type: 'separator'
  67. }];
  68. }
  69. if (opts.prepend) {
  70. const result = opts.prepend(props, win);
  71. if (Array.isArray(result)) {
  72. menuTpl.unshift(...result);
  73. }
  74. }
  75. if (opts.append) {
  76. const result = opts.append(props, win);
  77. if (Array.isArray(result)) {
  78. menuTpl.push(...result);
  79. }
  80. }
  81. if (opts.showInspectElement || (opts.showInspectElement !== false && isDev)) {
  82. menuTpl.push({
  83. type: 'separator'
  84. }, {
  85. id: 'inspect',
  86. label: 'Inspect Element',
  87. click() {
  88. win.inspectElement(props.x, props.y);
  89. if (webContents(win).isDevToolsOpened()) {
  90. webContents(win).devToolsWebContents.focus();
  91. }
  92. }
  93. }, {
  94. type: 'separator'
  95. });
  96. }
  97. // Apply custom labels for default menu items
  98. if (opts.labels) {
  99. for (const menuItem of menuTpl) {
  100. if (opts.labels[menuItem.id]) {
  101. menuItem.label = opts.labels[menuItem.id];
  102. }
  103. }
  104. }
  105. // Filter out leading/trailing separators
  106. // TODO: https://github.com/electron/electron/issues/5869
  107. menuTpl = delUnusedElements(menuTpl);
  108. if (menuTpl.length > 0) {
  109. const menu = (electron.remote ? electron.remote.Menu : electron.Menu).buildFromTemplate(menuTpl);
  110. /*
  111. * When electron.remote is not available this runs in the browser process.
  112. * We can safely use win in this case as it refers to the window the
  113. * context-menu should open in.
  114. * When this is being called from a webView, we can't use win as this
  115. * would refere to the webView which is not allowed to render a popup menu.
  116. */
  117. menu.popup(electron.remote ? electron.remote.getCurrentWindow() : win);
  118. }
  119. });
  120. }
  121. function delUnusedElements(menuTpl) {
  122. let notDeletedPrevEl;
  123. return menuTpl.filter(el => el.visible !== false).filter((el, i, arr) => {
  124. const toDelete = el.type === 'separator' && (!notDeletedPrevEl || i === arr.length - 1 || arr[i + 1].type === 'separator');
  125. notDeletedPrevEl = toDelete ? notDeletedPrevEl : el;
  126. return !toDelete;
  127. });
  128. }
  129. module.exports = (opts = {}) => {
  130. if (opts.window) {
  131. const win = opts.window;
  132. const wc = webContents(win);
  133. // When window is a webview that has not yet finished loading webContents is not available
  134. if (wc === undefined) {
  135. win.addEventListener('dom-ready', () => {
  136. create(win, opts);
  137. }, {once: true});
  138. return;
  139. }
  140. return create(win, opts);
  141. }
  142. (electron.BrowserWindow || electron.remote.BrowserWindow).getAllWindows().forEach(win => {
  143. create(win, opts);
  144. });
  145. (electron.app || electron.remote.app).on('browser-window-created', (e, win) => {
  146. create(win, opts);
  147. });
  148. };