mirror of
https://github.com/ankitects/anki.git
synced 2025-09-19 06:22:22 -04:00
122 lines
4.2 KiB
TypeScript
122 lines
4.2 KiB
TypeScript
// Copyright: Ankitects Pty Ltd and contributors
|
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|
|
|
import { openMedia, showInMediaFolder } from "@generated/backend";
|
|
import * as tr from "@generated/ftl";
|
|
import { bridgeCommand } from "@tslib/bridgecommand";
|
|
import { getSelection } from "@tslib/cross-browser";
|
|
import type ContextMenu from "svelte-contextmenu";
|
|
import type { ContextMenuMouseEvent } from "svelte-contextmenu/ContextMenuMouseEvent";
|
|
import { get } from "svelte/store";
|
|
import type { EditingInputAPI } from "./EditingArea.svelte";
|
|
import type { NoteEditorAPI } from "./NoteEditor.svelte";
|
|
import { editingInputIsPlainText } from "./plain-text-input";
|
|
import { editingInputIsRichText } from "./rich-text-input";
|
|
import { writeBlobToClipboard } from "./rich-text-input/data-transfer";
|
|
import { EditorState } from "./types";
|
|
|
|
async function getFieldSelection(focusedInput: EditingInputAPI): Promise<string | null> {
|
|
if (editingInputIsRichText(focusedInput)) {
|
|
const selection = getSelection(await focusedInput.element);
|
|
if (selection && selection.toString()) {
|
|
return selection.toString();
|
|
}
|
|
} else if (editingInputIsPlainText(focusedInput)) {
|
|
const selection = (await focusedInput.codeMirror.editor).getSelection();
|
|
if (selection) {
|
|
return selection;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function getImageFromMouseEvent(event: ContextMenuMouseEvent, element: HTMLElement): string | null {
|
|
const elements = element.getRootNode().elementsFromPoint(event.clientX, event.clientY);
|
|
for (const element of elements) {
|
|
if (element instanceof HTMLImageElement && (new URL(element.src)).hostname === window.location.hostname) {
|
|
return decodeURI(element.getAttribute("src")!);
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
interface ContextMenuItem {
|
|
label: string;
|
|
action: () => void;
|
|
}
|
|
|
|
export function setupContextMenu(): [
|
|
(
|
|
event: ContextMenuMouseEvent,
|
|
noteEditor: NoteEditorAPI,
|
|
focusedInput: EditingInputAPI | null,
|
|
contextMenu: ContextMenu,
|
|
) => Promise<void>,
|
|
ContextMenuItem[],
|
|
] {
|
|
const contextMenuItems: ContextMenuItem[] = $state([]);
|
|
async function onContextMenu(
|
|
event: ContextMenuMouseEvent,
|
|
noteEditor: NoteEditorAPI,
|
|
focusedInput: EditingInputAPI | null,
|
|
contextMenu: ContextMenu,
|
|
) {
|
|
contextMenuItems.length = 0;
|
|
contextMenuItems.push({
|
|
label: tr.editingPaste(),
|
|
action: () => {
|
|
bridgeCommand("paste");
|
|
},
|
|
});
|
|
const selection = focusedInput ? await getFieldSelection(focusedInput) : null;
|
|
if (selection) {
|
|
contextMenuItems.push({
|
|
label: tr.editingCut(),
|
|
action: () => {
|
|
bridgeCommand("cut");
|
|
},
|
|
}, {
|
|
label: tr.actionsCopy(),
|
|
action: () => {
|
|
bridgeCommand("copy");
|
|
},
|
|
});
|
|
}
|
|
|
|
let imagePath: string | null = null;
|
|
if (get(noteEditor.state) === EditorState.ImageOcclusionMasks) {
|
|
imagePath = get(noteEditor.lastIOImagePath);
|
|
} else if (focusedInput && editingInputIsRichText(focusedInput)) {
|
|
imagePath = getImageFromMouseEvent(event, await focusedInput.element);
|
|
}
|
|
if (imagePath) {
|
|
contextMenuItems.push({
|
|
label: tr.editingCopyImage(),
|
|
action: async () => {
|
|
const image = await fetch(imagePath);
|
|
const blob = await image.blob();
|
|
await writeBlobToClipboard(blob);
|
|
},
|
|
}, {
|
|
label: tr.editingOpenImage(),
|
|
action: () => {
|
|
openMedia({ val: imagePath });
|
|
},
|
|
}, {
|
|
label: tr.editingShowInFolder(),
|
|
action: () => {
|
|
showInMediaFolder({ val: imagePath });
|
|
},
|
|
});
|
|
}
|
|
|
|
if (contextMenuItems.length > 0) {
|
|
contextMenu?.show(event);
|
|
}
|
|
|
|
event.preventDefault();
|
|
}
|
|
|
|
return [onContextMenu, contextMenuItems];
|
|
}
|