From 0aebe6c85d1c6d5ac69bb81421f3afd31a528231 Mon Sep 17 00:00:00 2001 From: Emmanuel Ogbizi-Ugbe Date: Thu, 19 Dec 2024 00:12:52 -0500 Subject: [PATCH 1/9] feat(export-element): add export to png feature --- libraries/html2canvas.ts | 4 +++ package.json | 3 +- pnpm-lock.yaml | 39 ++++++++++++++++++++++++ scripts/export-element/index.user.ts | 45 ++++++++++++++++++++++++---- 4 files changed, 84 insertions(+), 7 deletions(-) create mode 100644 libraries/html2canvas.ts diff --git a/libraries/html2canvas.ts b/libraries/html2canvas.ts new file mode 100644 index 00000000..59c8d155 --- /dev/null +++ b/libraries/html2canvas.ts @@ -0,0 +1,4 @@ +// rigmarole to get html2canvas types to work with typescript imports +import { default as Html2Canvas } from "html2canvas"; +const html2canvasFn = require("html2canvas") as typeof Html2Canvas; +export { html2canvasFn as html2canvas }; diff --git a/package.json b/package.json index 67379ad2..edb581b7 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,8 @@ } }, "dependencies": { - "cheerio": "git+http://github.com/iamogbz/cheerio-web.git" + "cheerio": "git+http://github.com/iamogbz/cheerio-web.git", + "html2canvas": "^1.4.1" }, "devDependencies": { "@commitlint/cli": "^19.6.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 38a02c54..8fa9e6ce 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: cheerio: specifier: git+http://github.com/iamogbz/cheerio-web.git version: git+http://github.com/iamogbz/cheerio-web.git#4407cd49f753558d2d19a983079155dd7e026e6b + html2canvas: + specifier: ^1.4.1 + version: 1.4.1 devDependencies: '@commitlint/cli': specifier: ^19.6.1 @@ -1227,6 +1230,10 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + base64-arraybuffer@1.0.2: + resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==} + engines: {node: '>= 0.6.0'} + base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} @@ -1574,6 +1581,9 @@ packages: resolution: {integrity: sha512-x8dy3RnvYdlUcPOjkEHqozhiwzKNSq7GcPuXFbnyMOCHxX8V3OgIg/pYuabl2sbUPfIJaeAQB7PMOK8DFIdoRA==} engines: {node: '>=12'} + css-line-break@2.1.0: + resolution: {integrity: sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==} + css-select@2.0.2: resolution: {integrity: sha512-dSpYaDVoWaELjvZ3mS6IKZM/y2PMPa/XYoEfYNZePL4U/XgyxZNroHEHReDx/d+VgXh9VbCTtFqLkFbmeqeaRQ==} @@ -2420,6 +2430,10 @@ packages: html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + html2canvas@1.4.1: + resolution: {integrity: sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==} + engines: {node: '>=8.0.0'} + htmlparser2@3.10.1: resolution: {integrity: sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==} @@ -4246,6 +4260,9 @@ packages: resolution: {integrity: sha512-te/NtwBwfiNRLf9Ijqx3T0nlqZiQ2XrrtBvu+cLL8ZRrGkO0NHTug8MYFKyoSrv/sHTaSKfilUkizV6XhxMJ3g==} engines: {node: '>=8'} + text-segmentation@1.0.3: + resolution: {integrity: sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==} + text-table@0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} @@ -4508,6 +4525,9 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + utrie@1.0.2: + resolution: {integrity: sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==} + v8-compile-cache-lib@3.0.1: resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} @@ -6211,6 +6231,8 @@ snapshots: balanced-match@1.0.2: {} + base64-arraybuffer@1.0.2: {} + base64-js@1.5.1: {} basic-auth@2.0.1: @@ -6588,6 +6610,10 @@ snapshots: dependencies: type-fest: 1.4.0 + css-line-break@2.1.0: + dependencies: + utrie: 1.0.2 + css-select@2.0.2: dependencies: boolbase: 1.0.0 @@ -7617,6 +7643,11 @@ snapshots: html-escaper@2.0.2: {} + html2canvas@1.4.1: + dependencies: + css-line-break: 2.1.0 + text-segmentation: 1.0.3 + htmlparser2@3.10.1: dependencies: domelementtype: 1.3.1 @@ -9639,6 +9670,10 @@ snapshots: text-extensions@2.4.0: {} + text-segmentation@1.0.3: + dependencies: + utrie: 1.0.2 + text-table@0.2.0: {} then-request@6.0.2: @@ -9910,6 +9945,10 @@ snapshots: util-deprecate@1.0.2: {} + utrie@1.0.2: + dependencies: + base64-arraybuffer: 1.0.2 + v8-compile-cache-lib@3.0.1: {} v8-to-istanbul@9.1.0: diff --git a/scripts/export-element/index.user.ts b/scripts/export-element/index.user.ts index ad3c666d..8e668d6c 100644 --- a/scripts/export-element/index.user.ts +++ b/scripts/export-element/index.user.ts @@ -1,16 +1,23 @@ +import { html2canvas } from "libraries/html2canvas"; +import { doc } from "prettier"; + (function () { "use strict"; const Types = Object.freeze({ PDF: "pdf", - // PNG: "png", // TODO: add png support + PNG: "png", }); + type ExportType = (typeof Types)[keyof typeof Types]; + const params: { + matchWords: string[]; eventTarget: EventTarget | null; target: Node | null; - type: (typeof Types)[keyof typeof Types]; + type: ExportType; } = { + matchWords: [], eventTarget: null, target: null, type: Types.PDF, @@ -53,11 +60,11 @@ )?.value?.createRange?.()?.text ?? ""; const matchText = sanitizeText(selectedText); - const matchWords = matchText.split(" "); + params.matchWords = matchText.split(" "); params.target = findLastNodeWithPredicate(event.target as Node, (node) => { const nodeText = sanitizeText((node as HTMLElement).innerText); - return containsAll(nodeText, matchWords); + return containsAll(nodeText, params.matchWords); }); }); @@ -172,17 +179,43 @@ }); } + async function cloneAndDownloadImage(node: Node) { + const clone = cloneNodeWithStyles(window, node); + + // place the clone in a hidden div to enable html2canvas to render it + const placeholderDiv = document.createElement("div"); + placeholderDiv.style.visibility = "hidden"; + placeholderDiv.appendChild(clone); + document.body.appendChild(placeholderDiv); + + // https://stackoverflow.com/questions/3906142/how-to-save-a-png-from-javascript-variable + const canvas = await html2canvas(clone); + const dataURI = canvas.toDataURL("image/png"); + const filename = `${["screenshot", ...params.matchWords.slice(0, 10)] + .join("-") + .toLowerCase() + .replace(/[/\\?%*:|"<>]+/g, "_")}.png`; + const download = document.createElement("a"); + download.href = dataURI; + download.download = filename; + download.click(); + document.body.removeChild(placeholderDiv); + } + /** * Print the context node as specified type */ - function printNodeAs(type: (typeof params)["type"]) { + function printNodeAs(type: ExportType) { if (params.target) { switch (type) { case Types.PDF: { return cloneAndPrintNode(params.target); } + case Types.PNG: { + return cloneAndDownloadImage(params.target); + } default: { - alert(`Unsupported type: ${type}`); + return alert(`Unsupported type: ${type}`); } } } else { From 0389afc93381602cbf9405ec4120cbb2011351cd Mon Sep 17 00:00:00 2001 From: Emmanuel Ogbizi-Ugbe Date: Thu, 19 Dec 2024 00:19:54 -0500 Subject: [PATCH 2/9] wip: remove unused parameter --- scripts/export-element/index.user.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/scripts/export-element/index.user.ts b/scripts/export-element/index.user.ts index 8e668d6c..f64aae76 100644 --- a/scripts/export-element/index.user.ts +++ b/scripts/export-element/index.user.ts @@ -15,12 +15,10 @@ import { doc } from "prettier"; matchWords: string[]; eventTarget: EventTarget | null; target: Node | null; - type: ExportType; } = { matchWords: [], eventTarget: null, target: null, - type: Types.PDF, }; /** From c15d6a372eb65e329171211c21bd2aac1144ebbe Mon Sep 17 00:00:00 2001 From: Emmanuel Ogbizi-Ugbe Date: Thu, 19 Dec 2024 00:24:23 -0500 Subject: [PATCH 3/9] fix: remove unused dependency --- scripts/export-element/index.user.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/scripts/export-element/index.user.ts b/scripts/export-element/index.user.ts index f64aae76..3777e3a0 100644 --- a/scripts/export-element/index.user.ts +++ b/scripts/export-element/index.user.ts @@ -1,5 +1,4 @@ import { html2canvas } from "libraries/html2canvas"; -import { doc } from "prettier"; (function () { "use strict"; From d94c42da3cfa3b48dc88fc68b4df294c1422a4a2 Mon Sep 17 00:00:00 2001 From: Emmanuel Ogbizi-Ugbe Date: Thu, 19 Dec 2024 00:34:58 -0500 Subject: [PATCH 4/9] fix: update target on mouse up action --- scripts/export-element/index.user.ts | 38 +++++++++++++++++----------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/scripts/export-element/index.user.ts b/scripts/export-element/index.user.ts index 3777e3a0..6345a7e2 100644 --- a/scripts/export-element/index.user.ts +++ b/scripts/export-element/index.user.ts @@ -45,25 +45,33 @@ import { html2canvas } from "libraries/html2canvas"; } // Add context menu to user script - document.addEventListener("contextmenu", function (event) { + function handleUpdateTarget(event: MouseEvent) { params.eventTarget = event.target; - - const selectedText = - window.getSelection?.()?.toString() ?? - document.getSelection?.()?.toString() ?? - Object.getOwnPropertyDescriptor( - document, - "selection", - )?.value?.createRange?.()?.text ?? - ""; + const selection = window.getSelection?.() ?? + document.getSelection?.() ?? { + anchorNode: event.target as Node, + focusNode: event.target as Node, + toString: (): string => + Object.getOwnPropertyDescriptor( + document, + "selection", + )?.value?.createRange?.()?.text || "", + }; + + const selectedText = selection.toString(); const matchText = sanitizeText(selectedText); params.matchWords = matchText.split(" "); - params.target = findLastNodeWithPredicate(event.target as Node, (node) => { - const nodeText = sanitizeText((node as HTMLElement).innerText); - return containsAll(nodeText, params.matchWords); - }); - }); + params.target = findLastNodeWithPredicate( + selection.anchorNode ?? selection.focusNode, + (node) => { + const nodeText = sanitizeText((node as HTMLElement).innerText); + return containsAll(nodeText, params.matchWords); + }, + ); + } + document.addEventListener("contextmenu", handleUpdateTarget); + document.addEventListener("mouseup", handleUpdateTarget); function findBackgroundColor(element: Element) { const transparentColor = "transparent"; From e26c26442ad81587167e85f69815f09b08c9e8bd Mon Sep 17 00:00:00 2001 From: Emmanuel Ogbizi-Ugbe Date: Thu, 19 Dec 2024 00:43:16 -0500 Subject: [PATCH 5/9] wip: add image download link target --- scripts/export-element/index.user.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/scripts/export-element/index.user.ts b/scripts/export-element/index.user.ts index 6345a7e2..a8bf9db2 100644 --- a/scripts/export-element/index.user.ts +++ b/scripts/export-element/index.user.ts @@ -200,10 +200,11 @@ import { html2canvas } from "libraries/html2canvas"; .join("-") .toLowerCase() .replace(/[/\\?%*:|"<>]+/g, "_")}.png`; - const download = document.createElement("a"); - download.href = dataURI; - download.download = filename; - download.click(); + const imageLink = document.createElement("a"); + imageLink.target = "_blank"; + imageLink.href = dataURI; + imageLink.download = filename; + imageLink.click(); document.body.removeChild(placeholderDiv); } From 47d3c1b03819d4e7217d3298cdd91b57d6b550ef Mon Sep 17 00:00:00 2001 From: Emmanuel Ogbizi-Ugbe Date: Thu, 19 Dec 2024 00:54:25 -0500 Subject: [PATCH 6/9] wip: use canvas to blob over data url --- scripts/export-element/index.user.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/scripts/export-element/index.user.ts b/scripts/export-element/index.user.ts index a8bf9db2..28a7e71f 100644 --- a/scripts/export-element/index.user.ts +++ b/scripts/export-element/index.user.ts @@ -184,10 +184,14 @@ import { html2canvas } from "libraries/html2canvas"; }); } + /** + * Clone node as image and trigger download + */ async function cloneAndDownloadImage(node: Node) { const clone = cloneNodeWithStyles(window, node); // place the clone in a hidden div to enable html2canvas to render it + // TODO: render the image in page as dismissable modal for user to save if desired const placeholderDiv = document.createElement("div"); placeholderDiv.style.visibility = "hidden"; placeholderDiv.appendChild(clone); @@ -195,11 +199,15 @@ import { html2canvas } from "libraries/html2canvas"; // https://stackoverflow.com/questions/3906142/how-to-save-a-png-from-javascript-variable const canvas = await html2canvas(clone); - const dataURI = canvas.toDataURL("image/png"); + const imageType = "image/png"; + const dataBlob = await new Promise((resolve) => { + canvas.toBlob((blob) => (blob ? resolve(blob) : undefined), imageType); + }); + const dataURI = URL.createObjectURL(dataBlob); // canvas.toDataURL(imageType); const filename = `${["screenshot", ...params.matchWords.slice(0, 10)] .join("-") .toLowerCase() - .replace(/[/\\?%*:|"<>]+/g, "_")}.png`; + .replace(/[/\\?%*:|"<>]+/g, "_")}.${imageType.split("/")[1]}`; const imageLink = document.createElement("a"); imageLink.target = "_blank"; imageLink.href = dataURI; From a622881a07918b19de377338fcb04a69064118c9 Mon Sep 17 00:00:00 2001 From: Emmanuel Ogbizi-Ugbe Date: Thu, 19 Dec 2024 09:58:07 -0500 Subject: [PATCH 7/9] wip: render clone in page before downloading --- scripts/export-element/index.user.ts | 37 ++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/scripts/export-element/index.user.ts b/scripts/export-element/index.user.ts index 28a7e71f..8e2acda1 100644 --- a/scripts/export-element/index.user.ts +++ b/scripts/export-element/index.user.ts @@ -3,6 +3,8 @@ import { html2canvas } from "libraries/html2canvas"; (function () { "use strict"; + const transparentColor = "transparent"; + const Types = Object.freeze({ PDF: "pdf", PNG: "png", @@ -74,7 +76,6 @@ import { html2canvas } from "libraries/html2canvas"; document.addEventListener("mouseup", handleUpdateTarget); function findBackgroundColor(element: Element) { - const transparentColor = "transparent"; const backgroundElement = findLastNodeWithPredicate(element, (node) => { // return true if the node is an element with a background-color that is not transparent if (!(node instanceof Element)) { @@ -189,14 +190,32 @@ import { html2canvas } from "libraries/html2canvas"; */ async function cloneAndDownloadImage(node: Node) { const clone = cloneNodeWithStyles(window, node); + clone.style.backgroundColor = findBackgroundColor(node as Element); + clone.style.outlineColor = clone.style.backgroundColor; + clone.style.outlineStyle = "solid"; + clone.style.outlineWidth = "1vw"; + clone.style.margin = "1vw auto"; + clone.style.position = "relative"; // place the clone in a hidden div to enable html2canvas to render it - // TODO: render the image in page as dismissable modal for user to save if desired const placeholderDiv = document.createElement("div"); - placeholderDiv.style.visibility = "hidden"; placeholderDiv.appendChild(clone); document.body.appendChild(placeholderDiv); + // style the placeholder div and clone + placeholderDiv.style.alignItems = "center"; + placeholderDiv.style.backdropFilter = "blur(10px)"; + placeholderDiv.style.backgroundColor = `color-mix(in srgb, ${clone.style.backgroundColor}, ${transparentColor} 50%)`; + placeholderDiv.style.display = "block"; + placeholderDiv.style.height = "100vh"; + placeholderDiv.style.justifyContent = "center"; + placeholderDiv.style.left = "0"; + placeholderDiv.style.overflow = "auto"; + placeholderDiv.style.position = "fixed"; + placeholderDiv.style.top = "0"; + placeholderDiv.style.userSelect = "none"; + placeholderDiv.style.width = "100vw"; + // https://stackoverflow.com/questions/3906142/how-to-save-a-png-from-javascript-variable const canvas = await html2canvas(clone); const imageType = "image/png"; @@ -208,12 +227,20 @@ import { html2canvas } from "libraries/html2canvas"; .join("-") .toLowerCase() .replace(/[/\\?%*:|"<>]+/g, "_")}.${imageType.split("/")[1]}`; + const imageLink = document.createElement("a"); imageLink.target = "_blank"; imageLink.href = dataURI; imageLink.download = filename; - imageLink.click(); - document.body.removeChild(placeholderDiv); + + placeholderDiv.addEventListener("click", () => placeholderDiv.remove()); + const downloadImage = (e: MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + return imageLink.click(); + }; + clone.addEventListener("click", downloadImage); + clone.addEventListener("mouseup", downloadImage); } /** From 462a6c933089cefd727cd726ae43c4a99a12b186 Mon Sep 17 00:00:00 2001 From: Emmanuel Ogbizi-Ugbe Date: Thu, 19 Dec 2024 10:43:36 -0500 Subject: [PATCH 8/9] fix: properly wrap content for rendering as image --- scripts/export-element/index.user.ts | 86 ++++++++++++++++++---------- 1 file changed, 57 insertions(+), 29 deletions(-) diff --git a/scripts/export-element/index.user.ts b/scripts/export-element/index.user.ts index 8e2acda1..ce384a4a 100644 --- a/scripts/export-element/index.user.ts +++ b/scripts/export-element/index.user.ts @@ -190,57 +190,85 @@ import { html2canvas } from "libraries/html2canvas"; */ async function cloneAndDownloadImage(node: Node) { const clone = cloneNodeWithStyles(window, node); - clone.style.backgroundColor = findBackgroundColor(node as Element); - clone.style.outlineColor = clone.style.backgroundColor; - clone.style.outlineStyle = "solid"; - clone.style.outlineWidth = "1vw"; - clone.style.margin = "1vw auto"; - clone.style.position = "relative"; + + const modalContent = document.createElement("div"); + modalContent.style.backgroundColor = findBackgroundColor(node as Element); + modalContent.style.display = "block"; + modalContent.style.height = "fit-content"; + modalContent.style.outlineColor = modalContent.style.backgroundColor; + modalContent.style.outlineStyle = "solid"; + modalContent.style.outlineWidth = "1vw"; + modalContent.style.margin = "1vw auto"; + modalContent.style.position = "relative"; + modalContent.style.width = "fit-content"; + + const modalWrapper = document.createElement("div"); + modalWrapper.style.alignItems = "center"; + modalWrapper.style.backdropFilter = "blur(10px)"; + modalWrapper.style.backgroundColor = `color-mix(in srgb, ${clone.style.backgroundColor}, ${transparentColor} 50%)`; + modalWrapper.style.display = "block"; + modalWrapper.style.height = "100vh"; + modalWrapper.style.justifyContent = "center"; + modalWrapper.style.left = "0"; + modalWrapper.style.opacity = "0"; + modalWrapper.style.overflow = "auto"; + modalWrapper.style.position = "fixed"; + modalWrapper.style.top = "0"; + modalWrapper.style.userSelect = "none"; + modalWrapper.style.width = "100vw"; + modalWrapper.style.visibility = "hidden"; + modalWrapper.style.zIndex = `${Number.MAX_SAFE_INTEGER}`; // place the clone in a hidden div to enable html2canvas to render it - const placeholderDiv = document.createElement("div"); - placeholderDiv.appendChild(clone); - document.body.appendChild(placeholderDiv); - - // style the placeholder div and clone - placeholderDiv.style.alignItems = "center"; - placeholderDiv.style.backdropFilter = "blur(10px)"; - placeholderDiv.style.backgroundColor = `color-mix(in srgb, ${clone.style.backgroundColor}, ${transparentColor} 50%)`; - placeholderDiv.style.display = "block"; - placeholderDiv.style.height = "100vh"; - placeholderDiv.style.justifyContent = "center"; - placeholderDiv.style.left = "0"; - placeholderDiv.style.overflow = "auto"; - placeholderDiv.style.position = "fixed"; - placeholderDiv.style.top = "0"; - placeholderDiv.style.userSelect = "none"; - placeholderDiv.style.width = "100vw"; + modalContent.appendChild(clone); + modalWrapper.appendChild(modalContent); + document.body.appendChild(modalWrapper); + + // scale and position clone to fit the window + const positionPreview = () => { + const scale = { + x: window.innerWidth / clone.clientWidth, + y: window.innerHeight / clone.clientHeight, + }; + modalContent.scrollIntoView({ + behavior: "smooth", + block: "center", + inline: "center", + }); + modalWrapper.style.opacity = "1"; + modalWrapper.style.visibility = "visible"; + }; + + positionPreview(); // wait for clone to be rendered before positioning it // https://stackoverflow.com/questions/3906142/how-to-save-a-png-from-javascript-variable - const canvas = await html2canvas(clone); + const canvas = await html2canvas(modalContent); const imageType = "image/png"; const dataBlob = await new Promise((resolve) => { canvas.toBlob((blob) => (blob ? resolve(blob) : undefined), imageType); }); const dataURI = URL.createObjectURL(dataBlob); // canvas.toDataURL(imageType); + const filenameGlue = "-"; const filename = `${["screenshot", ...params.matchWords.slice(0, 10)] - .join("-") + .map((w) => w.trim()) + .filter(Boolean) + .join(filenameGlue) .toLowerCase() - .replace(/[/\\?%*:|"<>]+/g, "_")}.${imageType.split("/")[1]}`; + .replace(/[/\\?%*:|"<>]+/g, filenameGlue) + .replace(/[-]+/g, filenameGlue)}.${imageType.split("/")[1]}`; const imageLink = document.createElement("a"); imageLink.target = "_blank"; imageLink.href = dataURI; imageLink.download = filename; - placeholderDiv.addEventListener("click", () => placeholderDiv.remove()); + modalWrapper.addEventListener("click", () => modalWrapper.remove()); const downloadImage = (e: MouseEvent) => { e.preventDefault(); e.stopPropagation(); return imageLink.click(); }; clone.addEventListener("click", downloadImage); - clone.addEventListener("mouseup", downloadImage); } /** @@ -260,7 +288,7 @@ import { html2canvas } from "libraries/html2canvas"; } } } else { - console.error("Node not found!", params.eventTarget); + console.error("Node not found!", params); } } From 8b14820ebebb4d509c26a261776074d63c2843c0 Mon Sep 17 00:00:00 2001 From: Emmanuel Ogbizi-Ugbe Date: Thu, 19 Dec 2024 11:06:25 -0500 Subject: [PATCH 9/9] fix: render image at full size before scaling for window fit --- scripts/export-element/index.user.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/scripts/export-element/index.user.ts b/scripts/export-element/index.user.ts index ce384a4a..b6393dd0 100644 --- a/scripts/export-element/index.user.ts +++ b/scripts/export-element/index.user.ts @@ -193,6 +193,7 @@ import { html2canvas } from "libraries/html2canvas"; const modalContent = document.createElement("div"); modalContent.style.backgroundColor = findBackgroundColor(node as Element); + modalContent.style.cursor = "pointer"; modalContent.style.display = "block"; modalContent.style.height = "fit-content"; modalContent.style.outlineColor = modalContent.style.backgroundColor; @@ -210,14 +211,14 @@ import { html2canvas } from "libraries/html2canvas"; modalWrapper.style.height = "100vh"; modalWrapper.style.justifyContent = "center"; modalWrapper.style.left = "0"; - modalWrapper.style.opacity = "0"; + modalWrapper.style.opacity = "1"; modalWrapper.style.overflow = "auto"; modalWrapper.style.position = "fixed"; modalWrapper.style.top = "0"; modalWrapper.style.userSelect = "none"; modalWrapper.style.width = "100vw"; - modalWrapper.style.visibility = "hidden"; - modalWrapper.style.zIndex = `${Number.MAX_SAFE_INTEGER}`; + modalWrapper.style.visibility = "visible"; + modalWrapper.style.zIndex = `${Number.MIN_SAFE_INTEGER}`; // place the clone in a hidden div to enable html2canvas to render it modalContent.appendChild(clone); @@ -230,19 +231,18 @@ import { html2canvas } from "libraries/html2canvas"; x: window.innerWidth / clone.clientWidth, y: window.innerHeight / clone.clientHeight, }; + modalContent.style.scale = `${Math.min(1, scale.x, scale.y)}`; modalContent.scrollIntoView({ behavior: "smooth", block: "center", inline: "center", }); - modalWrapper.style.opacity = "1"; - modalWrapper.style.visibility = "visible"; + modalWrapper.style.zIndex = `${Number.MAX_SAFE_INTEGER}`; }; - positionPreview(); // wait for clone to be rendered before positioning it - // https://stackoverflow.com/questions/3906142/how-to-save-a-png-from-javascript-variable const canvas = await html2canvas(modalContent); + positionPreview(); // wait for clone to be rendered before positioning it const imageType = "image/png"; const dataBlob = await new Promise((resolve) => { canvas.toBlob((blob) => (blob ? resolve(blob) : undefined), imageType);