(host: BootHost)
| 110 | } |
| 111 | |
| 112 | export function openLegacyExportDialog(host: BootHost): void { |
| 113 | const room = host.SocialCalc._room ?? ''; |
| 114 | if (!room) return; |
| 115 | |
| 116 | const vex = |
| 117 | host.vex ?? |
| 118 | ( |
| 119 | typeof window !== 'undefined' |
| 120 | ? (window as Window & { vex?: BootHost['vex'] }).vex |
| 121 | : undefined |
| 122 | ); |
| 123 | if (!vex?.dialog?.open) return; |
| 124 | |
| 125 | // `window.parent.location` throws a SecurityError in cross-origin |
| 126 | // iframe embeds (Sandstorm, notion integrations, etc.). Fall back to |
| 127 | // the current document's location when that happens — the export-URL |
| 128 | // builder handles both shapes. |
| 129 | const readParentLocation = (): { href?: string; pathname: string } | undefined => { |
| 130 | if (host.parent?.location) return host.parent.location; |
| 131 | if (typeof window === 'undefined') return undefined; |
| 132 | try { |
| 133 | return window.parent.location; |
| 134 | } catch { |
| 135 | return undefined; |
| 136 | } |
| 137 | }; |
| 138 | const parentLocation = |
| 139 | readParentLocation() ?? { pathname: host.location.pathname }; |
| 140 | const multiRows = |
| 141 | (host as BootHost & { __MULTI__?: { rows?: unknown[] } }).__MULTI__?.rows ?? |
| 142 | (typeof window !== 'undefined' |
| 143 | ? (window as Window & { __MULTI__?: { rows?: unknown[] } }).__MULTI__?.rows |
| 144 | : undefined); |
| 145 | // Empty `rows: []` is truthy — must check length (#232 viewer export). |
| 146 | const isMultiple = (multiRows?.length ?? 0) > 0 || /\.[1-9]\d*$/.test(room); |
| 147 | // Production path: synthetic `<a>` click. `window.open()` is |
| 148 | // popup-blocked in Chrome when called from inside the vex-dialog |
| 149 | // button handler (it's one async tick removed from the direct user |
| 150 | // click, so Chrome's user-activation window has lapsed). Anchor |
| 151 | // clicks don't hit the popup blocker, and they also honor the |
| 152 | // server's `Content-Disposition` header: CSV/XLSX/ODS download |
| 153 | // (server sets attachment), HTML opens inline (server doesn't). |
| 154 | // |
| 155 | // Tests override via `host.__exportOpen`; we deliberately do NOT |
| 156 | // fall back to `host.open` because in production `host === window` |
| 157 | // and `window.open` is the popup-blocked path we're avoiding. |
| 158 | // Production download path. |
| 159 | // |
| 160 | // Why `fetch → blob → object URL → anchor click` instead of a plain |
| 161 | // anchor pointing at the server URL: Chrome's "automatic downloads" |
| 162 | // security policy blocks same-origin navigation/download anchors |
| 163 | // that fire after any async hop (our vex dialog button -> callback |
| 164 | // -> openFormat is two ticks past the direct user click, so the |
| 165 | // user-activation gate has already closed). Pulling the bytes via |
| 166 | // fetch and then clicking an anchor that points at a blob: URL |
| 167 | // bypasses that gate — the click is treated as a direct save of |
| 168 | // local data, not a new navigation. This works uniformly for all |
| 169 | // four formats (xlsx/ods are binary, csv/html are text) and |
no test coverage detected