diff --git a/app/client/components/CustomView.ts b/app/client/components/CustomView.ts index ae14ed61..32f1b019 100644 --- a/app/client/components/CustomView.ts +++ b/app/client/components/CustomView.ts @@ -3,6 +3,7 @@ import * as commands from 'app/client/components/commands'; import {Cursor} from 'app/client/components/Cursor'; import {GristDoc} from 'app/client/components/GristDoc'; import { + CommandAPI, ConfigNotifier, CustomSectionAPIImpl, GristDocAPIImpl, @@ -240,6 +241,10 @@ export class CustomView extends Disposable { access, this._promptAccess.bind(this)), new MinimumLevel(AccessLevel.none)); + frame.exposeAPI( + "CommandAPI", + new CommandAPI(access), + new MinimumLevel(AccessLevel.none)); frame.useEvents(RecordNotifier.create(frame, view), new MinimumLevel(AccessLevel.read_table)); frame.useEvents(TableNotifier.create(frame, view), new MinimumLevel(AccessLevel.read_table)); frame.exposeAPI( diff --git a/app/client/components/WidgetFrame.ts b/app/client/components/WidgetFrame.ts index 731b57a4..6c08d5c7 100644 --- a/app/client/components/WidgetFrame.ts +++ b/app/client/components/WidgetFrame.ts @@ -1,4 +1,6 @@ import BaseView from 'app/client/components/BaseView'; +import {CommandName} from 'app/client/components/commandList'; +import * as commands from 'app/client/components/commands'; import {GristDoc} from 'app/client/components/GristDoc'; import {hooks} from 'app/client/Hooks'; import {get as getBrowserGlobals} from 'app/client/lib/browserGlobals'; @@ -484,6 +486,26 @@ export class WidgetAPIImpl implements WidgetAPI { } } +const COMMAND_MINIMUM_ACCESS_LEVELS: Map = new Map([ + ['undo', AccessLevel.full], + ['redo', AccessLevel.full], +]); + +export class CommandAPI { + constructor(private _currentAccess: AccessLevel) {} + + public async run(commandName: CommandName): Promise { + const minimumAccess = COMMAND_MINIMUM_ACCESS_LEVELS.get(commandName); + if (minimumAccess === undefined || !isSatisfied(this._currentAccess, minimumAccess)) { + // If the command name is unrecognized, or the current access level doesn't meet the + // command's minimum access level, do nothing. + return; + } + + return await commands.allCommands[commandName].run(); + } +} + /************************ * Events that are sent to the CustomWidget. * diff --git a/app/plugin/grist-plugin-api.ts b/app/plugin/grist-plugin-api.ts index f617b33e..9c3fbf51 100644 --- a/app/plugin/grist-plugin-api.ts +++ b/app/plugin/grist-plugin-api.ts @@ -65,6 +65,8 @@ export const widgetApi = rpc.getStub('WidgetAPI', checkers.WidgetAPI) */ export const sectionApi = rpc.getStub('CustomSectionAPI', checkers.CustomSectionAPI); +export const commandApi = rpc.getStub('CommandAPI'); + /** * Shortcut for [[GristView.allowSelectBy]]. */ @@ -437,6 +439,12 @@ export async function addImporter(name: string, path: string, mode: 'fullscreen' }); } +export function enableKeyboardShortcuts() { + const Mousetrap = require('mousetrap'); + Mousetrap.bind('mod+z', () => commandApi.run('undo')); + Mousetrap.bind(['mod+shift+z', 'ctrl+y'], () => commandApi.run('redo')); +} + /** * Options when initializing connection to Grist. */ diff --git a/package.json b/package.json index 3a6c451d..21eabdb8 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "@types/minio": "7.0.15", "@types/mocha": "5.2.5", "@types/moment-timezone": "0.5.9", + "@types/mousetrap": "1.6.2", "@types/node": "^14", "@types/node-fetch": "2.6.2", "@types/pidusage": "2.0.1", diff --git a/static/custom-widget.html b/static/custom-widget.html index f7492ae6..c01c8764 100644 --- a/static/custom-widget.html +++ b/static/custom-widget.html @@ -7,6 +7,7 @@