Skip to content

Commit 41ba4b8

Browse files
committed
Add working basic context menu options to Monaco editors
1 parent 9b314c2 commit 41ba4b8

File tree

4 files changed

+111
-8
lines changed

4 files changed

+111
-8
lines changed

src/components/editor/base-editor.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { asError } from '../../util/error';
1717
import { UiStore } from '../../model/ui/ui-store';
1818

1919
import { FocusWrapper } from './focus-wrapper';
20+
import { buildContextMenuCallback } from './editor-context-menu';
2021

2122
let MonacoEditor: typeof _MonacoEditor | undefined;
2223
// Defer loading react-monaco-editor ever so slightly. This has two benefits:
@@ -146,6 +147,11 @@ export class SelfSizedEditor extends React.Component<
146147
ref={this.container}
147148
expanded={!!this.props.expanded}
148149
style={{ 'height': this.contentHeight + 'px' }}
150+
onContextMenu={buildContextMenuCallback(
151+
this.props.uiStore!,
152+
!!this.props.options?.readOnly,
153+
this.editor
154+
)}
149155
>
150156
<BaseEditor
151157
theme={this.props.uiStore!.theme.monacoTheme}
@@ -212,6 +218,11 @@ export class ContainerSizedEditor extends React.Component<
212218
return <ContainerSizedEditorContainer
213219
ref={this.container}
214220
expanded={!!this.props.expanded}
221+
onContextMenu={buildContextMenuCallback(
222+
this.props.uiStore!,
223+
!!this.props.options?.readOnly,
224+
this.editor
225+
)}
215226
>
216227
<BaseEditor
217228
theme={this.props.uiStore!.theme.monacoTheme}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import type * as monacoTypes from 'monaco-editor';
2+
3+
import { copyToClipboard } from '../../util/ui';
4+
5+
import { UiStore } from '../../model/ui/ui-store';
6+
import { ContextMenuItem } from '../../model/ui/context-menu';
7+
8+
export function buildContextMenuCallback(
9+
uiStore: UiStore,
10+
isReadOnly: boolean,
11+
// Anon base-editor type to avoid exporting this
12+
baseEditorRef: React.RefObject<{
13+
editor: monacoTypes.editor.IStandaloneCodeEditor | undefined
14+
}>
15+
) {
16+
return (mouseEvent: React.MouseEvent) => {
17+
const editor = baseEditorRef.current?.editor;
18+
if (!editor) return;
19+
const selection = editor.getSelection();
20+
21+
const items: ContextMenuItem<void>[] = [];
22+
23+
if (!isReadOnly) {
24+
items.push({
25+
type: 'option',
26+
label: "Cut",
27+
enabled: !!selection && !selection.isEmpty(),
28+
callback: async () => {
29+
const selection = editor.getSelection();
30+
if (!selection) return;
31+
const content = editor.getModel()?.getValueInRange(selection);
32+
if (!content) return;
33+
34+
await copyToClipboard(content);
35+
36+
editor.executeEdits("clipboard", [{
37+
range: selection,
38+
text: "",
39+
forceMoveMarkers: true,
40+
}]);
41+
},
42+
});
43+
}
44+
45+
if (selection && !selection.isEmpty()) {
46+
items.push({
47+
type: 'option',
48+
label: "Copy",
49+
enabled: !!selection && !selection.isEmpty(),
50+
callback: () => {
51+
const selection = editor.getSelection();
52+
if (!selection) return;
53+
const content = editor.getModel()?.getValueInRange(selection);
54+
if (!content) return;
55+
copyToClipboard(content);
56+
},
57+
});
58+
}
59+
60+
if (selection && !!navigator.clipboard) {
61+
items.push({
62+
type: 'option',
63+
label: "Paste",
64+
enabled: !isReadOnly,
65+
callback: async () => {
66+
const selection = editor.getSelection();
67+
if (!selection) return;
68+
const text = await navigator.clipboard.readText();
69+
70+
editor.executeEdits("clipboard", [{
71+
range: selection,
72+
text: text,
73+
forceMoveMarkers: true,
74+
}]);
75+
}
76+
});
77+
}
78+
79+
uiStore.handleContextMenuEvent(mouseEvent, items);
80+
}
81+
}

src/components/view/view-context-menu-builder.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ export class ViewEventContextMenuBuilder {
4949
: undefined;
5050

5151
if (event.isHttp()) {
52-
this.uiStore.handleContextMenuEvent(mouseEvent, event, [
52+
this.uiStore.handleContextMenuEvent(mouseEvent, [
5353
this.BaseOptions.Pin,
5454
{
5555
type: 'option',
@@ -110,13 +110,13 @@ export class ViewEventContextMenuBuilder {
110110
}))
111111
}))
112112
},
113-
])
113+
], event)
114114
} else {
115115
// For non-HTTP events, we just show the super-basic globally supported options:
116-
this.uiStore.handleContextMenuEvent(mouseEvent, event, [
116+
this.uiStore.handleContextMenuEvent(mouseEvent, [
117117
this.BaseOptions.Pin,
118118
this.BaseOptions.Delete
119-
]);
119+
], event);
120120
}
121121
};
122122
}

src/model/ui/ui-store.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -319,12 +319,23 @@ export class UiStore {
319319
@observable.ref // This shouldn't be mutated
320320
contextMenuState: ContextMenuState<any> | undefined;
321321

322+
handleContextMenuEvent<T>(
323+
event: React.MouseEvent,
324+
items: readonly ContextMenuItem<T>[],
325+
data: T
326+
): void;
327+
handleContextMenuEvent(
328+
event: React.MouseEvent,
329+
items: readonly ContextMenuItem<void>[]
330+
): void;
322331
@action.bound
323332
handleContextMenuEvent<T>(
324333
event: React.MouseEvent,
325-
data: T,
326-
items: readonly ContextMenuItem<T>[]
327-
) {
334+
items: readonly ContextMenuItem<T>[],
335+
data?: T
336+
): void {
337+
if (!items.length) return;
338+
328339
event.preventDefault();
329340

330341
if (DesktopApi.openContextMenu) {
@@ -337,7 +348,7 @@ export class UiStore {
337348
}).then((result) => {
338349
if (result) {
339350
const selectedItem = _.get(items, result) as ContextMenuOption<T>;
340-
selectedItem.callback(data);
351+
selectedItem.callback(data!);
341352
}
342353
}).catch((error) => {
343354
console.log(error);

0 commit comments

Comments
 (0)