diff --git a/packages/editor/src/extensions/image/image.ts b/packages/editor/src/extensions/image/image.ts
index 03f367be1..cf53ffad1 100644
--- a/packages/editor/src/extensions/image/image.ts
+++ b/packages/editor/src/extensions/image/image.ts
@@ -18,7 +18,10 @@ along with this program. If not, see .
*/
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({
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;
+ }
};
}
});
diff --git a/packages/editor/src/extensions/image/tests/image.test.ts b/packages/editor/src/extensions/image/tests/image.test.ts
new file mode 100644
index 000000000..0f328a832
--- /dev/null
+++ b/packages/editor/src/extensions/image/tests/image.test.ts
@@ -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 .
+*/
+
+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]);
+});
diff --git a/packages/editor/src/utils/downloader.ts b/packages/editor/src/utils/downloader.ts
index 409f0da68..7653ab426 100644
--- a/packages/editor/src/utils/downloader.ts
+++ b/packages/editor/src/utils/downloader.ts
@@ -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
+ });
+}