diff options
Diffstat (limited to 'src/webview/contextMenu.js')
-rw-r--r-- | src/webview/contextMenu.js | 178 |
1 files changed, 178 insertions, 0 deletions
diff --git a/src/webview/contextMenu.js b/src/webview/contextMenu.js new file mode 100644 index 000000000..195306fda --- /dev/null +++ b/src/webview/contextMenu.js | |||
@@ -0,0 +1,178 @@ | |||
1 | // This is heavily based on https://github.com/sindresorhus/electron-context-menu | ||
2 | // ❤ @sindresorhus | ||
3 | |||
4 | import { clipboard, remote, ipcRenderer, shell } from 'electron'; | ||
5 | |||
6 | import { isDevMode } from '../environment'; | ||
7 | |||
8 | const debug = require('debug')('Franz:contextMenu'); | ||
9 | |||
10 | const { Menu } = remote; | ||
11 | |||
12 | // const win = remote.getCurrentWindow(); | ||
13 | const webContents = remote.getCurrentWebContents(); | ||
14 | |||
15 | function delUnusedElements(menuTpl) { | ||
16 | let notDeletedPrevEl; | ||
17 | return menuTpl.filter(el => el.visible !== false).filter((el, i, array) => { | ||
18 | const toDelete = el.type === 'separator' && (!notDeletedPrevEl || i === array.length - 1 || array[i + 1].type === 'separator'); | ||
19 | notDeletedPrevEl = toDelete ? notDeletedPrevEl : el; | ||
20 | return !toDelete; | ||
21 | }); | ||
22 | } | ||
23 | |||
24 | const buildMenuTpl = (props, suggestions) => { | ||
25 | const { editFlags } = props; | ||
26 | const hasText = props.selectionText.trim().length > 0; | ||
27 | const can = type => editFlags[`can${type}`] && hasText; | ||
28 | |||
29 | console.log(props); | ||
30 | |||
31 | let menuTpl = [ | ||
32 | { | ||
33 | type: 'separator', | ||
34 | }, { | ||
35 | id: 'cut', | ||
36 | role: can('Cut') ? 'cut' : '', | ||
37 | enabled: can('Cut'), | ||
38 | visible: !!props.selectionText.trim(), | ||
39 | }, { | ||
40 | id: 'copy', | ||
41 | label: 'Copy', | ||
42 | role: can('Copy') ? 'copy' : '', | ||
43 | enabled: can('Copy'), | ||
44 | visible: props.isEditable || hasText, | ||
45 | }, { | ||
46 | id: 'paste', | ||
47 | label: 'Paste', | ||
48 | role: editFlags.canPaste ? 'paste' : '', | ||
49 | enabled: editFlags.canPaste, | ||
50 | visible: props.isEditable, | ||
51 | }, { | ||
52 | type: 'separator', | ||
53 | }, | ||
54 | ]; | ||
55 | |||
56 | if (props.linkURL && props.mediaType === 'none') { | ||
57 | menuTpl = [{ | ||
58 | type: 'separator', | ||
59 | }, { | ||
60 | id: 'openLink', | ||
61 | label: 'Open Link in Browser', | ||
62 | click() { | ||
63 | shell.openExternal(props.linkURL); | ||
64 | }, | ||
65 | }, { | ||
66 | id: 'copyLink', | ||
67 | label: 'Copy Link', | ||
68 | click() { | ||
69 | clipboard.write({ | ||
70 | bookmark: props.linkText, | ||
71 | text: props.linkURL, | ||
72 | }); | ||
73 | }, | ||
74 | }, { | ||
75 | type: 'separator', | ||
76 | }]; | ||
77 | } | ||
78 | |||
79 | if (props.mediaType === 'image') { | ||
80 | menuTpl.push({ | ||
81 | type: 'separator', | ||
82 | }, { | ||
83 | id: 'openImage', | ||
84 | label: 'Open Image in Browser', | ||
85 | click() { | ||
86 | shell.openExternal(props.srcURL); | ||
87 | }, | ||
88 | }, { | ||
89 | id: 'copyImageAddress', | ||
90 | label: 'Copy Image Address', | ||
91 | click() { | ||
92 | clipboard.write({ | ||
93 | bookmark: props.srcURL, | ||
94 | text: props.srcURL, | ||
95 | }); | ||
96 | }, | ||
97 | }, { | ||
98 | type: 'separator', | ||
99 | }); | ||
100 | } | ||
101 | |||
102 | if (props.mediaType === 'image') { | ||
103 | menuTpl.push({ | ||
104 | id: 'saveImageAs', | ||
105 | label: 'Save Image As…', | ||
106 | async click() { | ||
107 | if (props.srcURL.startsWith('blob:')) { | ||
108 | const url = new window.URL(props.srcURL.substr(5)); | ||
109 | const fileName = url.pathname.substr(1); | ||
110 | const resp = await window.fetch(props.srcURL); | ||
111 | const blob = await resp.blob(); | ||
112 | const reader = new window.FileReader(); | ||
113 | reader.readAsDataURL(blob); | ||
114 | reader.onloadend = () => { | ||
115 | const base64data = reader.result; | ||
116 | |||
117 | ipcRenderer.send('download-file', { | ||
118 | content: base64data, | ||
119 | fileOptions: { | ||
120 | name: fileName, | ||
121 | mime: blob.type, | ||
122 | }, | ||
123 | }); | ||
124 | }; | ||
125 | debug('binary string', blob); | ||
126 | } else { | ||
127 | ipcRenderer.send('download-file', { url: props.srcURL }); | ||
128 | } | ||
129 | }, | ||
130 | }, { | ||
131 | type: 'separator', | ||
132 | }); | ||
133 | } | ||
134 | |||
135 | console.log('suggestions', suggestions.length, suggestions); | ||
136 | if (suggestions.length > 0) { | ||
137 | suggestions.reverse().map(suggestion => menuTpl.unshift({ | ||
138 | id: `suggestion-${suggestion}`, | ||
139 | label: suggestion, | ||
140 | click() { | ||
141 | webContents.replaceMisspelling(suggestion); | ||
142 | }, | ||
143 | })); | ||
144 | } | ||
145 | |||
146 | if (isDevMode) { | ||
147 | menuTpl.push({ | ||
148 | type: 'separator', | ||
149 | }, { | ||
150 | id: 'inspect', | ||
151 | label: 'Inspect Element', | ||
152 | click() { | ||
153 | webContents.inspectElement(props.x, props.y); | ||
154 | }, | ||
155 | }, { | ||
156 | type: 'separator', | ||
157 | }); | ||
158 | } | ||
159 | |||
160 | return delUnusedElements(menuTpl); | ||
161 | }; | ||
162 | |||
163 | export default function contextMenu(spellcheckProvider) { | ||
164 | webContents.on('context-menu', (e, props) => { | ||
165 | e.preventDefault(); | ||
166 | |||
167 | let suggestions = []; | ||
168 | if (spellcheckProvider && props.misspelledWord) { | ||
169 | suggestions = spellcheckProvider.getSuggestion(props.misspelledWord); | ||
170 | |||
171 | debug('Suggestions', suggestions); | ||
172 | } | ||
173 | |||
174 | const menu = Menu.buildFromTemplate(buildMenuTpl(props, suggestions.slice(0, 5))); | ||
175 | |||
176 | menu.popup(remote.getCurrentWindow()); | ||
177 | }); | ||
178 | } | ||