fix : dont break new line on export image
This commit is contained in:
@@ -25,6 +25,117 @@ import { cn } from "@/lib/utils";
|
||||
|
||||
const EXPORT_WIDTH = 1200;
|
||||
|
||||
type OriginalTextStyles = {
|
||||
fontSize: string;
|
||||
overflowWrap: string;
|
||||
textOverflow: string;
|
||||
webkitLineClamp: string;
|
||||
whiteSpace: string;
|
||||
wordBreak: string;
|
||||
};
|
||||
|
||||
const originalTextStyles = new WeakMap<HTMLElement, OriginalTextStyles>();
|
||||
|
||||
export function scaledExportFontSize(
|
||||
fontSize: number,
|
||||
contentWidth: number,
|
||||
availableWidth: number,
|
||||
): number {
|
||||
if (fontSize <= 0 || contentWidth <= 0 || availableWidth <= 0) return fontSize;
|
||||
return Math.min(fontSize, fontSize * availableWidth / contentWidth);
|
||||
}
|
||||
|
||||
function directTextContent(element: HTMLElement): boolean {
|
||||
return Array.from(element.childNodes).some(
|
||||
(node) => node.nodeType === Node.TEXT_NODE && node.textContent?.trim(),
|
||||
);
|
||||
}
|
||||
|
||||
function restoreExportText(root: HTMLElement) {
|
||||
root.querySelectorAll<HTMLElement>("[data-export-fitted-text]").forEach((element) => {
|
||||
const original = originalTextStyles.get(element);
|
||||
if (!original) return;
|
||||
|
||||
element.style.fontSize = original.fontSize;
|
||||
element.style.overflowWrap = original.overflowWrap;
|
||||
element.style.textOverflow = original.textOverflow;
|
||||
element.style.setProperty("-webkit-line-clamp", original.webkitLineClamp);
|
||||
element.style.whiteSpace = original.whiteSpace;
|
||||
element.style.wordBreak = original.wordBreak;
|
||||
element.removeAttribute("data-export-fitted-text");
|
||||
originalTextStyles.delete(element);
|
||||
});
|
||||
}
|
||||
|
||||
function availableTextWidth(element: HTMLElement): number {
|
||||
if (element.clientWidth > 0) {
|
||||
const style = getComputedStyle(element);
|
||||
return element.clientWidth
|
||||
- Number.parseFloat(style.paddingLeft || "0")
|
||||
- Number.parseFloat(style.paddingRight || "0");
|
||||
}
|
||||
|
||||
const parent = element.parentElement;
|
||||
if (!parent) return 0;
|
||||
const style = getComputedStyle(parent);
|
||||
return parent.clientWidth
|
||||
- Number.parseFloat(style.paddingLeft || "0")
|
||||
- Number.parseFloat(style.paddingRight || "0");
|
||||
}
|
||||
|
||||
function textContentWidth(element: HTMLElement): number {
|
||||
if (element.scrollWidth > element.clientWidth) return element.scrollWidth;
|
||||
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(element);
|
||||
return range.getBoundingClientRect().width;
|
||||
}
|
||||
|
||||
function fitExportText(root: HTMLElement) {
|
||||
restoreExportText(root);
|
||||
|
||||
const candidates = Array.from(root.querySelectorAll<HTMLElement>("*"))
|
||||
.filter((element) => directTextContent(element))
|
||||
.filter((element) => {
|
||||
const style = getComputedStyle(element);
|
||||
return style.display !== "none" && style.visibility !== "hidden";
|
||||
})
|
||||
.filter((element, _index, elements) =>
|
||||
!elements.some((parent) => parent !== element && parent.contains(element)),
|
||||
);
|
||||
|
||||
const availableWidths = new Map(
|
||||
candidates.map((element) => [element, availableTextWidth(element)]),
|
||||
);
|
||||
|
||||
candidates.forEach((element) => {
|
||||
originalTextStyles.set(element, {
|
||||
fontSize: element.style.fontSize,
|
||||
overflowWrap: element.style.overflowWrap,
|
||||
textOverflow: element.style.textOverflow,
|
||||
webkitLineClamp: element.style.getPropertyValue("-webkit-line-clamp"),
|
||||
whiteSpace: element.style.whiteSpace,
|
||||
wordBreak: element.style.wordBreak,
|
||||
});
|
||||
element.dataset.exportFittedText = "";
|
||||
element.style.setProperty("overflow-wrap", "normal", "important");
|
||||
element.style.setProperty("text-overflow", "clip", "important");
|
||||
element.style.setProperty("-webkit-line-clamp", "unset", "important");
|
||||
element.style.setProperty("white-space", "nowrap", "important");
|
||||
element.style.setProperty("word-break", "normal", "important");
|
||||
});
|
||||
|
||||
candidates.forEach((element) => {
|
||||
const availableWidth = availableWidths.get(element) ?? 0;
|
||||
const contentWidth = textContentWidth(element);
|
||||
const fontSize = Number.parseFloat(getComputedStyle(element).fontSize);
|
||||
const fittedSize = scaledExportFontSize(fontSize, contentWidth, availableWidth);
|
||||
if (fittedSize < fontSize) {
|
||||
element.style.setProperty("font-size", `${fittedSize}px`, "important");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function withoutNotesFileName(fileName: string): string {
|
||||
return /\.[^.]+$/u.test(fileName)
|
||||
? fileName.replace(/(\.[^.]+)$/u, "-without-notes$1")
|
||||
@@ -219,6 +330,7 @@ export function GuideImageExport({
|
||||
|
||||
const { domToBlob, waitUntilLoad } = await import("modern-screenshot");
|
||||
await Promise.all([document.fonts.ready, waitUntilLoad(node)]);
|
||||
fitExportText(node);
|
||||
const blob = await domToBlob(node, {
|
||||
backgroundColor: getComputedStyle(node).backgroundColor,
|
||||
type: `image/${format}`,
|
||||
@@ -264,6 +376,7 @@ export function GuideImageExport({
|
||||
|
||||
const { domToBlob, waitUntilLoad } = await import("modern-screenshot");
|
||||
await Promise.all([document.fonts.ready, waitUntilLoad(node)]);
|
||||
fitExportText(node);
|
||||
const blob = await domToBlob(node, {
|
||||
backgroundColor: getComputedStyle(node).backgroundColor,
|
||||
type: `image/${format}`,
|
||||
|
||||
Reference in New Issue
Block a user