返回 presentation-ai
print-editor.ts
根目录 / src / lib / print-editor.ts
1 "use client";
2
3 const PRINT_PORTAL_ATTRIBUTE = "data-editor-print-portal";
4 const PRINT_CONTENT_ATTRIBUTE = "data-editor-print-content";
5 const PRINT_STYLE_ATTRIBUTE = "data-editor-print-style";
6 const PRINT_TITLE_ATTRIBUTE = "data-print-title";
7 const PRINT_TITLE_TEXT_ATTRIBUTE = "data-print-title-text";
8
9 function createPrintStyleElement(): HTMLStyleElement {
10 const style = document.createElement("style");
11 style.setAttribute(PRINT_STYLE_ATTRIBUTE, "true");
12 style.textContent = `
13 @media screen {
14 [${PRINT_PORTAL_ATTRIBUTE}="true"] {
15 display: none !important;
16 }
17 }
18
19 @media print {
20 @page {
21 margin: 0;
22 size: auto;
23 }
24
25 html,
26 body {
27 margin: 0 !important;
28 padding: 0 !important;
29 width: 100% !important;
30 min-height: auto !important;
31 background: #fff !important;
32 }
33
34 body > *:not([${PRINT_PORTAL_ATTRIBUTE}="true"]) {
35 display: none !important;
36 }
37
38 [${PRINT_PORTAL_ATTRIBUTE}="true"] {
39 display: block !important;
40 margin: 0 !important;
41 padding: 0 !important;
42 width: 100% !important;
43 background: #fff !important;
44 }
45
46 [${PRINT_CONTENT_ATTRIBUTE}="true"] {
47 display: block !important;
48 box-sizing: border-box !important;
49 width: 100% !important;
50 max-width: none !important;
51 min-height: auto !important;
52 height: auto !important;
53 margin: 0 !important;
54 padding: 0 !important;
55 overflow: visible !important;
56 background: #fff !important;
57 }
58
59 [${PRINT_CONTENT_ATTRIBUTE}="true"] {
60 color-scheme: light !important;
61 --background: 0 0% 100% !important;
62 --foreground: 0 0% 3.9% !important;
63 --dbi: 0 0% 0% !important;
64 --dbi-background: 0 0% 100% !important;
65 --card: 0 0% 100% !important;
66 --card-foreground: 0 0% 3.9% !important;
67 --popover: 0 0% 100% !important;
68 --popover-foreground: 0 0% 3.9% !important;
69 --primary: 0 0% 9% !important;
70 --primary-foreground: 0 0% 98% !important;
71 --brand: 0 0% 9% !important;
72 --secondary: 0 0% 96.1% !important;
73 --secondary-foreground: 0 0% 9% !important;
74 --muted: 0 0% 92.1% !important;
75 --muted-foreground: 0 0% 45.1% !important;
76 --accent: 0 0% 96.1% !important;
77 --accent-foreground: 0 0% 9% !important;
78 --destructive: 0 84.2% 60.2% !important;
79 --destructive-foreground: 0 0% 98% !important;
80 --border: 0 0% 89.8% !important;
81 --input: 0 0% 89.8% !important;
82 --ring: 0 0% 3.9% !important;
83 }
84
85 [${PRINT_CONTENT_ATTRIBUTE}="true"] * {
86 max-height: none !important;
87 }
88
89 [${PRINT_CONTENT_ATTRIBUTE}="true"],
90 [${PRINT_CONTENT_ATTRIBUTE}="true"] * {
91 -webkit-print-color-adjust: exact;
92 print-color-adjust: exact;
93 }
94
95 [${PRINT_CONTENT_ATTRIBUTE}="true"] [data-print-doc-shell="true"] {
96 background: #fff !important;
97 }
98
99 [${PRINT_CONTENT_ATTRIBUTE}="true"] [data-print-doc-layout="true"] {
100 display: block !important;
101 height: auto !important;
102 }
103
104 [${PRINT_CONTENT_ATTRIBUTE}="true"] [data-print-doc-width="true"] {
105 width: 100% !important;
106 max-width: none !important;
107 padding: 0 !important;
108 }
109
110 [${PRINT_CONTENT_ATTRIBUTE}="true"] [data-print-doc-surface="true"] {
111 min-height: auto !important;
112 margin: 0 !important;
113 border: 0 !important;
114 border-radius: 0 !important;
115 box-shadow: none !important;
116 color: hsl(0 0% 3.9%) !important;
117 }
118
119 [${PRINT_CONTENT_ATTRIBUTE}="true"] [data-print-doc-surface="true"] :where(
120 h1,
121 h2,
122 h3,
123 h4,
124 h5,
125 h6,
126 p,
127 span,
128 div,
129 li,
130 ul,
131 ol,
132 blockquote,
133 strong,
134 em,
135 a,
136 code,
137 pre,
138 td,
139 th
140 ) {
141 color: inherit !important;
142 }
143
144 [${PRINT_CONTENT_ATTRIBUTE}="true"] [data-print-doc-surface="true"] :where(
145 input,
146 textarea,
147 [contenteditable="true"],
148 [data-slate-editor="true"]
149 ) {
150 color: inherit !important;
151 -webkit-text-fill-color: currentcolor !important;
152 }
153
154 [${PRINT_CONTENT_ATTRIBUTE}="true"] [data-print-doc-surface="true"] :where(
155 input,
156 textarea
157 )::placeholder {
158 color: hsl(0 0% 45.1%) !important;
159 }
160
161 [${PRINT_CONTENT_ATTRIBUTE}="true"] [data-print-doc-body="true"] {
162 min-height: auto !important;
163 padding: 1.25rem 2rem 2rem !important;
164 }
165
166 [${PRINT_CONTENT_ATTRIBUTE}="true"] [${PRINT_TITLE_TEXT_ATTRIBUTE}="true"] {
167 display: block !important;
168 width: 100% !important;
169 min-width: 0 !important;
170 margin: 0 0 1rem 0 !important;
171 padding: 0 !important;
172 white-space: pre-wrap !important;
173 overflow-wrap: anywhere !important;
174 word-break: break-word !important;
175 }
176 }
177 `;
178
179 return style;
180 }
181
182 function replacePrintableTitleFields(root: HTMLElement): void {
183 const titleFields = root.querySelectorAll<
184 HTMLInputElement | HTMLTextAreaElement
185 >(`[${PRINT_TITLE_ATTRIBUTE}="true"]`);
186
187 for (const titleField of titleFields) {
188 const printableTitle = document.createElement("div");
189 printableTitle.setAttribute(PRINT_TITLE_TEXT_ATTRIBUTE, "true");
190 printableTitle.className = titleField.className;
191 printableTitle.textContent =
192 titleField.value.trim() || titleField.placeholder.trim();
193
194 titleField.replaceWith(printableTitle);
195 }
196 }
197
198 function clonePrintTarget(target: HTMLElement): HTMLElement {
199 const clone = target.cloneNode(true);
200
201 if (!(clone instanceof HTMLElement)) {
202 throw new Error("Failed to clone editor for printing.");
203 }
204
205 clone.setAttribute(PRINT_CONTENT_ATTRIBUTE, "true");
206 replacePrintableTitleFields(clone);
207
208 return clone;
209 }
210
211 export function printEditorElement(target: HTMLElement): void {
212 const portal = document.createElement("div");
213 portal.setAttribute(PRINT_PORTAL_ATTRIBUTE, "true");
214
215 const clonedTarget = clonePrintTarget(target);
216 const style = createPrintStyleElement();
217
218 portal.appendChild(clonedTarget);
219 document.body.appendChild(portal);
220 document.head.appendChild(style);
221
222 const cleanup = () => {
223 style.remove();
224 portal.remove();
225 };
226
227 if ("onafterprint" in window) {
228 window.addEventListener("afterprint", cleanup, { once: true });
229 }
230
231 try {
232 window.print();
233 } finally {
234 if (!("onafterprint" in window)) {
235 cleanup();
236 }
237 }
238 }
239
239 lines TYPESCRIPT