| 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 |