|
44 | 44 | let data = $state<PreviewResponse | null>(null) |
45 | 45 | /** Index into the flat list of [...recipients, confirmation?] */ |
46 | 46 | let activeIdx = $state(0) |
| 47 | + /** Which MIME alternative to show in the body. Mail clients pick |
| 48 | + * HTML when available; the plain-text branch is what text-only |
| 49 | + * clients (or accessibility tools) actually render. */ |
| 50 | + let bodyView: 'html' | 'text' = $state('html') |
47 | 51 |
|
48 | 52 | let allEmails = $derived.by<RenderedEmail[]>(() => { |
49 | 53 | if (!data) return [] |
|
83 | 87 | function handleKey(e: KeyboardEvent) { |
84 | 88 | if (e.key === 'Escape') onClose() |
85 | 89 | } |
| 90 | +
|
| 91 | + /** Inject `<base target="_blank">` into the rendered email HTML so |
| 92 | + * anchor clicks open in a new top-level tab instead of trying to |
| 93 | + * navigate the sandboxed iframe (which has no `allow-top-navigation` |
| 94 | + * and would otherwise blank the body). The `allow-popups` / |
| 95 | + * `allow-popups-to-escape-sandbox` flags already permit the new |
| 96 | + * tab. We try to splice into an existing `<head>`; if cryptify ever |
| 97 | + * changes the template shape, the prepended fallback still works |
| 98 | + * because browsers tolerate a `<base>` before `<html>`. */ |
| 99 | + function withBaseTarget(html: string): string { |
| 100 | + const tag = '<base target="_blank" rel="noopener">' |
| 101 | + const headIdx = html.search(/<head[^>]*>/i) |
| 102 | + if (headIdx >= 0) { |
| 103 | + const end = html.indexOf('>', headIdx) + 1 |
| 104 | + return html.slice(0, end) + tag + html.slice(end) |
| 105 | + } |
| 106 | + return tag + html |
| 107 | + } |
86 | 108 | </script> |
87 | 109 |
|
88 | 110 | <svelte:window onkeydown={handleKey} /> |
|
182 | 204 | </div> |
183 | 205 | </section> |
184 | 206 |
|
185 | | - <iframe |
186 | | - class="email-frame" |
187 | | - title={$_('filesharing.emailPreview.iframeTitle')} |
188 | | - srcdoc={active.html} |
189 | | - sandbox="allow-popups allow-popups-to-escape-sandbox" |
190 | | - ></iframe> |
| 207 | + <div class="body-tabs" role="tablist"> |
| 208 | + <button |
| 209 | + type="button" |
| 210 | + role="tab" |
| 211 | + aria-selected={bodyView === 'html'} |
| 212 | + class="body-tab" |
| 213 | + class:active={bodyView === 'html'} |
| 214 | + onclick={() => (bodyView = 'html')} |
| 215 | + >{$_('filesharing.emailPreview.viewHtml')}</button |
| 216 | + > |
| 217 | + <button |
| 218 | + type="button" |
| 219 | + role="tab" |
| 220 | + aria-selected={bodyView === 'text'} |
| 221 | + class="body-tab" |
| 222 | + class:active={bodyView === 'text'} |
| 223 | + onclick={() => (bodyView = 'text')} |
| 224 | + >{$_('filesharing.emailPreview.viewText')}</button |
| 225 | + > |
| 226 | + </div> |
| 227 | + |
| 228 | + {#if bodyView === 'html'} |
| 229 | + <iframe |
| 230 | + class="email-frame" |
| 231 | + title={$_('filesharing.emailPreview.iframeTitle')} |
| 232 | + srcdoc={withBaseTarget(active.html)} |
| 233 | + sandbox="allow-popups allow-popups-to-escape-sandbox" |
| 234 | + ></iframe> |
| 235 | + {:else} |
| 236 | + <pre class="email-text">{active.text}</pre> |
| 237 | + {/if} |
191 | 238 | {/if} |
192 | 239 | </div> |
193 | 240 | </div> |
|
313 | 360 | font-size: var(--pg-font-size-sm); |
314 | 361 | } |
315 | 362 |
|
| 363 | + .body-tabs { |
| 364 | + display: flex; |
| 365 | + gap: 0.25rem; |
| 366 | + padding: 0.5rem 1.5rem 0; |
| 367 | + background: var(--pg-soft-background); |
| 368 | + border-bottom: 1px solid var(--pg-strong-background); |
| 369 | + } |
| 370 | +
|
| 371 | + .body-tab { |
| 372 | + background: transparent; |
| 373 | + border: 0; |
| 374 | + border-bottom: 2px solid transparent; |
| 375 | + padding: 0.4rem 0.8rem; |
| 376 | + font-family: var(--pg-font-family); |
| 377 | + font-size: var(--pg-font-size-sm); |
| 378 | + color: var(--pg-text-secondary); |
| 379 | + cursor: pointer; |
| 380 | + } |
| 381 | +
|
| 382 | + .body-tab:hover { |
| 383 | + color: var(--pg-text); |
| 384 | + } |
| 385 | +
|
| 386 | + .body-tab.active { |
| 387 | + color: var(--pg-text); |
| 388 | + border-bottom-color: var(--pg-primary-contrast); |
| 389 | + } |
| 390 | +
|
| 391 | + .body-tab:focus-visible { |
| 392 | + outline: 2px solid var(--pg-text); |
| 393 | + outline-offset: 2px; |
| 394 | + } |
| 395 | +
|
316 | 396 | .email-frame { |
317 | 397 | width: 100%; |
318 | 398 | height: min(70vh, 720px); |
319 | 399 | border: 0; |
320 | 400 | background: var(--pg-soft-background); |
321 | 401 | } |
322 | 402 |
|
| 403 | + .email-text { |
| 404 | + margin: 0; |
| 405 | + padding: 1rem 1.5rem; |
| 406 | + max-height: min(70vh, 720px); |
| 407 | + overflow: auto; |
| 408 | + background: var(--pg-soft-background); |
| 409 | + color: var(--pg-text); |
| 410 | + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; |
| 411 | + font-size: var(--pg-font-size-sm); |
| 412 | + line-height: 1.5; |
| 413 | + white-space: pre-wrap; |
| 414 | + word-break: break-word; |
| 415 | + } |
| 416 | +
|
323 | 417 | .state { |
324 | 418 | padding: 2rem 1.5rem; |
325 | 419 | text-align: center; |
|
0 commit comments