文件
chen 301194963b
一些检查失败了
automerge / automerge (push) Has been cancelled
react-native-android-build-apk / AssembleRelease (push) Has been cancelled
Build macOS M1 / Main (push) Has been cancelled
Joplin Continuous Integration / Main (macos-15-intel) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-11-vs2026-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-2025) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04-arm) (push) Has been cancelled
Joplin UI tests / Main (ubuntu-22.04) (push) Has been cancelled
Joplin UI tests / Main (windows-2025) (push) Has been cancelled
Joplin UI tests / Mobile (push) Has been cancelled
Close stale issues / ProcessStaleIssues (push) Has been cancelled
feat: Initial clean release of CHM
2026-11-04 11:29:39 +08:00

185 行
6.5 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 { useContext, useEffect } from 'react';
import bridge from '../../../../../services/bridge';
import { ContextMenuOptions, ContextMenuItemType, buildMenuItems } 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';
import { WindowIdContext } from '../../../../NewWindowOrIFrame';
import Logger from '@joplin/utils/Logger';
const logger = Logger.create('useContextMenu');
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) {
const windowId = useContext(WindowIdContext);
useEffect(() => {
if (!editor) return () => {};
const targetWindow = bridge().windowById(windowId);
if (!targetWindow) return () => {};
const contextMenuItems = menuItems(dispatch);
const makeMainMenuItems = async (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,
};
return buildMenuItems(contextMenuItems, contextMenuActionOptions.current);
};
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 = async (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(...(await 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 = async (event: ElectronEvent, params: ContextMenuParams) => {
if (!lastTarget) return;
const element = lastTarget as HTMLElement;
lastTarget = null;
event.preventDefault();
await showContextMenu(element, params.misspelledWord, params.dictionarySuggestions);
};
const onBrowserContextMenu = async (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();
await showContextMenu(lastTarget as HTMLElement, null, []);
lastTarget = null;
}
};
try {
targetWindow.webContents.prependListener('context-menu', onElectronContextMenu);
} catch (error) {
logger.error('Failed to register context menu', error);
}
editor.on('contextmenu', onBrowserContextMenu);
return () => {
editor.off('contextmenu', onBrowserContextMenu);
try {
if (!targetWindow.isDestroyed() && targetWindow?.webContents?.off) {
targetWindow.webContents.off('context-menu', onElectronContextMenu);
}
} catch (error) {
// This can happen if the window closes after the isDestroyed check, but before webContents.off
// finishes running.
logger.error('Error removing context menu listener', error);
}
};
}, [editor, plugins, dispatch, htmlToMd, mdToHtml, editDialog, windowId]);
}