You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-11-26 22:41:17 +02:00
Desktop: Fixes #9588: Rich Text Editor: Fix keyboard and plugin-opened context menus sometimes not displayed or have incorrect content (#12076)
This commit is contained in:
@@ -7,8 +7,7 @@ import { ContextMenuOptions, ContextMenuItemType } from '../../../utils/contextM
|
|||||||
import { menuItems } from '../../../utils/contextMenu';
|
import { menuItems } from '../../../utils/contextMenu';
|
||||||
import MenuUtils from '@joplin/lib/services/commands/MenuUtils';
|
import MenuUtils from '@joplin/lib/services/commands/MenuUtils';
|
||||||
import CommandService from '@joplin/lib/services/CommandService';
|
import CommandService from '@joplin/lib/services/CommandService';
|
||||||
import Setting from '@joplin/lib/models/Setting';
|
import type { ContextMenuParams, Event as ElectronEvent, MenuItemConstructorOptions } from 'electron';
|
||||||
import type { Event as ElectronEvent, MenuItemConstructorOptions } from 'electron';
|
|
||||||
|
|
||||||
import Resource from '@joplin/lib/models/Resource';
|
import Resource from '@joplin/lib/models/Resource';
|
||||||
import { TinyMceEditorEvents } from './types';
|
import { TinyMceEditorEvents } from './types';
|
||||||
@@ -23,33 +22,6 @@ const Menu = bridge().Menu;
|
|||||||
const MenuItem = bridge().MenuItem;
|
const MenuItem = bridge().MenuItem;
|
||||||
const menuUtils = new MenuUtils(CommandService.instance());
|
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: Editor, x: number, y: number) {
|
|
||||||
if (!editor || !editor.getDoc()) return null;
|
|
||||||
|
|
||||||
const containerDoc = editor.getContainer().ownerDocument;
|
|
||||||
const iframes = containerDoc.getElementsByClassName('tox-edit-area__iframe');
|
|
||||||
if (!iframes.length) return null;
|
|
||||||
|
|
||||||
const zoom = Setting.value('windowContentZoomFactor') / 100;
|
|
||||||
const xScreen = x / zoom;
|
|
||||||
const yScreen = y / zoom;
|
|
||||||
|
|
||||||
// We use .elementFromPoint to handle the case where a dialog is covering
|
|
||||||
// part of the editor.
|
|
||||||
const targetElement = containerDoc.elementFromPoint(xScreen, yScreen);
|
|
||||||
if (targetElement !== iframes[0]) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const iframeRect = iframes[0].getBoundingClientRect();
|
|
||||||
const relativeX = xScreen - iframeRect.left;
|
|
||||||
const relativeY = yScreen - iframeRect.top;
|
|
||||||
return editor.getDoc().elementFromPoint(relativeX, relativeY);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ContextMenuActionOptions {
|
interface ContextMenuActionOptions {
|
||||||
current: ContextMenuOptions;
|
current: ContextMenuOptions;
|
||||||
}
|
}
|
||||||
@@ -130,13 +102,7 @@ export default function(editor: Editor, plugins: PluginStates, dispatch: Dispatc
|
|||||||
return [];
|
return [];
|
||||||
};
|
};
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
const showContextMenu = (element: HTMLElement, misspelledWord: string|null, dictionarySuggestions: string[]) => {
|
||||||
function onContextMenu(event: ElectronEvent, params: any) {
|
|
||||||
const element = contextMenuElement(editor, params.x, params.y);
|
|
||||||
if (!element) return;
|
|
||||||
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
const menu = new Menu();
|
const menu = new Menu();
|
||||||
const menuItems: MenuItemType[] = [];
|
const menuItems: MenuItemType[] = [];
|
||||||
const toMenuItems = (specs: MenuItemConstructorOptions[]) => {
|
const toMenuItems = (specs: MenuItemConstructorOptions[]) => {
|
||||||
@@ -145,7 +111,7 @@ export default function(editor: Editor, plugins: PluginStates, dispatch: Dispatc
|
|||||||
|
|
||||||
menuItems.push(...makeEditableMenuItems(element));
|
menuItems.push(...makeEditableMenuItems(element));
|
||||||
menuItems.push(...makeMainMenuItems(element));
|
menuItems.push(...makeMainMenuItems(element));
|
||||||
const spellCheckerMenuItems = SpellCheckerService.instance().contextMenuItems(params.misspelledWord, params.dictionarySuggestions);
|
const spellCheckerMenuItems = SpellCheckerService.instance().contextMenuItems(misspelledWord, dictionarySuggestions);
|
||||||
menuItems.push(
|
menuItems.push(
|
||||||
...toMenuItems(spellCheckerMenuItems),
|
...toMenuItems(spellCheckerMenuItems),
|
||||||
);
|
);
|
||||||
@@ -157,13 +123,49 @@ export default function(editor: Editor, plugins: PluginStates, dispatch: Dispatc
|
|||||||
menu.append(item);
|
menu.append(item);
|
||||||
}
|
}
|
||||||
menu.popup({ window: targetWindow });
|
menu.popup({ window: targetWindow });
|
||||||
}
|
};
|
||||||
|
|
||||||
targetWindow.webContents.prependListener('context-menu', onContextMenu);
|
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 () => {
|
return () => {
|
||||||
|
editor.off('contextmenu', onBrowserContextMenu);
|
||||||
if (!targetWindow.isDestroyed() && targetWindow?.webContents?.off) {
|
if (!targetWindow.isDestroyed() && targetWindow?.webContents?.off) {
|
||||||
targetWindow.webContents.off('context-menu', onContextMenu);
|
targetWindow.webContents.off('context-menu', onElectronContextMenu);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [editor, plugins, dispatch, htmlToMd, mdToHtml, editDialog]);
|
}, [editor, plugins, dispatch, htmlToMd, mdToHtml, editDialog]);
|
||||||
|
|||||||
Reference in New Issue
Block a user