add context menu

This commit is contained in:
David Lechner
2020-06-08 20:56:00 -05:00
committed by David Lechner
parent 89f93b0faf
commit e8ddf09323
11 changed files with 132 additions and 20 deletions
+1
View File
@@ -0,0 +1 @@
nodejs 10.21.0
+1 -1
View File
@@ -19,7 +19,7 @@
"@types/react-splitter-layout": "^3.0.0",
"@types/redux-logger": "^3.0.8",
"@types/web-bluetooth": "^0.0.6",
"ace-builds": "^1.4.9",
"ace-builds": "^1.4.11",
"file-saver": "^2.0.2",
"jszip": "^3.4.0",
"node-sass": "^4.14.1",
+80 -13
View File
@@ -1,27 +1,33 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020 The Pybricks Authors
import { ResizeSensor } from '@blueprintjs/core';
import { Menu, MenuDivider, MenuItem, ResizeSensor } from '@blueprintjs/core';
// importing this way due to https://github.com/palantir/blueprint/issues/3891
import { ContextMenuTarget } from '@blueprintjs/core/lib/esnext/components/context-menu/contextMenuTarget';
import { WithI18nProps, withI18n } from '@shopify/react-i18n';
import { Ace } from 'ace-builds';
import { Ace, config } from 'ace-builds';
import React from 'react';
import AceEditor from 'react-ace';
import { connect } from 'react-redux';
import { Action, Dispatch } from '../actions';
import { setEditSession } from '../actions/editor';
import { EditorStringId } from './editor';
import en from './editor.en.json';
import 'ace-builds/src-noconflict/mode-python';
import 'ace-builds/src-noconflict/theme-xcode';
import 'ace-builds/src-min-noconflict/ext-searchbox';
import 'ace-builds/src-min-noconflict/ext-language_tools';
import 'ace-builds/src-noconflict/ext-searchbox';
import 'ace-builds/src-noconflict/ext-keybinding_menu';
import 'ace-builds/src-noconflict/ext-language_tools';
type DispatchProps = { onSessionChanged: (session?: Ace.EditSession) => void };
type EditorProps = DispatchProps & WithI18nProps;
@ContextMenuTarget
class Editor extends React.Component<EditorProps> {
private editorRef: React.RefObject<AceEditor>;
private keyBindings?: Array<{ key: string; command: string }>;
constructor(props: EditorProps) {
super(props);
@@ -29,11 +35,11 @@ class Editor extends React.Component<EditorProps> {
}
render(): JSX.Element {
const { i18n, onSessionChanged } = this.props;
const editor = this.editorRef.current?.editor;
return (
<ResizeSensor
onResize={(): void => this.editorRef.current?.editor?.resize()}
>
<div className="h-100">
<div className="h-100">
<ResizeSensor onResize={(): void => editor?.resize()}>
<AceEditor
ref={this.editorRef}
mode="python"
@@ -42,17 +48,30 @@ class Editor extends React.Component<EditorProps> {
width="100%"
height="100%"
focus={true}
placeholder={this.props.i18n.translate('editor.placeholder')}
placeholder={i18n.translate(EditorStringId.Placeholder)}
defaultValue={localStorage.getItem('program') || undefined}
editorProps={{ $blockScrolling: true }}
setOptions={{
enableBasicAutocompletion: true,
enableLiveAutocompletion: true,
}}
onLoad={(e): void => {
config.loadModule(
'ace/ext/menu_tools/get_editor_keyboard_shortcuts',
(m) => {
this.keyBindings = m.getEditorKeybordShortcuts(e);
},
);
config.loadModule('ace/ext/keybinding_menu', (m) =>
m.init(e),
);
}}
onFocus={(_, e): void => {
this.props.onSessionChanged(e?.session);
onSessionChanged(e?.session);
}}
onChange={(v): void => {
localStorage.setItem('program', v);
}}
onChange={(v): void => localStorage.setItem('program', v)}
commands={[
{
name: 'save',
@@ -66,8 +85,56 @@ class Editor extends React.Component<EditorProps> {
},
]}
/>
</div>
</ResizeSensor>
</ResizeSensor>
</div>
);
}
renderContextMenu(): JSX.Element {
const { i18n } = this.props;
const editor = this.editorRef.current?.editor;
return (
<Menu>
<MenuItem
onClick={(): void => {
const selected = editor?.getSelectedText();
if (selected) {
navigator.clipboard.writeText(selected);
}
}}
text={i18n.translate(EditorStringId.Copy)}
icon="duplicate"
label={/mac/i.test(navigator.platform) ? 'Cmd-C' : 'Ctrl-C'}
disabled={editor?.getSelection().isEmpty()}
/>
<MenuItem
onClick={async (): Promise<void> => {
editor?.execCommand(
'paste',
await navigator.clipboard.readText(),
);
}}
text={i18n.translate(EditorStringId.Paste)}
icon="clipboard"
label={/mac/i.test(navigator.platform) ? 'Cmd-V' : 'Ctrl-V'}
/>
<MenuDivider />
<MenuItem
onClick={(): void => editor?.undo()}
text={i18n.translate(EditorStringId.Undo)}
icon="undo"
label={this.keyBindings?.find((x) => x.command === 'undo')?.key}
disabled={!editor?.session.getUndoManager().canUndo()}
/>
<MenuItem
onClick={(): void => editor?.redo()}
text={i18n.translate(EditorStringId.Redo)}
icon="redo"
label={this.keyBindings?.find((x) => x.command === 'redo')?.key}
active
disabled={!editor?.session.getUndoManager().canRedo()}
/>
</Menu>
);
}
}
+1 -1
View File
@@ -13,7 +13,7 @@ type StatusProps = StateProps;
class StatusBar extends React.Component<StatusProps> {
render(): JSX.Element {
return (
<div className="status-bar">
<div className="status-bar" onContextMenu={(e): void => e.preventDefault()}>
<ProgressBar
className="status-bar-item"
value={this.props.progress}
+5 -1
View File
@@ -63,7 +63,11 @@ class Terminal extends React.Component<TerminalProps> {
render(): JSX.Element {
return (
<ResizeSensor onResize={(): void => this.fitAddon.fit()}>
<div ref={this.terminalRef} className="h-100" />
<div
ref={this.terminalRef}
className="h-100"
onContextMenu={(e): void => e.preventDefault()}
/>
</ResizeSensor>
);
}
+5 -1
View File
@@ -14,7 +14,11 @@ import StopButton from './StopButton';
class Toolbar extends React.Component {
render(): JSX.Element {
return (
<Navbar fixedToTop={true} className="no-box-shadow">
<Navbar
onContextMenu={(e): void => e.preventDefault()}
fixedToTop={true}
className="no-box-shadow"
>
<Navbar.Group>
<ButtonGroup>
<OpenButton id="open" />
+5 -1
View File
@@ -1,5 +1,9 @@
{
"editor": {
"placeholder": "Write your program here..."
"placeholder": "Write your program here...",
"copy": "Copy",
"paste": "Paste",
"undo": "Undo",
"redo": "Redo"
}
}
+21
View File
@@ -0,0 +1,21 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020 The Pybricks Authors
import { EditorStringId } from './editor';
import en from './editor.en.json';
function lookup(obj: object, id: string): string | undefined {
const value = id
.split('.')
.reduce((pv, cv) => pv && (pv as Record<string, object>)[cv], obj);
if (typeof value === 'string') {
return value;
}
return undefined;
}
describe('Ensure .json file has matches for EditorStringId', () => {
test.each(Object.values(EditorStringId))('%s', (id) => {
expect(lookup(en, id)).toBeDefined();
});
});
+10
View File
@@ -0,0 +1,10 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020 The Pybricks Authors
export enum EditorStringId {
Placeholder = 'editor.placeholder',
Copy = 'editor.copy',
Paste = 'editor.paste',
Undo = 'editor.undo',
Redo = 'editor.redo',
}
+2 -1
View File
@@ -14,7 +14,8 @@
"isolatedModules": true,
"noEmit": true,
"jsx": "react",
"downlevelIteration": true
"downlevelIteration": true,
"experimentalDecorators": true
},
"include": ["src"]
}
+1 -1
View File
@@ -2053,7 +2053,7 @@ accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.7:
mime-types "~2.1.24"
negotiator "0.6.2"
ace-builds@^1.4.6, ace-builds@^1.4.9:
ace-builds@^1.4.11, ace-builds@^1.4.6:
version "1.4.11"
resolved "https://registry.yarnpkg.com/ace-builds/-/ace-builds-1.4.11.tgz#b1f19a891afcef1d26522473082baf80067e855f"
integrity sha512-keACH1d7MvAh72fE/us36WQzOFQPJbHphNpj33pXwVZOM84pTWcdFzIAvngxOGIGLTm7gtUP2eJ4Ku6VaPo8bw==