Skip to content

Commit

Permalink
Add working basic context menu options to Monaco editors
Browse files Browse the repository at this point in the history
  • Loading branch information
pimterry committed Aug 17, 2023
1 parent 9b314c2 commit 41ba4b8
Show file tree
Hide file tree
Showing 4 changed files with 111 additions and 8 deletions.
11 changes: 11 additions & 0 deletions src/components/editor/base-editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { asError } from '../../util/error';
import { UiStore } from '../../model/ui/ui-store';

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

let MonacoEditor: typeof _MonacoEditor | undefined;
// Defer loading react-monaco-editor ever so slightly. This has two benefits:
Expand Down Expand Up @@ -146,6 +147,11 @@ export class SelfSizedEditor extends React.Component<
ref={this.container}
expanded={!!this.props.expanded}
style={{ 'height': this.contentHeight + 'px' }}
onContextMenu={buildContextMenuCallback(
this.props.uiStore!,
!!this.props.options?.readOnly,
this.editor
)}
>
<BaseEditor
theme={this.props.uiStore!.theme.monacoTheme}
Expand Down Expand Up @@ -212,6 +218,11 @@ export class ContainerSizedEditor extends React.Component<
return <ContainerSizedEditorContainer
ref={this.container}
expanded={!!this.props.expanded}
onContextMenu={buildContextMenuCallback(
this.props.uiStore!,
!!this.props.options?.readOnly,
this.editor
)}
>
<BaseEditor
theme={this.props.uiStore!.theme.monacoTheme}
Expand Down
81 changes: 81 additions & 0 deletions src/components/editor/editor-context-menu.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import type * as monacoTypes from 'monaco-editor';

import { copyToClipboard } from '../../util/ui';

import { UiStore } from '../../model/ui/ui-store';
import { ContextMenuItem } from '../../model/ui/context-menu';

export function buildContextMenuCallback(
uiStore: UiStore,
isReadOnly: boolean,
// Anon base-editor type to avoid exporting this
baseEditorRef: React.RefObject<{
editor: monacoTypes.editor.IStandaloneCodeEditor | undefined
}>
) {
return (mouseEvent: React.MouseEvent) => {
const editor = baseEditorRef.current?.editor;
if (!editor) return;
const selection = editor.getSelection();

const items: ContextMenuItem<void>[] = [];

if (!isReadOnly) {
items.push({
type: 'option',
label: "Cut",
enabled: !!selection && !selection.isEmpty(),
callback: async () => {
const selection = editor.getSelection();
if (!selection) return;
const content = editor.getModel()?.getValueInRange(selection);
if (!content) return;

await copyToClipboard(content);

editor.executeEdits("clipboard", [{
range: selection,
text: "",
forceMoveMarkers: true,
}]);
},
});
}

if (selection && !selection.isEmpty()) {
items.push({
type: 'option',
label: "Copy",
enabled: !!selection && !selection.isEmpty(),
callback: () => {
const selection = editor.getSelection();
if (!selection) return;
const content = editor.getModel()?.getValueInRange(selection);
if (!content) return;
copyToClipboard(content);
},
});
}

if (selection && !!navigator.clipboard) {
items.push({
type: 'option',
label: "Paste",
enabled: !isReadOnly,
callback: async () => {
const selection = editor.getSelection();
if (!selection) return;
const text = await navigator.clipboard.readText();

editor.executeEdits("clipboard", [{
range: selection,
text: text,
forceMoveMarkers: true,
}]);
}
});
}

uiStore.handleContextMenuEvent(mouseEvent, items);
}
}
8 changes: 4 additions & 4 deletions src/components/view/view-context-menu-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export class ViewEventContextMenuBuilder {
: undefined;

if (event.isHttp()) {
this.uiStore.handleContextMenuEvent(mouseEvent, event, [
this.uiStore.handleContextMenuEvent(mouseEvent, [
this.BaseOptions.Pin,
{
type: 'option',
Expand Down Expand Up @@ -110,13 +110,13 @@ export class ViewEventContextMenuBuilder {
}))
}))
},
])
], event)
} else {
// For non-HTTP events, we just show the super-basic globally supported options:
this.uiStore.handleContextMenuEvent(mouseEvent, event, [
this.uiStore.handleContextMenuEvent(mouseEvent, [
this.BaseOptions.Pin,
this.BaseOptions.Delete
]);
], event);
}
};
}
Expand Down
19 changes: 15 additions & 4 deletions src/model/ui/ui-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -319,12 +319,23 @@ export class UiStore {
@observable.ref // This shouldn't be mutated
contextMenuState: ContextMenuState<any> | undefined;

handleContextMenuEvent<T>(
event: React.MouseEvent,
items: readonly ContextMenuItem<T>[],
data: T
): void;
handleContextMenuEvent(
event: React.MouseEvent,
items: readonly ContextMenuItem<void>[]
): void;
@action.bound
handleContextMenuEvent<T>(
event: React.MouseEvent,
data: T,
items: readonly ContextMenuItem<T>[]
) {
items: readonly ContextMenuItem<T>[],
data?: T
): void {
if (!items.length) return;

event.preventDefault();

if (DesktopApi.openContextMenu) {
Expand All @@ -337,7 +348,7 @@ export class UiStore {
}).then((result) => {
if (result) {
const selectedItem = _.get(items, result) as ContextMenuOption<T>;
selectedItem.callback(data);
selectedItem.callback(data!);
}
}).catch((error) => {
console.log(error);
Expand Down

0 comments on commit 41ba4b8

Please sign in to comment.