mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-03-23 07:40:46 +08:00
Compare commits
3 Commits
v2026.3.11
...
d4c74eb05a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d4c74eb05a | ||
|
|
8f571ddc30 | ||
|
|
13ad1beb10 |
@@ -17,7 +17,14 @@ export async function printToPdf(
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const iframe = document.createElement('iframe');
|
||||
document.body.append(iframe);
|
||||
iframe.style.display = 'none';
|
||||
// Use a hidden but rendering-enabled state instead of display: none
|
||||
Object.assign(iframe.style, {
|
||||
visibility: 'hidden',
|
||||
position: 'absolute',
|
||||
width: '0',
|
||||
height: '0',
|
||||
border: 'none',
|
||||
});
|
||||
iframe.srcdoc = '<!DOCTYPE html>';
|
||||
iframe.onload = async () => {
|
||||
if (!iframe.contentWindow) {
|
||||
@@ -28,6 +35,44 @@ export async function printToPdf(
|
||||
reject(new Error('Root element not defined, unable to print pdf'));
|
||||
return;
|
||||
}
|
||||
|
||||
const doc = iframe.contentWindow.document;
|
||||
|
||||
doc.write(`<!DOCTYPE html><html><head><style>@media print {
|
||||
html, body {
|
||||
height: initial !important;
|
||||
overflow: initial !important;
|
||||
print-color-adjust: exact;
|
||||
-webkit-print-color-adjust: exact;
|
||||
color: #000 !important;
|
||||
background: #fff !important;
|
||||
color-scheme: light !important;
|
||||
}
|
||||
::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
:root, body {
|
||||
--affine-text-primary: #000 !important;
|
||||
--affine-text-secondary: #111 !important;
|
||||
--affine-text-tertiary: #333 !important;
|
||||
--affine-background-primary: #fff !important;
|
||||
--affine-background-secondary: #fff !important;
|
||||
--affine-background-tertiary: #fff !important;
|
||||
}
|
||||
body, [data-theme='dark'] {
|
||||
color: #000 !important;
|
||||
background: #fff !important;
|
||||
}
|
||||
body * {
|
||||
color: #000 !important;
|
||||
-webkit-text-fill-color: #000 !important;
|
||||
}
|
||||
:root {
|
||||
--affine-note-shadow-box: none !important;
|
||||
--affine-note-shadow-sticker: none !important;
|
||||
}
|
||||
}</style></head><body></body></html>`);
|
||||
doc.close();
|
||||
iframe.contentWindow.document
|
||||
.write(`<!DOCTYPE html><html><head><style>@media print {
|
||||
html, body {
|
||||
@@ -71,7 +116,7 @@ export async function printToPdf(
|
||||
for (const element of document.styleSheets) {
|
||||
try {
|
||||
for (const cssRule of element.cssRules) {
|
||||
const target = iframe.contentWindow.document.styleSheets[0];
|
||||
const target = doc.styleSheets[0];
|
||||
target.insertRule(cssRule.cssText, target.cssRules.length);
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -86,12 +131,33 @@ export async function printToPdf(
|
||||
}
|
||||
}
|
||||
|
||||
// Recursive function to find all canvases, including those in shadow roots
|
||||
const findAllCanvases = (root: Node): HTMLCanvasElement[] => {
|
||||
const canvases: HTMLCanvasElement[] = [];
|
||||
const traverse = (node: Node) => {
|
||||
if (node instanceof HTMLCanvasElement) {
|
||||
canvases.push(node);
|
||||
}
|
||||
if (node instanceof HTMLElement || node instanceof ShadowRoot) {
|
||||
node.childNodes.forEach(traverse);
|
||||
}
|
||||
if (node instanceof HTMLElement && node.shadowRoot) {
|
||||
traverse(node.shadowRoot);
|
||||
}
|
||||
};
|
||||
traverse(root);
|
||||
return canvases;
|
||||
};
|
||||
|
||||
// convert all canvas to image
|
||||
const canvasImgObjectUrlMap = new Map<string, string>();
|
||||
const allCanvas = rootElement.getElementsByTagName('canvas');
|
||||
const allCanvas = findAllCanvases(rootElement);
|
||||
let canvasKey = 1;
|
||||
const canvasToKeyMap = new Map<HTMLCanvasElement, string>();
|
||||
|
||||
for (const canvas of allCanvas) {
|
||||
canvas.dataset['printToPdfCanvasKey'] = canvasKey.toString();
|
||||
const key = canvasKey.toString();
|
||||
canvasToKeyMap.set(canvas, key);
|
||||
canvasKey++;
|
||||
const canvasImgObjectUrl = await new Promise<Blob | null>(resolve => {
|
||||
try {
|
||||
@@ -106,20 +172,42 @@ export async function printToPdf(
|
||||
);
|
||||
continue;
|
||||
}
|
||||
canvasImgObjectUrlMap.set(
|
||||
canvas.dataset['printToPdfCanvasKey'],
|
||||
URL.createObjectURL(canvasImgObjectUrl)
|
||||
);
|
||||
canvasImgObjectUrlMap.set(key, URL.createObjectURL(canvasImgObjectUrl));
|
||||
}
|
||||
|
||||
const importedRoot = iframe.contentWindow.document.importNode(
|
||||
rootElement,
|
||||
true
|
||||
) as HTMLDivElement;
|
||||
// Recursive deep clone that flattens Shadow DOM into Light DOM
|
||||
const deepCloneWithShadows = (node: Node): Node => {
|
||||
const clone = doc.importNode(node, false);
|
||||
|
||||
if (
|
||||
clone instanceof HTMLCanvasElement &&
|
||||
node instanceof HTMLCanvasElement
|
||||
) {
|
||||
const key = canvasToKeyMap.get(node);
|
||||
if (key) {
|
||||
clone.dataset['printToPdfCanvasKey'] = key;
|
||||
}
|
||||
}
|
||||
|
||||
const appendChildren = (source: Node) => {
|
||||
source.childNodes.forEach(child => {
|
||||
(clone as Element).append(deepCloneWithShadows(child));
|
||||
});
|
||||
};
|
||||
|
||||
if (node instanceof HTMLElement && node.shadowRoot) {
|
||||
appendChildren(node.shadowRoot);
|
||||
}
|
||||
appendChildren(node);
|
||||
|
||||
return clone;
|
||||
};
|
||||
|
||||
const importedRoot = deepCloneWithShadows(rootElement) as HTMLDivElement;
|
||||
|
||||
// force light theme in print iframe
|
||||
iframe.contentWindow.document.documentElement.dataset.theme = 'light';
|
||||
iframe.contentWindow.document.body.dataset.theme = 'light';
|
||||
doc.documentElement.dataset.theme = 'light';
|
||||
doc.body.dataset.theme = 'light';
|
||||
importedRoot.dataset.theme = 'light';
|
||||
|
||||
// draw saved canvas image to canvas
|
||||
@@ -138,17 +226,67 @@ export async function printToPdf(
|
||||
}
|
||||
}
|
||||
|
||||
// append to iframe and print
|
||||
iframe.contentWindow.document.body.append(importedRoot);
|
||||
// Remove lazy loading from all images and force reload
|
||||
const allImages = importedRoot.querySelectorAll('img');
|
||||
allImages.forEach(img => {
|
||||
img.removeAttribute('loading');
|
||||
const src = img.getAttribute('src');
|
||||
if (src) img.setAttribute('src', src);
|
||||
});
|
||||
|
||||
// append to iframe
|
||||
doc.body.append(importedRoot);
|
||||
|
||||
await options.beforeprint?.(iframe);
|
||||
|
||||
// browser may take some time to load font
|
||||
await new Promise<void>(resolve => {
|
||||
setTimeout(() => {
|
||||
resolve();
|
||||
}, 1000);
|
||||
});
|
||||
// Robust image waiting logic
|
||||
const waitForImages = async (container: HTMLElement) => {
|
||||
const images: HTMLImageElement[] = [];
|
||||
const view = container.ownerDocument.defaultView;
|
||||
if (!view) return;
|
||||
|
||||
const findImages = (root: Node) => {
|
||||
if (root instanceof view.HTMLImageElement) {
|
||||
images.push(root);
|
||||
}
|
||||
if (
|
||||
root instanceof view.HTMLElement ||
|
||||
root instanceof view.ShadowRoot
|
||||
) {
|
||||
root.childNodes.forEach(findImages);
|
||||
}
|
||||
if (root instanceof view.HTMLElement && root.shadowRoot) {
|
||||
findImages(root.shadowRoot);
|
||||
}
|
||||
};
|
||||
|
||||
findImages(container);
|
||||
|
||||
await Promise.all(
|
||||
images.map(img => {
|
||||
if (img.complete) {
|
||||
if (img.naturalWidth === 0) {
|
||||
console.warn('Image failed to load:', img.src);
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
return new Promise(resolve => {
|
||||
img.onload = resolve;
|
||||
img.onerror = resolve;
|
||||
});
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
await waitForImages(importedRoot);
|
||||
|
||||
// browser may take some time to load font or other resources
|
||||
await (doc.fonts?.ready ??
|
||||
new Promise<void>(resolve => {
|
||||
setTimeout(() => {
|
||||
resolve();
|
||||
}, 1000);
|
||||
}));
|
||||
|
||||
iframe.contentWindow.onafterprint = async () => {
|
||||
iframe.remove();
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/apollographql/apollo-ios",
|
||||
"state" : {
|
||||
"revision" : "d591c1dd55824867877cff4f6551fe32983d7f51",
|
||||
"version" : "1.23.0"
|
||||
"revision" : "1abd62f6cbc8c1f4405919d7eb6cd8e96967b07c",
|
||||
"version" : "1.25.3"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -14,7 +14,7 @@ let package = Package(
|
||||
.library(name: "AffineGraphQL", targets: ["AffineGraphQL"]),
|
||||
],
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/apollographql/apollo-ios", exact: "1.23.0"),
|
||||
.package(url: "https://github.com/apollographql/apollo-ios", exact: "1.25.3"),
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
|
||||
@@ -16,7 +16,7 @@ let package = Package(
|
||||
dependencies: [
|
||||
.package(path: "../AffineGraphQL"),
|
||||
.package(path: "../AffineResources"),
|
||||
.package(url: "https://github.com/apollographql/apollo-ios.git", from: "1.23.0"),
|
||||
.package(url: "https://github.com/apollographql/apollo-ios.git", from: "1.25.3"),
|
||||
.package(url: "https://github.com/apple/swift-collections.git", from: "1.4.0"),
|
||||
.package(url: "https://github.com/SnapKit/SnapKit.git", from: "5.7.1"),
|
||||
.package(url: "https://github.com/SwifterSwift/SwifterSwift.git", from: "6.2.0"),
|
||||
|
||||
@@ -54,13 +54,22 @@ export class I18n extends Entity {
|
||||
constructor(private readonly cache: GlobalCache) {
|
||||
super();
|
||||
this.i18n.on('languageChanged', (language: Language) => {
|
||||
document.documentElement.lang = language;
|
||||
this.applyDocumentLanguage(language);
|
||||
this.cache.set('i18n_lng', language);
|
||||
});
|
||||
}
|
||||
|
||||
init() {
|
||||
this.changeLanguage(this.currentLanguageKey$.value ?? 'en');
|
||||
const language = this.currentLanguageKey$.value ?? 'en';
|
||||
this.applyDocumentLanguage(language);
|
||||
this.changeLanguage(language);
|
||||
}
|
||||
|
||||
private applyDocumentLanguage(language: Language) {
|
||||
document.documentElement.lang = language;
|
||||
document.documentElement.dir = SUPPORTED_LANGUAGES[language]?.rtl
|
||||
? 'rtl'
|
||||
: 'ltr';
|
||||
}
|
||||
|
||||
changeLanguage = effect(
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -32,6 +32,7 @@ export const SUPPORTED_LANGUAGES: Record<
|
||||
name: string;
|
||||
originalName: string;
|
||||
flagEmoji: string;
|
||||
rtl?: boolean;
|
||||
resource:
|
||||
| LanguageResource
|
||||
| (() => Promise<{ default: Partial<LanguageResource> }>);
|
||||
@@ -149,18 +150,21 @@ export const SUPPORTED_LANGUAGES: Record<
|
||||
name: 'Urdu',
|
||||
originalName: 'اردو',
|
||||
flagEmoji: '🇵🇰',
|
||||
rtl: true,
|
||||
resource: () => import('./ur.json'),
|
||||
},
|
||||
ar: {
|
||||
name: 'Arabic',
|
||||
originalName: 'العربية',
|
||||
flagEmoji: '🇸🇦',
|
||||
rtl: true,
|
||||
resource: () => import('./ar.json'),
|
||||
},
|
||||
fa: {
|
||||
name: 'Persian',
|
||||
originalName: 'فارسی',
|
||||
flagEmoji: '🇮🇷',
|
||||
rtl: true,
|
||||
resource: () => import('./fa.json'),
|
||||
},
|
||||
uk: {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<html lang="en" dir="ltr">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta
|
||||
|
||||
Reference in New Issue
Block a user