1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-08-10 22:11:50 +02:00

Plugins: Added the webviewApi.menuPopupFromTemplate() API to create context menus

This commit is contained in:
Laurent Cozic
2025-05-05 00:29:14 +01:00
parent 067ce65532
commit 370f6bd70e
6 changed files with 59 additions and 6 deletions

View File

@@ -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();

View File

@@ -33,6 +33,13 @@
target: 'postMessageService.registerViewMessageHandler',
});
},
menuPopupFromTemplate: (args) => {
postMessage({
target: 'webviewApi.menuPopupFromTemplate',
args,
});
},
};
function docReady(fn) {

View File

@@ -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<HTMLIFrameElement>, isReady: boolean, pluginId: string, viewId: string, windowId: string, postMessage: Function) {
@@ -32,6 +35,19 @@ export default function(webviewRef: RefObject<HTMLIFrameElement>, 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);
}
}

View File

@@ -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<any | void>;
execute(context: CommandContext, ...args: CommandArgument[]): Promise<any | void>;
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<any | void> {
public async execute(commandName: string, ...args: CommandArgument[]): Promise<any | void> {
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

View File

@@ -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 {

View File

@@ -417,6 +417,20 @@ export interface EditorActivationCheckFilterObject {
export type FilterHandler<T> = (object: T)=> Promise<T>;
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
// =================================================================