Skip to content

Commit 1eb434b

Browse files
committed
feat(core): iframe address bar
1 parent 293b95b commit 1eb434b

File tree

6 files changed

+265
-22
lines changed

6 files changed

+265
-22
lines changed

packages/core/src/client/webcomponents/.generated/css.ts

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

packages/core/src/client/webcomponents/components/ViewBuiltinSettings.vue

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,32 @@ function resetSettings() {
260260
</div>
261261
</section>
262262

263+
<section class="border-t border-base pt-6 mb-8">
264+
<h2 class="text-lg font-medium mb-4 op75">
265+
Appearance
266+
</h2>
267+
268+
<div class="flex flex-col gap-3">
269+
<!-- Show iframe address bar toggle -->
270+
<label class="flex items-center gap-3 cursor-pointer group">
271+
<button
272+
class="w-10 h-6 rounded-full transition-colors relative shrink-0"
273+
:class="settings.showIframeAddressBar ? 'bg-lime' : 'bg-gray/30'"
274+
@click="settingsStore.mutate((s) => { s.showIframeAddressBar = !s.showIframeAddressBar })"
275+
>
276+
<div
277+
class="absolute top-1 w-4 h-4 rounded-full bg-white shadow transition-transform"
278+
:class="settings.showIframeAddressBar ? 'translate-x-5' : 'translate-x-1'"
279+
/>
280+
</button>
281+
<div class="flex flex-col">
282+
<span class="text-sm">Show iframe address bar</span>
283+
<span class="text-xs op50">Display navigation controls and URL bar for iframe views</span>
284+
</div>
285+
</label>
286+
</div>
287+
</section>
288+
263289
<section class="border-t border-base pt-6">
264290
<h2 class="text-lg font-medium mb-4 op75">
265291
Reset

packages/core/src/client/webcomponents/components/ViewIframe.vue

Lines changed: 227 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import type { DevToolsViewIframe } from '@vitejs/devtools-kit'
33
import type { DocksContext } from '@vitejs/devtools-kit/client'
44
import type { CSSProperties } from 'vue'
55
import 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
89
const 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+
1520
const isLoading = ref(true)
21+
const isIframeLoading = ref(false)
1622
const 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
18155
onMounted(() => {
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
53209
onUnmounted(() => {
@@ -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>

packages/core/src/client/webcomponents/utils/PersistedDomViewsManager.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ export class PersistedDomViewsManager {
4848
export class PersistedDomHolder<ElementType extends HTMLElement> {
4949
readonly element: ElementType
5050
readonly id: string
51-
parent?: Element
51+
anchor?: Element
5252

5353
_cleanups: (() => void)[] = []
5454

@@ -63,17 +63,17 @@ export class PersistedDomHolder<ElementType extends HTMLElement> {
6363
}
6464

6565
mount(parent: Element) {
66-
if (this.parent === parent) {
66+
if (this.anchor === parent) {
6767
this.show()
6868
return
6969
}
7070

7171
this.cleanup()
72-
this.parent = parent
72+
this.anchor = parent
7373

74-
const func = () => this.update()
75-
window.addEventListener('resize', func)
76-
this._cleanups.push(() => window.removeEventListener('resize', func))
74+
const update = () => this.update()
75+
window.addEventListener('resize', update)
76+
this._cleanups.push(() => window.removeEventListener('resize', update))
7777
this.show()
7878
}
7979

@@ -87,9 +87,9 @@ export class PersistedDomHolder<ElementType extends HTMLElement> {
8787
}
8888

8989
update() {
90-
if (!this.parent)
90+
if (!this.anchor)
9191
return
92-
const rect = this.parent.getBoundingClientRect()
92+
const rect = this.anchor.getBoundingClientRect()
9393
this.element.style.position = 'absolute'
9494
this.element.style.width = `${rect.width}px`
9595
this.element.style.height = `${rect.height}px`
@@ -98,6 +98,6 @@ export class PersistedDomHolder<ElementType extends HTMLElement> {
9898
unmount() {
9999
this.cleanup()
100100
this.hide()
101-
this.parent = undefined
101+
this.anchor = undefined
102102
}
103103
}

packages/kit/src/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,5 @@ export const DEFAULT_STATE_DOCKS_SETTINGS: () => DevToolsDocksUserSettings = ()
1515
hiddenCategories: [],
1616
pinnedDocks: [],
1717
customOrder: {},
18+
showIframeAddressBar: false,
1819
})

packages/kit/src/types/settings.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ export interface DevToolsDocksUserSettings {
33
hiddenCategories: string[]
44
pinnedDocks: string[]
55
customOrder: Record<string, number>
6+
showIframeAddressBar: boolean
67
}

0 commit comments

Comments
 (0)