|
5 | 5 |
|
6 | 6 | const i18n = getContext('i18n'); |
7 | 7 |
|
| 8 | +
|
8 | 9 | export let baseUrl: string; |
9 | 10 | export let port: number; |
10 | 11 | export let path: string = ''; |
|
23 | 24 | $: canGoForward = historyIndex < history.length - 1; |
24 | 25 |
|
25 | 26 | const pushHistory = (newPath: string) => { |
26 | | - // Trim forward history when navigating from a non-tip position |
27 | 27 | if (historyIndex < history.length - 1) { |
28 | 28 | history = history.slice(0, historyIndex + 1); |
29 | 29 | } |
|
35 | 35 | if (!canGoBack) return; |
36 | 36 | historyIndex -= 1; |
37 | 37 | path = history[historyIndex]; |
| 38 | + syncUrlBar(); |
38 | 39 | iframeKey += 1; |
39 | 40 | }; |
40 | 41 |
|
41 | 42 | const goForward = () => { |
42 | 43 | if (!canGoForward) return; |
43 | 44 | historyIndex += 1; |
44 | 45 | path = history[historyIndex]; |
| 46 | + syncUrlBar(); |
45 | 47 | iframeKey += 1; |
46 | 48 | }; |
47 | 49 |
|
48 | 50 | // ── URLs ───────────────────────────────────────────────────────────── |
49 | 51 | $: proxyUrl = getPortProxyUrl(baseUrl, port, path); |
50 | 52 |
|
51 | | - // The proxy path prefix, e.g. "/proxy/8080/" |
52 | 53 | $: proxyPathPrefix = (() => { |
53 | 54 | try { |
54 | | - return new URL(getPortProxyUrl(baseUrl, port, '')).pathname; |
| 55 | + return new URL(getPortProxyUrl(baseUrl, port, ''), window.location.origin).pathname; |
55 | 56 | } catch { |
56 | 57 | return `/proxy/${port}/`; |
57 | 58 | } |
58 | 59 | })(); |
59 | 60 |
|
60 | | - // Display-friendly localhost URL for the address bar |
61 | | - $: displayUrl = `localhost:${port}${path ? '/' + path : ''}`; |
62 | | - $: urlInput = displayUrl; |
| 61 | + const makeDisplayUrl = (p: string) => `localhost:${port}${p ? '/' + p : ''}`; |
| 62 | + const syncUrlBar = () => { urlInput = makeDisplayUrl(path); }; |
| 63 | + urlInput = makeDisplayUrl(path); |
63 | 64 |
|
64 | | - const refresh = () => { |
65 | | - iframeKey += 1; |
66 | | - }; |
| 65 | + const refresh = () => { iframeKey += 1; }; |
67 | 66 |
|
68 | 67 | const openExternal = () => { |
69 | 68 | window.open(proxyUrl, '_blank', 'noopener,noreferrer'); |
|
84 | 83 | path = newPath; |
85 | 84 | pushHistory(path); |
86 | 85 | } |
| 86 | + syncUrlBar(); |
87 | 87 | iframeKey += 1; |
88 | 88 | }; |
89 | 89 |
|
90 | 90 | /** |
91 | | - * After each iframe load: |
92 | | - * 1. Sync the address bar with the current iframe path |
93 | | - * 2. Inject a click interceptor to rewrite links BEFORE navigation |
94 | | - * 3. Redirect if the page already escaped the proxy (fallback) |
| 91 | + * Read the iframe's current location and sync the URL bar. |
| 92 | + * If the iframe escaped the proxy prefix, redirect it back. |
95 | 93 | */ |
96 | 94 | const onIframeLoad = () => { |
97 | 95 | isLoading = false; |
98 | 96 | if (!iframeEl) return; |
99 | 97 | try { |
100 | | - const doc = iframeEl.contentDocument; |
101 | | - const win = iframeEl.contentWindow; |
102 | | - if (!doc || !win) return; |
103 | | -
|
104 | | - const loc = win.location; |
105 | | - const iframePath = loc?.pathname ?? ''; |
106 | | - const iframeSearch = loc?.search ?? ''; |
107 | | - const iframeHash = loc?.hash ?? ''; |
| 98 | + const loc = iframeEl.contentWindow?.location; |
| 99 | + if (!loc) return; |
| 100 | + const iframePath = loc.pathname ?? ''; |
| 101 | + const iframeSearch = loc.search ?? ''; |
| 102 | + const iframeHash = loc.hash ?? ''; |
108 | 103 |
|
109 | 104 | if (iframePath.startsWith(proxyPathPrefix)) { |
110 | | - // Still inside the proxy — sync address bar |
111 | 105 | const relativePath = iframePath.slice(proxyPathPrefix.length) + iframeSearch + iframeHash; |
112 | 106 | if (relativePath !== path) { |
113 | 107 | path = relativePath; |
114 | 108 | pushHistory(path); |
115 | | - urlInput = displayUrl; |
| 109 | + syncUrlBar(); |
116 | 110 | } |
117 | | -
|
118 | | - // Inject click interceptor to rewrite links before navigation |
119 | | - injectLinkInterceptor(doc, win); |
120 | | - } else if (iframePath && iframePath !== 'about:blank') { |
121 | | - // Escaped the proxy — redirect back through it |
122 | | - const escapedPath = iframePath.replace(/^\//, '') + iframeSearch + iframeHash; |
123 | | - const correctedUrl = getPortProxyUrl(baseUrl, port, escapedPath); |
124 | | - loc?.replace(correctedUrl); |
125 | 111 | } |
126 | 112 | } catch { |
127 | 113 | // Cross-origin — can't access |
128 | 114 | } |
129 | 115 | }; |
130 | | -
|
131 | | - /** |
132 | | - * Inject a click interceptor into the iframe document that rewrites |
133 | | - * anchor hrefs to stay within the proxy path. |
134 | | - */ |
135 | | - const injectLinkInterceptor = (doc: Document, win: Window) => { |
136 | | - // Skip if we already injected |
137 | | - if ((win as any).__portPreviewIntercepted) return; |
138 | | - (win as any).__portPreviewIntercepted = true; |
139 | | -
|
140 | | - const prefix = proxyPathPrefix; |
141 | | -
|
142 | | - // Intercept all click events on anchors |
143 | | - doc.addEventListener('click', (e: MouseEvent) => { |
144 | | - const anchor = (e.target as HTMLElement)?.closest?.('a'); |
145 | | - if (!anchor || !anchor.href) return; |
146 | | -
|
147 | | - try { |
148 | | - const url = new URL(anchor.href, win.location.href); |
149 | | -
|
150 | | - // Only intercept same-origin links that escaped the proxy |
151 | | - if (url.origin === win.location.origin && !url.pathname.startsWith(prefix)) { |
152 | | - e.preventDefault(); |
153 | | - e.stopPropagation(); |
154 | | - // Rewrite to go through proxy |
155 | | - const rewrittenPath = prefix + url.pathname.replace(/^\//, '') + url.search + url.hash; |
156 | | - win.location.href = url.origin + rewrittenPath; |
157 | | - } |
158 | | - } catch { |
159 | | - // Invalid URL — let browser handle it |
160 | | - } |
161 | | - }, true); // Use capture phase to intercept before SPA routers |
162 | | -
|
163 | | - // Also intercept form submissions |
164 | | - doc.addEventListener('submit', (e: Event) => { |
165 | | - const form = e.target as HTMLFormElement; |
166 | | - if (!form?.action) return; |
167 | | -
|
168 | | - try { |
169 | | - const url = new URL(form.action, win.location.href); |
170 | | - if (url.origin === win.location.origin && !url.pathname.startsWith(prefix)) { |
171 | | - form.action = url.origin + prefix + url.pathname.replace(/^\//, '') + url.search; |
172 | | - } |
173 | | - } catch {} |
174 | | - }, true); |
175 | | -
|
176 | | - // Intercept window.open and programmatic navigation |
177 | | - const origOpen = win.open.bind(win); |
178 | | - win.open = function(url?: string | URL, ...args: any[]) { |
179 | | - if (url && typeof url === 'string') { |
180 | | - try { |
181 | | - const parsed = new URL(url, win.location.href); |
182 | | - if (parsed.origin === win.location.origin && !parsed.pathname.startsWith(prefix)) { |
183 | | - url = parsed.origin + prefix + parsed.pathname.replace(/^\//, '') + parsed.search + parsed.hash; |
184 | | - } |
185 | | - } catch {} |
186 | | - } |
187 | | - return origOpen(url, ...args); |
188 | | - }; |
189 | | - }; |
190 | | -
|
191 | | - const onIframeLoadStart = () => { |
192 | | - isLoading = true; |
193 | | - }; |
194 | 116 | </script> |
195 | 117 |
|
196 | 118 | <div class="flex flex-col h-full min-h-0"> |
|
230 | 152 | </button> |
231 | 153 | </Tooltip> |
232 | 154 |
|
233 | | - <!-- Refresh / Stop --> |
| 155 | + <!-- Refresh --> |
234 | 156 | <Tooltip content={$i18n.t('Refresh')}> |
235 | 157 | <button |
236 | 158 | class="p-1 rounded text-gray-500 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-800 hover:text-gray-700 dark:hover:text-gray-300 transition" |
237 | 159 | on:click={refresh} |
238 | 160 | aria-label={$i18n.t('Refresh')} |
239 | 161 | > |
240 | | - {#if isLoading} |
241 | | - <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-3.5 animate-spin"> |
242 | | - <path fill-rule="evenodd" d="M15.312 11.424a5.5 5.5 0 0 1-9.201 2.466l-.312-.311h2.451a.75.75 0 0 0 0-1.5H4.5a.75.75 0 0 0-.75.75v3.75a.75.75 0 0 0 1.5 0v-2.127l.13.13a7 7 0 0 0 11.712-3.138.75.75 0 0 0-1.449-.39Zm-10.624-2.85a5.5 5.5 0 0 1 9.201-2.465l.312.31H11.75a.75.75 0 0 0 0 1.5h3.75a.75.75 0 0 0 .75-.75V3.42a.75.75 0 0 0-1.5 0v2.126l-.13-.129A7 7 0 0 0 3.239 8.555a.75.75 0 0 0 1.449.39Z" clip-rule="evenodd" /> |
243 | | - </svg> |
244 | | - {:else} |
245 | | - <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-3.5"> |
246 | | - <path fill-rule="evenodd" d="M15.312 11.424a5.5 5.5 0 0 1-9.201 2.466l-.312-.311h2.451a.75.75 0 0 0 0-1.5H4.5a.75.75 0 0 0-.75.75v3.75a.75.75 0 0 0 1.5 0v-2.127l.13.13a7 7 0 0 0 11.712-3.138.75.75 0 0 0-1.449-.39Zm-10.624-2.85a5.5 5.5 0 0 1 9.201-2.465l.312.31H11.75a.75.75 0 0 0 0 1.5h3.75a.75.75 0 0 0 .75-.75V3.42a.75.75 0 0 0-1.5 0v2.126l-.13-.129A7 7 0 0 0 3.239 8.555a.75.75 0 0 0 1.449.39Z" clip-rule="evenodd" /> |
247 | | - </svg> |
248 | | - {/if} |
| 162 | + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-3.5" class:animate-spin={isLoading}> |
| 163 | + <path fill-rule="evenodd" d="M15.312 11.424a5.5 5.5 0 0 1-9.201 2.466l-.312-.311h2.451a.75.75 0 0 0 0-1.5H4.5a.75.75 0 0 0-.75.75v3.75a.75.75 0 0 0 1.5 0v-2.127l.13.13a7 7 0 0 0 11.712-3.138.75.75 0 0 0-1.449-.39Zm-10.624-2.85a5.5 5.5 0 0 1 9.201-2.465l.312.31H11.75a.75.75 0 0 0 0 1.5h3.75a.75.75 0 0 0 .75-.75V3.42a.75.75 0 0 0-1.5 0v2.126l-.13-.129A7 7 0 0 0 3.239 8.555a.75.75 0 0 0 1.449.39Z" clip-rule="evenodd" /> |
| 164 | + </svg> |
249 | 165 | </button> |
250 | 166 | </Tooltip> |
251 | 167 |
|
|
275 | 191 | </button> |
276 | 192 | </Tooltip> |
277 | 193 |
|
278 | | - <!-- Close preview --> |
| 194 | + <!-- Close --> |
279 | 195 | <Tooltip content={$i18n.t('Close')}> |
280 | 196 | <button |
281 | 197 | class="p-1 rounded text-gray-500 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-800 hover:text-gray-700 dark:hover:text-gray-300 transition" |
|
296 | 212 | </div> |
297 | 213 | {/if} |
298 | 214 |
|
299 | | - <!-- Iframe content --> |
| 215 | + <!-- Iframe --> |
300 | 216 | {#key iframeKey} |
301 | 217 | <iframe |
302 | 218 | bind:this={iframeEl} |
|
0 commit comments