1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-11-23 22:36:32 +02:00
Files

180 lines
6.1 KiB
TypeScript

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 { ContextMenuOptions, ContextMenuItemType } from '../../../utils/contextMenuUtils';
import { menuItems } from '../../../utils/contextMenu';
import MenuUtils from '@joplin/lib/services/commands/MenuUtils';
import CommandService from '@joplin/lib/services/CommandService';
import type { ContextMenuParams, Event as ElectronEvent, MenuItemConstructorOptions } from 'electron';
import Resource from '@joplin/lib/models/Resource';
import { TinyMceEditorEvents } from './types';
import { HtmlToMarkdownHandler, MarkupToHtmlHandler } from '../../../utils/types';
import { Editor } from 'tinymce';
import { EditDialogControl } from './useEditDialog';
import { Dispatch } from 'redux';
import { _ } from '@joplin/lib/locale';
import type { MenuItem as MenuItemType } from 'electron';
import isItemId from '@joplin/lib/models/utils/isItemId';
const Menu = bridge().Menu;
const MenuItem = bridge().MenuItem;
const menuUtils = new MenuUtils(CommandService.instance());
interface ContextMenuActionOptions {
current: ContextMenuOptions;
}
const contextMenuActionOptions: ContextMenuActionOptions = { current: null };
export default function(editor: Editor, plugins: PluginStates, dispatch: Dispatch, htmlToMd: HtmlToMarkdownHandler, mdToHtml: MarkupToHtmlHandler, editDialog: EditDialogControl) {
useEffect(() => {
if (!editor) return () => {};
const contextMenuItems = menuItems(dispatch);
const targetWindow = bridge().activeWindow();
const makeMainMenuItems = (element: Element) => {
let itemType: ContextMenuItemType = ContextMenuItemType.None;
let resourceId = '';
let linkUrl = null;
const pathToId = (path: string) => {
const id = Resource.pathToId(path);
return isItemId(id) ? id : '';
};
if (element.nodeName === 'IMG') {
itemType = ContextMenuItemType.Image;
resourceId = pathToId((element as HTMLImageElement).src);
} else if (element.nodeName === 'A') {
resourceId = pathToId((element as HTMLAnchorElement).href);
itemType = resourceId ? ContextMenuItemType.Resource : ContextMenuItemType.Link;
linkUrl = element.getAttribute('href') || '';
} else {
itemType = ContextMenuItemType.Text;
}
contextMenuActionOptions.current = {
itemType,
resourceId,
filename: null,
mime: null,
linkToCopy: linkUrl,
linkToOpen: linkUrl,
textToCopy: null,
htmlToCopy: editor.selection ? editor.selection.getContent() : '',
insertContent: (content: string) => {
editor.insertContent(content);
},
isReadOnly: false,
fireEditorEvent: (event: TinyMceEditorEvents) => {
editor.fire(event);
},
htmlToMd,
mdToHtml,
};
const result = [];
for (const itemName in contextMenuItems) {
const item = contextMenuItems[itemName];
if (!item.isActive(itemType, contextMenuActionOptions.current)) continue;
result.push(new MenuItem({
label: item.label,
click: () => {
item.onAction(contextMenuActionOptions.current);
},
}));
}
return result;
};
const makeEditableMenuItems = (element: Element) => {
if (editDialog.isEditable(element)) {
return [
new MenuItem({
type: 'normal',
label: _('Edit'),
click: () => {
editDialog.editExisting(element);
},
}),
new MenuItem({ type: 'separator' }),
];
}
return [];
};
const showContextMenu = (element: HTMLElement, misspelledWord: string|null, dictionarySuggestions: string[]) => {
const menu = new Menu();
const menuItems: MenuItemType[] = [];
const toMenuItems = (specs: MenuItemConstructorOptions[]) => {
return specs.map(spec => new MenuItem(spec));
};
menuItems.push(...makeEditableMenuItems(element));
menuItems.push(...makeMainMenuItems(element));
const spellCheckerMenuItems = SpellCheckerService.instance().contextMenuItems(misspelledWord, dictionarySuggestions);
menuItems.push(
...toMenuItems(spellCheckerMenuItems),
);
menuItems.push(
...toMenuItems(menuUtils.pluginContextMenuItems(plugins, MenuItemLocation.EditorContextMenu)),
);
for (const item of menuItems) {
menu.append(item);
}
menu.popup({ window: targetWindow });
};
let lastTarget: EventTarget|null = null;
const onElectronContextMenu = (event: ElectronEvent, params: ContextMenuParams) => {
if (!lastTarget) return;
const element = lastTarget as HTMLElement;
lastTarget = null;
event.preventDefault();
showContextMenu(element, params.misspelledWord, params.dictionarySuggestions);
};
const onBrowserContextMenu = (event: PointerEvent) => {
const isKeyboard = event.buttons === 0;
if (isKeyboard) {
// Context menu events from the keyboard seem to always use <body> as the
// event target. Since which context menu is displayed depends on what the
// target is, using event.target for keyboard-triggered contextmenu events
// would prevent keyboard-only users from accessing certain functionality.
// To fix this, use the selection instead.
lastTarget = editor.selection.getNode();
} else {
lastTarget = event.target;
}
// Plugins in the Rich Text Editor (e.g. the mermaid renderer) can sometimes
// create custom right-click events. These don't trigger the Electron 'context-menu'
// event. As such, the context menu must be shown manually.
const isFromPlugin = !event.isTrusted;
if (isFromPlugin) {
event.preventDefault();
showContextMenu(lastTarget as HTMLElement, null, []);
lastTarget = null;
}
};
targetWindow.webContents.prependListener('context-menu', onElectronContextMenu);
editor.on('contextmenu', onBrowserContextMenu);
return () => {
editor.off('contextmenu', onBrowserContextMenu);
if (!targetWindow.isDestroyed() && targetWindow?.webContents?.off) {
targetWindow.webContents.off('context-menu', onElectronContextMenu);
}
};
}, [editor, plugins, dispatch, htmlToMd, mdToHtml, editDialog]);
}