1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-11-06 09:19:22 +02:00

Plugins: Add support for editor context menu

This commit is contained in:
Laurent Cozic
2020-11-14 00:02:17 +00:00
parent 7151a48138
commit 4f41fb7b54
77 changed files with 6096 additions and 158 deletions

View File

@@ -17,6 +17,10 @@ import { _ } from '@joplin/lib/locale';
import bridge from '../../../../services/bridge';
import markdownUtils from '@joplin/lib/markdownUtils';
import shim from '@joplin/lib/shim';
import { MenuItemLocation } from '@joplin/lib/services/plugins/api/types';
import MenuUtils from '@joplin/lib/services/commands/MenuUtils';
import CommandService from '@joplin/lib/services/CommandService';
import { themeStyle } from '@joplin/lib/theme';
const Note = require('@joplin/lib/models/Note.js');
const { clipboard } = require('electron');
@@ -25,7 +29,8 @@ const Menu = bridge().Menu;
const MenuItem = bridge().MenuItem;
const { reg } = require('@joplin/lib/registry.js');
const dialogs = require('../../../dialogs');
const { themeStyle } = require('@joplin/lib/theme');
const menuUtils = new MenuUtils(CommandService.instance());
function markupRenderOptions(override: any = null) {
return { ...override };
@@ -290,8 +295,12 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
})
);
menuUtils.pluginContextMenuItems(props.plugins, MenuItemLocation.EditorContextMenu).forEach((item: any) => {
menu.append(new MenuItem(item));
});
menu.popup(bridge().window());
}, [props.content, editorCutText, editorPasteText, editorCopyText, onEditorPaste]);
}, [props.content, editorCutText, editorPasteText, editorCopyText, onEditorPaste, props.plugins]);
const loadScript = async (script: any) => {
return new Promise((resolve) => {
@@ -643,11 +652,7 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
return (
<div style={styles.root} ref={rootRef}>
<div style={styles.rowToolbar}>
<Toolbar
themeId={props.themeId}
// dispatch={props.dispatch}
// plugins={props.plugins}
/>
<Toolbar themeId={props.themeId} />
{props.noteToolbar}
</div>
<div style={styles.rowEditorViewer}>

View File

@@ -11,13 +11,13 @@ import ToolbarButton from '../../../../gui/ToolbarButton/ToolbarButton';
import usePluginServiceRegistration from '../../utils/usePluginServiceRegistration';
import { utils as pluginUtils } from '@joplin/lib/services/plugins/reducer';
import { _, closestSupportedLocale } from '@joplin/lib/locale';
import setupContextMenu from './utils/setupContextMenu';
import useContextMenu from './utils/useContextMenu';
import shim from '@joplin/lib/shim';
const { MarkupToHtml } = require('@joplin/renderer');
const taboverride = require('taboverride');
const { reg } = require('@joplin/lib/registry.js');
const BaseItem = require('@joplin/lib/models/BaseItem');
const shim = require('@joplin/lib/shim').default;
const { themeStyle } = require('@joplin/lib/theme');
const { clipboard } = require('electron');
const supportedLocales = require('./supportedLocales');
@@ -161,6 +161,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
const { scrollToPercent } = useScroll({ editor, onScroll: props.onScroll });
usePluginServiceRegistration(ref);
useContextMenu(editor, props.plugins);
const dispatchDidUpdate = (editor: any) => {
if (dispatchDidUpdateIID_) shim.clearTimeout(dispatchDidUpdateIID_);
@@ -668,7 +669,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
});
}
setupContextMenu(editor);
// setupContextMenu(editor);
// TODO: remove event on unmount?
editor.on('DblClick', (event: any) => {

View File

@@ -1,90 +0,0 @@
import SpellCheckerService from '@joplin/lib/services/spellChecker/SpellCheckerService';
import bridge from '../../../../../services/bridge';
import { menuItems, ContextMenuOptions, ContextMenuItemType } from '../../../utils/contextMenu';
const Resource = require('@joplin/lib/models/Resource');
// x and y are the absolute coordinates, as returned by the context-menu event
// handler on the webContent. This function will return null if the point is
// not within the TinyMCE editor.
function contextMenuElement(editor: any, x: number, y: number) {
if (!editor || !editor.getDoc()) return null;
const iframes = document.getElementsByClassName('tox-edit-area__iframe');
if (!iframes.length) return null;
const iframeRect = iframes[0].getBoundingClientRect();
if (iframeRect.x < x && iframeRect.y < y && iframeRect.right > x && iframeRect.bottom > y) {
const relativeX = x - iframeRect.x;
const relativeY = y - iframeRect.y;
return editor.getDoc().elementFromPoint(relativeX, relativeY);
}
return null;
}
interface ContextMenuActionOptions {
current: ContextMenuOptions;
}
const contextMenuActionOptions: ContextMenuActionOptions = { current: null };
export default function(editor: any) {
const contextMenuItems = menuItems();
bridge().window().webContents.on('context-menu', (_event: any, params: any) => {
const element = contextMenuElement(editor, params.x, params.y);
if (!element) return;
let itemType: ContextMenuItemType = ContextMenuItemType.None;
let resourceId = '';
let linkToCopy = null;
if (element.nodeName === 'IMG') {
itemType = ContextMenuItemType.Image;
resourceId = Resource.pathToId(element.src);
} else if (element.nodeName === 'A') {
resourceId = Resource.pathToId(element.href);
itemType = resourceId ? ContextMenuItemType.Resource : ContextMenuItemType.Link;
linkToCopy = element.getAttribute('href') || '';
} else {
itemType = ContextMenuItemType.Text;
}
contextMenuActionOptions.current = {
itemType,
resourceId,
linkToCopy,
textToCopy: null,
htmlToCopy: editor.selection ? editor.selection.getContent() : '',
insertContent: (content: string) => {
editor.insertContent(content);
},
isReadOnly: false,
};
const template = [];
for (const itemName in contextMenuItems) {
const item = contextMenuItems[itemName];
if (!item.isActive(itemType, contextMenuActionOptions.current)) continue;
template.push({
label: item.label,
click: () => {
item.onAction(contextMenuActionOptions.current);
},
});
}
const spellCheckerMenuItems = SpellCheckerService.instance().contextMenuItems(params.misspelledWord, params.dictionarySuggestions);
for (const item of spellCheckerMenuItems) {
template.push(item);
}
const menu = bridge().Menu.buildFromTemplate(template);
menu.popup(bridge().window());
});
}

View File

@@ -0,0 +1,112 @@
import { MenuItemLocation } from '@joplin/lib/services/plugins/api/types';
import { PluginStates } from '@joplin/lib/services/plugins/reducer';
import SpellCheckerService from '@joplin/lib/services/spellChecker/SpellCheckerService';
import { useEffect } from 'react';
import bridge from '../../../../../services/bridge';
import { menuItems, ContextMenuOptions, ContextMenuItemType } from '../../../utils/contextMenu';
import MenuUtils from '@joplin/lib/services/commands/MenuUtils';
import CommandService from '@joplin/lib/services/CommandService';
const Resource = require('@joplin/lib/models/Resource');
const menuUtils = new MenuUtils(CommandService.instance());
// x and y are the absolute coordinates, as returned by the context-menu event
// handler on the webContent. This function will return null if the point is
// not within the TinyMCE editor.
function contextMenuElement(editor: any, x: number, y: number) {
if (!editor || !editor.getDoc()) return null;
const iframes = document.getElementsByClassName('tox-edit-area__iframe');
if (!iframes.length) return null;
const iframeRect = iframes[0].getBoundingClientRect();
if (iframeRect.x < x && iframeRect.y < y && iframeRect.right > x && iframeRect.bottom > y) {
const relativeX = x - iframeRect.x;
const relativeY = y - iframeRect.y;
return editor.getDoc().elementFromPoint(relativeX, relativeY);
}
return null;
}
interface ContextMenuActionOptions {
current: ContextMenuOptions;
}
const contextMenuActionOptions: ContextMenuActionOptions = { current: null };
export default function(editor: any, plugins: PluginStates) {
useEffect(() => {
if (!editor) return () => {};
const contextMenuItems = menuItems();
function onContextMenu(_event: any, params: any) {
const element = contextMenuElement(editor, params.x, params.y);
if (!element) return;
let itemType: ContextMenuItemType = ContextMenuItemType.None;
let resourceId = '';
let linkToCopy = null;
if (element.nodeName === 'IMG') {
itemType = ContextMenuItemType.Image;
resourceId = Resource.pathToId(element.src);
} else if (element.nodeName === 'A') {
resourceId = Resource.pathToId(element.href);
itemType = resourceId ? ContextMenuItemType.Resource : ContextMenuItemType.Link;
linkToCopy = element.getAttribute('href') || '';
} else {
itemType = ContextMenuItemType.Text;
}
contextMenuActionOptions.current = {
itemType,
resourceId,
linkToCopy,
textToCopy: null,
htmlToCopy: editor.selection ? editor.selection.getContent() : '',
insertContent: (content: string) => {
editor.insertContent(content);
},
isReadOnly: false,
};
let template = [];
for (const itemName in contextMenuItems) {
const item = contextMenuItems[itemName];
if (!item.isActive(itemType, contextMenuActionOptions.current)) continue;
template.push({
label: item.label,
click: () => {
item.onAction(contextMenuActionOptions.current);
},
});
}
const spellCheckerMenuItems = SpellCheckerService.instance().contextMenuItems(params.misspelledWord, params.dictionarySuggestions);
for (const item of spellCheckerMenuItems) {
template.push(item);
}
template = template.concat(menuUtils.pluginContextMenuItems(plugins, MenuItemLocation.EditorContextMenu));
const menu = bridge().Menu.buildFromTemplate(template);
menu.popup(bridge().window());
}
bridge().window().webContents.on('context-menu', onContextMenu);
return () => {
if (bridge().window()?.webContents?.off) {
bridge().window().webContents.off('context-menu', onContextMenu);
}
};
}, [editor, plugins]);
}

View File

@@ -230,7 +230,16 @@ function NoteEditor(props: NoteEditorProps) {
}
}, [handleProvisionalFlag, formNote, isNewNote, titleHasBeenManuallyChanged]);
useWindowCommandHandler({ dispatch: props.dispatch, formNote, setShowLocalSearch, noteSearchBarRef, editorRef, titleInputRef, saveNoteAndWait });
useWindowCommandHandler({
dispatch: props.dispatch,
formNote,
setShowLocalSearch,
noteSearchBarRef,
editorRef,
titleInputRef,
saveNoteAndWait,
setFormNote,
});
const onDrop = useDropHandler({ editorRef });

View File

@@ -89,6 +89,9 @@ const declarations: CommandDeclaration[] = [
{
name: 'replaceSelection',
},
{
name: 'editorSetText',
},
];
export default declarations;

View File

@@ -19,9 +19,10 @@ interface HookDependencies {
editorRef: any;
titleInputRef: any;
saveNoteAndWait: Function;
setFormNote: Function;
}
function editorCommandRuntime(declaration: CommandDeclaration, editorRef: any): CommandRuntime {
function editorCommandRuntime(declaration: CommandDeclaration, editorRef: any, setFormNote: Function): CommandRuntime {
return {
execute: async (_context: CommandContext, ...args: any[]) => {
if (!editorRef.current.execCommand) {
@@ -39,6 +40,10 @@ function editorCommandRuntime(declaration: CommandDeclaration, editorRef: any):
type: ScrollOptionTypes.Hash,
value: args[0],
});
} else if (declaration.name === 'editorSetText') {
setFormNote((prev: FormNote) => {
return { ...prev, body: args[0] };
});
} else {
return editorRef.current.execCommand({
name: declaration.name,
@@ -51,11 +56,11 @@ function editorCommandRuntime(declaration: CommandDeclaration, editorRef: any):
}
export default function useWindowCommandHandler(dependencies: HookDependencies) {
const { setShowLocalSearch, noteSearchBarRef, editorRef, titleInputRef } = dependencies;
const { setShowLocalSearch, noteSearchBarRef, editorRef, titleInputRef, setFormNote } = dependencies;
useEffect(() => {
for (const declaration of editorCommandDeclarations) {
CommandService.instance().registerRuntime(declaration.name, editorCommandRuntime(declaration, editorRef));
CommandService.instance().registerRuntime(declaration.name, editorCommandRuntime(declaration, editorRef, setFormNote));
}
const dependencies = {