mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-02-23 19:49:56 +01:00
editor: support ctrl+c for images (#8386)
* editor: support ctrl+c for images Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com> * editor: add test for copy image functionality Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
This commit is contained in:
@@ -18,7 +18,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Node, nodeInputRule, mergeAttributes } from "@tiptap/core";
|
||||
import { hasSameAttributes } from "../../utils/prosemirror.js";
|
||||
import {
|
||||
findSelectedNode,
|
||||
hasSameAttributes
|
||||
} from "../../utils/prosemirror.js";
|
||||
import {
|
||||
ImageAlignmentOptions,
|
||||
ImageAttachment,
|
||||
@@ -29,6 +32,7 @@ import { TextDirections } from "../text-direction/index.js";
|
||||
import { ImageComponent } from "./component.js";
|
||||
import { tiptapKeys } from "@notesnook/common";
|
||||
import { hasPermission } from "../../types.js";
|
||||
import { toBlob } from "../../utils/downloader.js";
|
||||
|
||||
export interface ImageOptions {
|
||||
inline: boolean;
|
||||
@@ -215,7 +219,41 @@ export const ImageNode = Node.create<ImageOptions>({
|
||||
addKeyboardShortcuts() {
|
||||
return {
|
||||
[tiptapKeys.addImage.keys]: () =>
|
||||
this.editor.storage.openAttachmentPicker?.("image") || true
|
||||
this.editor.storage.openAttachmentPicker?.("image") || true,
|
||||
"Mod-c": () => {
|
||||
if (!this.editor.isActive("image")) return false;
|
||||
|
||||
const imageNode = findSelectedNode(this.editor, "image");
|
||||
if (!imageNode || imageNode.type.name !== "image") return false;
|
||||
|
||||
const { hash, mime } = imageNode.attrs as ImageAttributes;
|
||||
if (!hash || !mime) return false;
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const imageData = await this.editor.storage.getAttachmentData?.({
|
||||
type: "image",
|
||||
hash
|
||||
});
|
||||
if (typeof imageData !== "string" || !imageData) return;
|
||||
|
||||
const imageBlob = toBlob(imageData, mime);
|
||||
if (!imageBlob) return;
|
||||
|
||||
if (navigator.clipboard && navigator.clipboard.write) {
|
||||
await navigator.clipboard.write([
|
||||
new ClipboardItem({
|
||||
[imageBlob.type]: imageBlob
|
||||
})
|
||||
]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to copy image to clipboard:", error);
|
||||
}
|
||||
})();
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
91
packages/editor/src/extensions/image/tests/image.test.ts
Normal file
91
packages/editor/src/extensions/image/tests/image.test.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { expect, test, vi, beforeEach, afterEach } from "vitest";
|
||||
import { createEditor, h } from "../../../../test-utils";
|
||||
import { ImageNode } from "../image";
|
||||
|
||||
const mockClipboardWrite = vi.fn();
|
||||
const mockClipboardItem = vi.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
mockClipboardWrite.mockReset();
|
||||
mockClipboardItem.mockReset();
|
||||
|
||||
// @ts-expect-error supports is declared here
|
||||
global.ClipboardItem = mockClipboardItem;
|
||||
|
||||
Object.defineProperty(navigator, "clipboard", {
|
||||
value: {
|
||||
write: mockClipboardWrite
|
||||
},
|
||||
writable: true
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
test("copy image to clipboard when Ctrl+C is pressed on selected image", async () => {
|
||||
const testHash = "test-hash";
|
||||
const base64ImageData =
|
||||
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=";
|
||||
const mockImageData = "data:image/png;base64," + base64ImageData;
|
||||
const expectedBlob = new Blob([Buffer.from(base64ImageData, "base64")], {
|
||||
type: "image/png"
|
||||
});
|
||||
|
||||
const editorElement = h("div");
|
||||
const { editor } = createEditor({
|
||||
element: editorElement,
|
||||
extensions: {
|
||||
image: ImageNode
|
||||
}
|
||||
});
|
||||
editor.storage.getAttachmentData = vi.fn().mockResolvedValue(mockImageData);
|
||||
editor.commands.insertImage({
|
||||
src: "test.png",
|
||||
hash: testHash,
|
||||
mime: "image/png",
|
||||
filename: "test.png"
|
||||
});
|
||||
editor.commands.setNodeSelection(0);
|
||||
|
||||
expect(editor.isActive("image")).toBe(true);
|
||||
|
||||
const keydownEvent = new KeyboardEvent("keydown", {
|
||||
key: "c",
|
||||
ctrlKey: true,
|
||||
bubbles: true,
|
||||
cancelable: true
|
||||
});
|
||||
editor.view.dom.dispatchEvent(keydownEvent);
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
expect(editor.storage.getAttachmentData).toHaveBeenCalledWith({
|
||||
type: "image",
|
||||
hash: testHash
|
||||
});
|
||||
expect(mockClipboardItem).toHaveBeenCalledWith({
|
||||
"image/png": expectedBlob
|
||||
});
|
||||
const clipboardItemInstance = mockClipboardItem.mock.results[0].value;
|
||||
expect(mockClipboardWrite).toHaveBeenCalledWith([clipboardItemInstance]);
|
||||
});
|
||||
@@ -146,3 +146,15 @@ export function revokeBloburl(id: string) {
|
||||
URL.revokeObjectURL(url);
|
||||
OBJECT_URL_CACHE[id] = undefined;
|
||||
}
|
||||
|
||||
export function toBlob(dataurl: string, mimeType: string): Blob | undefined {
|
||||
if (!DataURL.isValid(dataurl)) return;
|
||||
|
||||
const dataurlObject = DataURL.toObject(dataurl);
|
||||
const data = dataurlObject.data;
|
||||
if (!data) return;
|
||||
|
||||
return new Blob([Buffer.from(data, "base64")], {
|
||||
type: mimeType
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user