Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implemented utility function to copy an HTML element as an image to the clipboard #3963

Merged
merged 2 commits into from
Aug 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions lib/utils/copy-to-clipboard.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { copyNodeAsImage } from "./copy-to-clipboard";

if (typeof ClipboardItem === "undefined") {
globalThis.ClipboardItem = class ClipboardItem {
types: string[];

constructor(public items: Record<string, string | Blob | PromiseLike<string | Blob>>) {
this.types = Object.keys(items);
}

async getType(type: string): Promise<Blob> {
const item = this.items[type];
if (item instanceof Blob) {
return item;
} else if (typeof item === "string") {
return new Blob([item]);
} else if (item instanceof Promise) {
const resolvedItem = await item;
return resolvedItem instanceof Blob ? resolvedItem : new Blob([String(resolvedItem)]);
}
throw new Error(`No item for type: ${type}`);
}
} as unknown as typeof ClipboardItem;
}

describe("copyNodeAsImage", () => {
beforeEach(() => {
vi.clearAllMocks();
});

afterEach(() => {
vi.restoreAllMocks();
});

it("should copy node as image to clipboard when node is valid", async () => {
document.body.innerHTML = '<div id="test-node">hello</div>';
const node = document.getElementById("test-node");

const mockWrite = vi.fn().mockResolvedValue(undefined);
Object.defineProperty(navigator, "clipboard", {
value: {
write: mockWrite,
read: vi.fn().mockResolvedValue([
new ClipboardItem({
"image/png": new Blob(["mock image data"], { type: "image/png" }),
}),
]),
},
writable: true,
});

if (node !== null) {
await copyNodeAsImage(node);
}

// Check if clipboard.write was called
expect(mockWrite).toHaveBeenCalled();

// Check if the written data is of the correct type
const [writeArg] = mockWrite.mock.calls[0][0];
expect(writeArg).toBeInstanceOf(ClipboardItem);
expect(writeArg.types).toContain("image/png");
});

it("should throw error when node is null", async () => {
await expect(copyNodeAsImage(null)).rejects.toThrow("Failed to copy image to clipboard");
});
});
31 changes: 31 additions & 0 deletions lib/utils/copy-to-clipboard.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import html2canvas from "html2canvas";
import { shortenUrl } from "./shorten-url";

export const copyToClipboard = async (content: string) => {
Expand All @@ -9,3 +10,33 @@ export const copyToClipboard = async (content: string) => {
console.log("This browser does not support the clipboard.", error);
}
};

/**
* Copies the content of an HTML element as an image to the clipboard.
*
* @param node The HTML element to copy as an image.
*
* @returns A promise that resolves when the image has been copied to the clipboard.
*/
export async function copyNodeAsImage(node: HTMLElement | null) {
if (!node) {
throw new Error("Failed to copy image to clipboard");
}

await navigator.clipboard.write([
new ClipboardItem({
"image/png": new Promise(async (resolve, reject) => {
html2canvas(node).then((canvas) => {
canvas.toBlob((blob) => {
if (!blob) {
reject("Failed to copy image to clipboard");
return;
}

resolve(new Blob([blob], { type: "image/png" }));
}, "image/png");
});
}),
}),
]);
}
50 changes: 50 additions & 0 deletions npm-shrinkwrap.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@
"echarts": "^5.4.1",
"echarts-for-react": "^3.0.2",
"embla-carousel-react": "^8.1.3",
"html2canvas": "^1.4.1",
"next": "^14.1.4",
"nextjs-progressbar": "^0.0.16",
"octokit": "^3.1.2",
Expand Down
Loading