diff --git a/packages/app-desktop/bridge.ts b/packages/app-desktop/bridge.ts index 5f528aee3b..a04988d725 100644 --- a/packages/app-desktop/bridge.ts +++ b/packages/app-desktop/bridge.ts @@ -1,7 +1,7 @@ import ElectronAppWrapper from './ElectronAppWrapper'; import shim, { MessageBoxType } from '@joplin/lib/shim'; import { _, setLocale } from '@joplin/lib/locale'; -import { BrowserWindow, nativeTheme, nativeImage, shell, dialog, MessageBoxSyncOptions, safeStorage } from 'electron'; +import { BrowserWindow, nativeTheme, nativeImage, shell, dialog, MessageBoxSyncOptions, safeStorage, Menu, MenuItemConstructorOptions, MenuItem } from 'electron'; import { dirname, toSystemSlashes } from '@joplin/lib/path-utils'; import { fileUriToPath } from '@joplin/utils/url'; import { urlDecode } from '@joplin/lib/string-utils'; @@ -602,6 +602,11 @@ export class Bridge { return nativeImage.createFromPath(path); } + public menuPopupFromTemplate(template: ((MenuItemConstructorOptions) | (MenuItem))[]) { + const menu = Menu.buildFromTemplate(template); + return menu.popup({ window: this.mainWindow() }); + } + public safeStorage = { isEncryptionAvailable() { return safeStorage.isEncryptionAvailable(); diff --git a/packages/app-desktop/services/plugins/UserWebviewIndex.js b/packages/app-desktop/services/plugins/UserWebviewIndex.js index d37ab7e531..ffcbbc4524 100644 --- a/packages/app-desktop/services/plugins/UserWebviewIndex.js +++ b/packages/app-desktop/services/plugins/UserWebviewIndex.js @@ -33,6 +33,13 @@ target: 'postMessageService.registerViewMessageHandler', }); }, + + menuPopupFromTemplate: (args) => { + postMessage({ + target: 'webviewApi.menuPopupFromTemplate', + args, + }); + }, }; function docReady(fn) { diff --git a/packages/app-desktop/services/plugins/hooks/useWebviewToPluginMessages.ts b/packages/app-desktop/services/plugins/hooks/useWebviewToPluginMessages.ts index 779fc740bc..41d1b5d2f8 100644 --- a/packages/app-desktop/services/plugins/hooks/useWebviewToPluginMessages.ts +++ b/packages/app-desktop/services/plugins/hooks/useWebviewToPluginMessages.ts @@ -1,6 +1,9 @@ import PostMessageService, { MessageResponse, ResponderComponentType } from '@joplin/lib/services/PostMessageService'; import { RefObject, useEffect } from 'react'; - +import bridge from '../../bridge'; +import CommandService from '@joplin/lib/services/CommandService'; +import { MenuItemConstructorOptions } from 'electron'; +import { MenuTemplateItem } from '@joplin/lib/services/plugins/api/types'; // eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied, Old code before rule was applied export default function(webviewRef: RefObject, isReady: boolean, pluginId: string, viewId: string, windowId: string, postMessage: Function) { @@ -32,6 +35,19 @@ export default function(webviewRef: RefObject, isReady: boole windowId, ...event.data.message, }); + } else if (event.data.target === 'webviewApi.menuPopupFromTemplate') { + const template = event.data.args as MenuTemplateItem[]; + const finalTemplate = template.map((menuItem) => { + const output: MenuItemConstructorOptions = { + label: menuItem.label ?? '', + click: () => { + const args = menuItem.commandArgs ?? []; + void CommandService.instance().execute(menuItem.command, ...args); + }, + }; + return output; + }); + bridge().menuPopupFromTemplate(finalTemplate); } } diff --git a/packages/lib/services/CommandService.ts b/packages/lib/services/CommandService.ts index 7fa9b026b7..0adcc95d92 100644 --- a/packages/lib/services/CommandService.ts +++ b/packages/lib/services/CommandService.ts @@ -8,6 +8,8 @@ import type { WhenClauseContext } from './commands/stateToWhenClauseContext'; type LabelFunction = ()=> string; type EnabledCondition = string; +export type CommandArgument = string|number|object|boolean|null; + export interface CommandContext { // The state may also be of type "AppState" (used by the desktop app), which inherits from "State" (used by all apps) state: State; @@ -17,7 +19,7 @@ export interface CommandContext { export interface CommandRuntime { // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied - execute(context: CommandContext, ...args: any[]): Promise; + execute(context: CommandContext, ...args: CommandArgument[]): Promise; enabledCondition?: EnabledCondition; // Used for the (optional) toolbar button title // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied @@ -317,7 +319,7 @@ export default class CommandService extends BaseService { } // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied - public async execute(commandName: string, ...args: any[]): Promise { + public async execute(commandName: string, ...args: CommandArgument[]): Promise { const command = this.commandByName(commandName); // Some commands such as "showModalMessage" can be executed many // times per seconds, so we should only display this message in diff --git a/packages/lib/services/plugins/api/JoplinViews.ts b/packages/lib/services/plugins/api/JoplinViews.ts index 9b9eebc8a1..bb0d3e0c05 100644 --- a/packages/lib/services/plugins/api/JoplinViews.ts +++ b/packages/lib/services/plugins/api/JoplinViews.ts @@ -12,8 +12,17 @@ import JoplinViewsEditors from './JoplinViewsEditor'; /** * This namespace provides access to view-related services. * - * All view services provide a `create()` method which you would use to create the view object, whether it's a dialog, a toolbar button or a menu item. - * In some cases, the `create()` method will return a [[ViewHandle]], which you would use to act on the view, for example to set certain properties or call some methods. + * ## Creating a view + * + * All view services provide a `create()` method which you would use to create the view object, + * whether it's a dialog, a toolbar button or a menu item. In some cases, the `create()` method will + * return a [[ViewHandle]], which you would use to act on the view, for example to set certain + * properties or call some methods. + * + * ## The `webviewApi` object + * + * Within a view, you can use the global object `webviewApi` for various utility functions, such as + * sending messages or displaying context menu. Refer to [[WebviewApi]] for the full documentation. */ export default class JoplinViews { diff --git a/packages/lib/services/plugins/api/types.ts b/packages/lib/services/plugins/api/types.ts index 3cd7b0f0ae..3138f39cf3 100644 --- a/packages/lib/services/plugins/api/types.ts +++ b/packages/lib/services/plugins/api/types.ts @@ -417,6 +417,20 @@ export interface EditorActivationCheckFilterObject { export type FilterHandler = (object: T)=> Promise; +export type CommandArgument = string|number|object|boolean|null; + +export interface MenuTemplateItem { + label?: string; + command?: string; + commandArgs?: CommandArgument[]; +} + +export interface WebviewApi { + postMessage: (message: object)=> unknown; + onMessage: (message: object)=> void; + menuPopupFromTemplate: (template: MenuTemplateItem[])=> void; +} + // ================================================================= // Settings types // =================================================================