@@ -3,7 +3,8 @@ import type { DevToolsViewIframe } from '@vitejs/devtools-kit'
33import type { DocksContext } from ' @vitejs/devtools-kit/client'
44import type { CSSProperties } from ' vue'
55import type { PersistedDomViewsManager } from ' ../utils/PersistedDomViewsManager'
6- import { nextTick , onMounted , onUnmounted , ref , useTemplateRef , watch , watchEffect } from ' vue'
6+ import { computed , nextTick , onMounted , onUnmounted , ref , useTemplateRef , watch , watchEffect } from ' vue'
7+ import { sharedStateToRef } from ' ../state/docks'
78
89const props = defineProps <{
910 context: DocksContext
@@ -12,26 +13,175 @@ const props = defineProps<{
1213 iframeStyle? : CSSProperties
1314}>()
1415
16+ const settings = sharedStateToRef (props .context .docks .settings )
17+ const showAddressBar = computed (() => settings .value .showIframeAddressBar ?? true )
18+ const ADDRESS_BAR_HEIGHT = 50
19+
1520const isLoading = ref (true )
21+ const isIframeLoading = ref (false )
1622const viewFrame = useTemplateRef <HTMLDivElement >(' viewFrame' )
23+ const urlInputRef = useTemplateRef <HTMLInputElement >(' urlInput' )
24+
25+ // Address bar state
26+ const currentUrl = ref (props .entry .url )
27+ const editingUrl = ref (props .entry .url )
28+ const isEditing = ref (false )
29+
30+ const iframeElement = computed (() => {
31+ return props .persistedDoms .getHolder (props .entry .id , ' iframe' )?.element
32+ })
33+
34+ // Get current page's origin for comparison
35+ const currentPageOrigin = computed (() => {
36+ try {
37+ return window .location .origin
38+ }
39+ catch {
40+ return ' '
41+ }
42+ })
43+
44+ // Check if iframe URL is cross-origin
45+ const isCrossOrigin = computed (() => {
46+ try {
47+ const url = new URL (currentUrl .value )
48+ return url .origin !== currentPageOrigin .value
49+ }
50+ catch {
51+ return true // Assume cross-origin if URL parsing fails
52+ }
53+ })
54+
55+ // Display URL - hides host if same as current page
56+ const displayUrl = computed (() => {
57+ if (isCrossOrigin .value ) {
58+ return currentUrl .value
59+ }
60+ try {
61+ const url = new URL (currentUrl .value )
62+ // Show only pathname + search + hash for same-origin
63+ return url .pathname + url .search + url .hash
64+ }
65+ catch {
66+ return currentUrl .value
67+ }
68+ })
69+
70+ function updateCurrentUrl() {
71+ try {
72+ // Try to get the current URL from the iframe (may fail due to cross-origin)
73+ const iframe = iframeElement .value
74+ if (iframe ?.contentWindow ?.location ?.href ) {
75+ currentUrl .value = iframe .contentWindow .location .href
76+ }
77+ }
78+ catch {
79+ // Cross-origin restriction, keep the last known URL
80+ }
81+ }
82+
83+ function navigateTo(url : string ) {
84+ const iframe = iframeElement .value
85+ if (! iframe )
86+ return
87+
88+ // Ensure URL has protocol
89+ let normalizedUrl = url .trim ()
90+ if (normalizedUrl && ! normalizedUrl .match (/ ^ https? :\/\/ / i )) {
91+ // If it starts with /, treat as same-origin path
92+ if (normalizedUrl .startsWith (' /' )) {
93+ normalizedUrl = ` ${window .location .origin }${normalizedUrl } `
94+ }
95+ else {
96+ normalizedUrl = ` http://${normalizedUrl } `
97+ }
98+ }
99+
100+ currentUrl .value = normalizedUrl
101+ editingUrl .value = normalizedUrl
102+ iframe .src = normalizedUrl
103+ isIframeLoading .value = true
104+ }
105+
106+ function handleUrlSubmit() {
107+ isEditing .value = false
108+ if (editingUrl .value !== currentUrl .value ) {
109+ navigateTo (editingUrl .value )
110+ }
111+ }
112+
113+ function handleUrlFocus() {
114+ isEditing .value = true
115+ editingUrl .value = currentUrl .value
116+ nextTick (() => {
117+ urlInputRef .value ?.select ()
118+ })
119+ }
120+
121+ function handleUrlBlur() {
122+ isEditing .value = false
123+ editingUrl .value = currentUrl .value
124+ }
125+
126+ function handleUrlKeydown(e : KeyboardEvent ) {
127+ if (e .key === ' Escape' ) {
128+ isEditing .value = false
129+ editingUrl .value = currentUrl .value
130+ urlInputRef .value ?.blur ()
131+ }
132+ }
133+
134+ function goBack() {
135+ try {
136+ iframeElement .value ?.contentWindow ?.history .back ()
137+ }
138+ catch {
139+ // Cross-origin restriction
140+ }
141+ }
142+
143+ function refresh() {
144+ const iframe = iframeElement .value
145+ if (! iframe )
146+ return
147+
148+ isIframeLoading .value = true
149+ // Reload by reassigning the src
150+ const src = iframe .src
151+ iframe .src = ' '
152+ iframe .src = src
153+ }
17154
18155onMounted (() => {
19156 const holder = props .persistedDoms .getOrCreateHolder (props .entry .id , ' iframe' )
20157 holder .element .style .boxShadow = ' none'
21158 holder .element .style .outline = ' none'
22- Object .assign (holder .element .style , props .iframeStyle )
23159
24160 if (! holder .element .src )
25161 holder .element .src = props .entry .url
26162
163+ // Listen for iframe load events
164+ holder .element .addEventListener (' load' , () => {
165+ isIframeLoading .value = false
166+ updateCurrentUrl ()
167+ })
168+
27169 const entryState = props .context .docks .getStateById (props .entry .id )
28170 if (entryState )
29171 entryState .domElements .iframe = holder .element
30172
31- holder .mount (viewFrame .value ! )
32- isLoading .value = false
33- nextTick (() => {
34- holder .update ()
173+ watchEffect (() => {
174+ Object .assign (holder .element .style , props .iframeStyle )
175+ if (showAddressBar .value ) {
176+ holder .element .style .marginTop = ` ${ADDRESS_BAR_HEIGHT }px `
177+ holder .element .style .borderTopLeftRadius = ' 0px'
178+ holder .element .style .borderTopRightRadius = ' 0px'
179+ }
180+ else {
181+ holder .element .style .marginTop = ' 0px'
182+ holder .element .style .borderTopLeftRadius = ' '
183+ holder .element .style .borderTopRightRadius = ' '
184+ }
35185 })
36186
37187 watch (
@@ -48,6 +198,12 @@ onMounted(() => {
48198 },
49199 { flush: ' sync' },
50200 )
201+
202+ holder .mount (viewFrame .value ! )
203+ isLoading .value = false
204+ nextTick (() => {
205+ holder .update ()
206+ })
51207})
52208
53209onUnmounted (() => {
@@ -57,12 +213,71 @@ onUnmounted(() => {
57213 </script >
58214
59215<template >
60- <div
61- ref =" viewFrame"
62- class =" vite-devtools-view-iframe w-full h-full flex items-center justify-center"
63- >
64- <div v-if =" isLoading" class =" op50 z--1" >
65- Loading iframe...
216+ <div class =" w-full h-full flex flex-col" >
217+ <div
218+ v-if =" showAddressBar"
219+ class =" flex-none px-2 w-full flex items-center gap-1 border rounded-t-md border-base border-b-0 bg-gray/5"
220+ :style =" { height: `${ADDRESS_BAR_HEIGHT}px` }"
221+ >
222+ <!-- Navigation buttons (hidden for cross-origin) -->
223+ <template v-if =" ! isCrossOrigin " >
224+ <!-- Back button -->
225+ <button
226+ class =" w-7 h-7 flex items-center justify-center rounded hover:bg-gray/15 transition-colors shrink-0"
227+ title =" Back"
228+ @click =" goBack"
229+ >
230+ <div class =" i-ph-caret-left text-base op60" />
231+ </button >
232+
233+ <!-- Refresh button -->
234+ <button
235+ class =" w-7 h-7 flex items-center justify-center rounded hover:bg-gray/15 transition-colors shrink-0"
236+ title =" Refresh"
237+ @click =" refresh"
238+ >
239+ <div class =" i-ph-arrow-clockwise text-base op60" />
240+ </button >
241+ </template >
242+
243+ <!-- Cross-origin badge -->
244+ <div
245+ v-else
246+ class =" flex items-center gap-1 px-2 py-1 rounded text-xs bg-amber/10 text-amber border border-amber/20 shrink-0"
247+ title =" Cross-origin iframe - navigation controls unavailable"
248+ >
249+ <div class =" i-ph-globe text-sm" />
250+ <span >Cross-Origin</span >
251+ </div >
252+
253+ <!-- URL input -->
254+ <div class =" flex-1 flex items-center h-8 px-2.5 rounded bg-gray/10 border border-transparent hover:border-gray/20 focus-within:border-gray/30 transition-colors" >
255+ <input
256+ ref =" urlInput"
257+ :value =" isEditing ? editingUrl : displayUrl"
258+ type =" text"
259+ class =" flex-1 bg-transparent outline-none text-sm font-mono"
260+ placeholder =" Enter URL..."
261+ :readonly =" isCrossOrigin"
262+ @input =" editingUrl = ($event.target as HTMLInputElement).value"
263+ @focus =" handleUrlFocus"
264+ @blur =" handleUrlBlur"
265+ @keydown =" handleUrlKeydown"
266+ @keydown.enter =" handleUrlSubmit"
267+ >
268+ <div
269+ v-if =" isIframeLoading"
270+ class =" i-ph-circle-notch text-sm op40 ml-2 shrink-0 animate-spin"
271+ />
272+ </div >
273+ </div >
274+ <div
275+ ref =" viewFrame"
276+ class =" vite-devtools-view-iframe w-full h-full flex-1 items-center justify-center"
277+ >
278+ <div v-if =" isLoading" class =" op50 z--1" >
279+ Loading iframe...
280+ </div >
66281 </div >
67282 </div >
68283</template >
0 commit comments