diff --git a/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/CodeMirror.tsx b/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/CodeMirror.tsx index 0fe230802..0f300ea10 100644 --- a/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/CodeMirror.tsx +++ b/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/CodeMirror.tsx @@ -36,6 +36,8 @@ const MenuItem = bridge().MenuItem; import { reg } from '@joplin/lib/registry'; import ErrorBoundary from '../../../ErrorBoundary'; import { MarkupToHtmlOptions } from '../../utils/useMarkupToHtml'; +import eventManager from '@joplin/lib/eventManager'; +import { EditContextMenuFilterObject } from '@joplin/lib/services/plugins/api/JoplinWorkspace'; const menuUtils = new MenuUtils(CommandService.instance()); @@ -733,7 +735,7 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) { return rect.x < x && rect.y < y && rect.right > x && rect.bottom > y; } - function onContextMenu(_event: any, params: any) { + async function onContextMenu(_event: any, params: any) { if (!pointerInsideEditor(params.x, params.y)) return; const menu = new Menu(); @@ -787,6 +789,23 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) { editorRef.current.alignSelection(params); } + let filterObject: EditContextMenuFilterObject = { + items: [], + }; + + filterObject = await eventManager.filterEmit('editorContextMenu', filterObject); + + for (const item of filterObject.items) { + menu.append(new MenuItem({ + label: item.label, + click: async () => { + const args = item.commandArgs || []; + void CommandService.instance().execute(item.commandName, ...args); + }, + type: item.type, + })); + } + menuUtils.pluginContextMenuItems(props.plugins, MenuItemLocation.EditorContextMenu).forEach((item: any) => { menu.append(new MenuItem(item)); }); diff --git a/packages/app-desktop/testPluginDemo.sh b/packages/app-desktop/testPluginDemo.sh index cd7559378..0ff927426 100755 --- a/packages/app-desktop/testPluginDemo.sh +++ b/packages/app-desktop/testPluginDemo.sh @@ -4,5 +4,5 @@ # It could be used to develop plugins too. SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" -PLUGIN_PATH="$SCRIPT_DIR/../app-cli/tests/support/plugins/toc" -yarn i --prefix="$PLUGIN_PATH" && yarn start --dev-plugins "$PLUGIN_PATH" \ No newline at end of file +PLUGIN_PATH=~/src/joplin-rich-markdown +npm install --prefix="$PLUGIN_PATH" && yarn start --dev-plugins "$PLUGIN_PATH" \ No newline at end of file diff --git a/packages/lib/eventManager.ts b/packages/lib/eventManager.ts index d6bd566d9..dfae08941 100644 --- a/packages/lib/eventManager.ts +++ b/packages/lib/eventManager.ts @@ -1,3 +1,5 @@ +const fastDeepEqual = require('fast-deep-equal'); + const events = require('events'); export class EventManager { @@ -43,21 +45,22 @@ export class EventManager { return this.removeListener(`filter:${filterName}`, callback); } - filterEmit(filterName: string, object: any) { - // We freeze the object we pass to the listeners so that they - // don't modify it directly. Instead they must return a - // modified copy (or the input itself). - let output = Object.freeze(object); + public async filterEmit(filterName: string, object: any) { + let output = object; const listeners = this.emitter_.listeners(`filter:${filterName}`); for (const listener of listeners) { - const newOutput = listener(output); + // When we pass the object to the plugin, it is always going to be + // modified since it is serialized/unserialized. So we need to use a + // deep equality check to see if it's been changed. Normally the + // filter objects should be relatively small so there shouldn't be + // much of a performance hit. + const newOutput = await listener(output); - if (newOutput === undefined) { - throw new Error(`Filter "${filterName}": Filter must return a value or the unmodified input. Returning nothing or "undefined" is not supported.`); - } + // Plugin didn't return anything - so we leave the object as it is. + if (newOutput === undefined) continue; - if (newOutput !== output) { - output = Object.freeze(newOutput); + if (!fastDeepEqual(newOutput, output)) { + output = newOutput; } } diff --git a/packages/lib/services/plugins/api/JoplinWorkspace.ts b/packages/lib/services/plugins/api/JoplinWorkspace.ts index 523a615c6..7a0ea8812 100644 --- a/packages/lib/services/plugins/api/JoplinWorkspace.ts +++ b/packages/lib/services/plugins/api/JoplinWorkspace.ts @@ -3,7 +3,7 @@ import eventManager from '../../../eventManager'; import Setting from '../../../models/Setting'; import { FolderEntity } from '../../database/types'; import makeListener from '../utils/makeListener'; -import { Disposable } from './types'; +import { Disposable, MenuItem } from './types'; /** * @ignore @@ -15,6 +15,12 @@ import Note from '../../../models/Note'; */ import Folder from '../../../models/Folder'; +export interface EditContextMenuFilterObject { + items: MenuItem[]; +} + +type FilterHandler = (object: T)=> Promise; + enum ItemChangeEventType { Create = 1, Update = 2, @@ -113,6 +119,14 @@ export default class JoplinWorkspace { return makeListener(eventManager, 'syncComplete', callback); } + /** + * Called just before the editor context menu is about to open. Allows + * adding items to it. + */ + public filterEditorContextMenu(handler: FilterHandler) { + eventManager.filterOn('editorContextMenu', handler); + } + /** * Gets the currently selected note */ @@ -139,4 +153,5 @@ export default class JoplinWorkspace { public async selectedNoteIds(): Promise { return this.store.getState().selectedNoteIds.slice(); } + } diff --git a/packages/lib/services/plugins/api/types.ts b/packages/lib/services/plugins/api/types.ts index e0b2d96c0..af9e1295c 100644 --- a/packages/lib/services/plugins/api/types.ts +++ b/packages/lib/services/plugins/api/types.ts @@ -269,6 +269,17 @@ export interface MenuItem { */ commandName?: string; + /** + * Arguments that should be passed to the command. They will be as rest + * parameters. + */ + commandArgs?: any[]; + + /** + * Set to "separator" to create a divider line + */ + type?: string; + /** * Accelerator associated with the menu item */