Skip to content

Commit 4429efd

Browse files
authored
feat: manage persistent iframes/panels with iframe-pane (#394)
1 parent 180af09 commit 4429efd

15 files changed

Lines changed: 144 additions & 233 deletions

File tree

‎packages/core/package.json‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@
7979
"dompurify": "catalog:frontend",
8080
"fuse.js": "catalog:frontend",
8181
"human-id": "catalog:inlined",
82+
"iframe-pane": "catalog:frontend",
8283
"tsdown": "catalog:build",
8384
"typescript": "catalog:devtools",
8485
"unplugin-vue": "catalog:build",
@@ -103,6 +104,7 @@
103104
"fast-wrap-ansi": "0.2.0",
104105
"fuse.js": "7.3.0",
105106
"get-port-please": "3.2.0",
107+
"iframe-pane": "1.1.0",
106108
"sisteransi": "1.0.5",
107109
"zod": "4.3.6"
108110
}

‎packages/core/src/client/webcomponents/components/dock/DockEdge.vue‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
<script setup lang="ts">
22
import type { DocksContext } from '@vitejs/devtools-kit/client'
33
import type { CSSProperties } from 'vue'
4-
import { computed, h, markRaw, useTemplateRef } from 'vue'
4+
import { computed, h, useTemplateRef } from 'vue'
55
import { getEntryGroup } from '../../state/dock-settings'
66
import { setEdgePositionDropdown, setFloatingTooltip, useEdgePositionDropdown } from '../../state/floating-tooltip'
7-
import { PersistedDomViewsManager } from '../../utils/PersistedDomViewsManager'
7+
import { useIframePanes } from '../../utils/useIframePanes'
88
import ViewEntry from '../views/ViewEntry.vue'
99
import DockEntriesWithCategories from './DockEntriesWithCategories.vue'
1010
import DockGroupSidebar from './DockGroupSidebar.vue'
@@ -18,7 +18,7 @@ const context = props.context
1818
const store = context.panel.store
1919
2020
const viewsContainer = useTemplateRef<HTMLElement>('viewsContainer')
21-
const persistedDoms = markRaw(new PersistedDomViewsManager(viewsContainer))
21+
const panes = useIframePanes(viewsContainer, context.panel)
2222
2323
const isVertical = computed(() => store.position === 'left' || store.position === 'right')
2424
@@ -264,11 +264,11 @@ const contentClass = computed(() => {
264264
/>
265265
<div class="relative flex-1 min-w-0 h-full">
266266
<ViewEntry
267-
v-if="hasPanelContent && viewsContainer && selectedEntry"
267+
v-if="hasPanelContent && panes && selectedEntry"
268268
:key="selectedEntry.id"
269269
:context
270270
:entry="selectedEntry"
271-
:persisted-doms="persistedDoms"
271+
:panes="panes"
272272
/>
273273
<div
274274
id="vite-devtools-views-container"

‎packages/core/src/client/webcomponents/components/dock/DockPanel.vue‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ import type { DevToolsDockEntry } from '@vitejs/devtools-kit'
33
import type { DocksContext } from '@vitejs/devtools-kit/client'
44
import type { CSSProperties } from 'vue'
55
import { useElementBounding, useWindowSize } from '@vueuse/core'
6-
import { computed, markRaw, onMounted, reactive, ref, toRefs, useTemplateRef } from 'vue'
6+
import { computed, onMounted, reactive, ref, toRefs, useTemplateRef } from 'vue'
77
import { getEntryGroup } from '../../state/dock-settings'
8-
import { PersistedDomViewsManager } from '../../utils/PersistedDomViewsManager'
8+
import { useIframePanes } from '../../utils/useIframePanes'
99
import ViewEntry from '../views/ViewEntry.vue'
1010
import { openDockContextMenu } from './DockContextMenu'
1111
import DockGroupSidebar from './DockGroupSidebar.vue'
@@ -30,7 +30,7 @@ const mousePosition = reactive({ x: 0, y: 0 })
3030
3131
const dockPanel = useTemplateRef<HTMLDivElement>('dockPanel')
3232
const viewsContainer = useTemplateRef<HTMLElement>('viewsContainer')
33-
const persistedDoms = markRaw(new PersistedDomViewsManager(viewsContainer))
33+
const panes = useIframePanes(viewsContainer, context.panel)
3434
3535
function clamp(value: number, min: number, max: number) {
3636
return Math.min(Math.max(value, min), max)
@@ -198,11 +198,11 @@ onMounted(() => {
198198
/>
199199
<div class="relative flex-1 min-w-0 h-full">
200200
<ViewEntry
201-
v-if="selected && viewsContainer"
201+
v-if="selected && panes"
202202
:key="selected.id"
203203
:context
204204
:entry="selected"
205-
:persisted-doms="persistedDoms"
205+
:panes="panes"
206206
:iframe-style="{
207207
border: 'none',
208208
borderRadius: '0.5rem',

‎packages/core/src/client/webcomponents/components/dock/DockStandalone.vue‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
<script setup lang="ts">
22
import type { DocksContext } from '@vitejs/devtools-kit/client'
3-
import { computed, markRaw, ref, useTemplateRef, watch } from 'vue'
3+
import { computed, ref, useTemplateRef, watch } from 'vue'
44
import { getEntryGroup } from '../../state/dock-settings'
5-
import { PersistedDomViewsManager } from '../../utils/PersistedDomViewsManager'
5+
import { useIframePanes } from '../../utils/useIframePanes'
66
import CommandPalette from '../command-palette/CommandPalette.vue'
77
import Confirm from '../display/Confirm.vue'
88
import ToastOverlay from '../display/ToastOverlay.vue'
@@ -19,7 +19,7 @@ const props = defineProps<{
1919
2020
const context = props.context
2121
const viewsContainer = useTemplateRef<HTMLElement>('viewsContainer')
22-
const persistedDoms = markRaw(new PersistedDomViewsManager(viewsContainer))
22+
const panes = useIframePanes(viewsContainer, context.panel)
2323
2424
const isRpcTrusted = ref(context.rpc.isTrusted)
2525
context.rpc.events.on('rpc:is-trusted:updated', (isTrusted) => {
@@ -80,11 +80,11 @@ function switchEntry(id: string | undefined) {
8080
<div class="relative flex-1 min-w-0 min-h-0">
8181
<div id="vite-devtools-views-container" ref="viewsContainer" class="pointer-events-auto" />
8282
<ViewEntry
83-
v-if="context.docks.selected && viewsContainer"
83+
v-if="context.docks.selected && panes"
8484
:key="context.docks.selected.id"
8585
:entry="context.docks.selected"
8686
:context
87-
:persisted-doms="persistedDoms"
87+
:panes="panes"
8888
/>
8989
</div>
9090
</div>

‎packages/core/src/client/webcomponents/components/views/ViewCustomRenderer.vue‎

Lines changed: 11 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,35 @@
11
<script setup lang="ts">
22
import type { DevToolsViewCustomRender } from '@vitejs/devtools-kit'
33
import type { DocksContext } from '@vitejs/devtools-kit/client'
4+
import type { IframePanes } from 'iframe-pane'
45
import type { CSSProperties } from 'vue'
5-
import type { PersistedDomViewsManager } from '../../utils/PersistedDomViewsManager'
6-
import { nextTick, onMounted, onUnmounted, ref, useTemplateRef, watch, watchEffect } from 'vue'
6+
import { onMounted, onUnmounted, useTemplateRef } from 'vue'
77
88
const props = defineProps<{
99
context: DocksContext
1010
entry: DevToolsViewCustomRender
11-
persistedDoms: PersistedDomViewsManager
11+
panes: IframePanes
1212
divStyle?: CSSProperties
1313
}>()
1414
15-
const isLoading = ref(true)
1615
const viewFrame = useTemplateRef<HTMLDivElement>('viewFrame')
1716
1817
onMounted(() => {
19-
const holder = props.persistedDoms.getOrCreateHolder(props.entry.id, 'div')
20-
holder.element.style.boxShadow = 'none'
21-
holder.element.style.outline = 'none'
22-
Object.assign(holder.element.style, props.divStyle)
18+
const pane = props.panes.ensure(props.entry.id, { tagName: 'div' })
19+
const el = pane.element
20+
el.style.boxShadow = 'none'
21+
el.style.outline = 'none'
22+
Object.assign(el.style, props.divStyle)
2323
2424
const entryState = props.context.docks.getStateById(props.entry.id)
2525
if (entryState)
26-
entryState.domElements.panel = holder.element
26+
entryState.domElements.panel = el
2727
28-
holder.mount(viewFrame.value!)
29-
isLoading.value = false
30-
nextTick(() => {
31-
holder.update()
32-
})
33-
34-
watch(
35-
() => props.context.panel,
36-
() => {
37-
holder.update()
38-
},
39-
{ deep: true },
40-
)
41-
42-
watchEffect(
43-
() => {
44-
holder.element.style.pointerEvents = (props.context.panel.isDragging || props.context.panel.isResizing) ? 'none' : 'auto'
45-
},
46-
{ flush: 'sync' },
47-
)
28+
pane.mount(viewFrame.value!)
4829
})
4930
5031
onUnmounted(() => {
51-
const holder = props.persistedDoms.getHolder(props.entry.id, 'div')
52-
holder?.unmount()
32+
props.panes.get(props.entry.id)?.unmount()
5333
})
5434
</script>
5535

‎packages/core/src/client/webcomponents/components/views/ViewEntry.vue‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
<script setup lang="ts">
22
import type { DevToolsDockEntry } from '@vitejs/devtools-kit'
33
import type { DocksContext } from '@vitejs/devtools-kit/client'
4+
import type { IframePanes } from 'iframe-pane'
45
import type { CSSProperties } from 'vue'
5-
import type { PersistedDomViewsManager } from '../../utils/PersistedDomViewsManager'
66
import { defineAsyncComponent } from 'vue'
77
import ViewBuiltinClientAuthNotice from '../views-builtin/ViewBuiltinClientAuthNotice.vue'
88
import ViewBuiltinSettings from '../views-builtin/ViewBuiltinSettings.vue'
@@ -13,7 +13,7 @@ import ViewLauncher from './ViewLauncher.vue'
1313
defineProps<{
1414
context: DocksContext
1515
entry: DevToolsDockEntry
16-
persistedDoms: PersistedDomViewsManager
16+
panes: IframePanes
1717
iframeStyle?: CSSProperties
1818
divStyle?: CSSProperties
1919
}>()
@@ -59,14 +59,14 @@ const ViewJsonRender = defineAsyncComponent(() => import('./ViewJsonRender.vue')
5959
v-else-if="entry.type === 'iframe'"
6060
:context
6161
:entry
62-
:persisted-doms="persistedDoms"
62+
:panes="panes"
6363
:iframe-style="iframeStyle"
6464
/>
6565
<ViewCustomRenderer
6666
v-else-if="entry.type === 'custom-render'"
6767
:context
6868
:entry
69-
:persisted-doms="persistedDoms"
69+
:panes="panes"
7070
:div-style="divStyle"
7171
/>
7272
<ViewLauncher

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

Lines changed: 37 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
<script setup lang="ts">
22
import type { DevToolsViewIframe } from '@vitejs/devtools-kit'
33
import type { DocksContext } from '@vitejs/devtools-kit/client'
4+
import type { IframePanes } from 'iframe-pane'
45
import type { CSSProperties } from 'vue'
5-
import type { PersistedDomViewsManager } from '../../utils/PersistedDomViewsManager'
66
import { REMOTE_CONNECTION_KEY } from '@vitejs/devtools-kit/constants'
7-
import { computed, nextTick, onMounted, onUnmounted, ref, useTemplateRef, watch, watchEffect } from 'vue'
7+
import { computed, nextTick, onMounted, onUnmounted, ref, useTemplateRef, watchEffect } from 'vue'
88
import { sharedStateToRef } from '../../state/docks'
99
1010
const props = defineProps<{
1111
context: DocksContext
1212
entry: DevToolsViewIframe
13-
persistedDoms: PersistedDomViewsManager
13+
panes: IframePanes
1414
iframeStyle?: CSSProperties
1515
}>()
1616
@@ -58,7 +58,7 @@ const editingUrl = ref(props.entry.url)
5858
const isEditing = ref(false)
5959
6060
const iframeElement = computed(() => {
61-
return props.persistedDoms.getHolder(props.entry.id, 'iframe')?.element
61+
return props.panes.get(props.entry.id)?.iframe
6262
})
6363
6464
// Get current page's origin for comparison
@@ -183,72 +183,63 @@ function refresh() {
183183
iframe.src = src
184184
}
185185
186+
let onIframeLoad: (() => void) | undefined
187+
186188
onMounted(() => {
187-
if (props.persistedDoms.getHolder(props.entry.id, 'iframe')) {
188-
updateCurrentUrl()
189-
}
190-
const holder = props.persistedDoms.getOrCreateHolder(props.entry.id, 'iframe')
191-
holder.element.style.boxShadow = 'none'
192-
holder.element.style.outline = 'none'
189+
const existed = props.panes.has(props.entry.id)
190+
// `src` is only assigned when the pane is first created, so re-mounting an
191+
// existing iframe (tab switch) preserves its navigation/scroll/JS state.
192+
const pane = props.panes.ensure(props.entry.id, {
193+
src: props.entry.url,
194+
style: { boxShadow: 'none', outline: 'none' },
195+
})
196+
const iframe = pane.iframe
193197
194-
if (!holder.element.src)
195-
holder.element.src = props.entry.url
198+
if (existed)
199+
updateCurrentUrl()
196200
197201
// Listen for iframe load events
198-
holder.element.addEventListener('load', () => {
202+
onIframeLoad = () => {
199203
isIframeLoading.value = false
200204
updateCurrentUrl()
201-
})
205+
}
206+
iframe.addEventListener('load', onIframeLoad)
202207
203208
const entryState = props.context.docks.getStateById(props.entry.id)
204209
if (entryState)
205-
entryState.domElements.iframe = holder.element
210+
entryState.domElements.iframe = iframe
206211
212+
// iframe-pane positions the iframe exactly over the mount target (the view
213+
// frame below the address bar), so no manual offset is needed — only the
214+
// cosmetic borders differ between edge/float and address-bar states.
207215
watchEffect(() => {
208-
Object.assign(holder.element.style, props.iframeStyle)
209-
if (showAddressBar.value) {
210-
holder.element.style.marginTop = `${ADDRESS_BAR_HEIGHT}px`
211-
if (!isEdgeMode.value) {
212-
holder.element.style.borderTopLeftRadius = '0px'
213-
holder.element.style.borderTopRightRadius = '0px'
214-
}
216+
Object.assign(iframe.style, props.iframeStyle)
217+
if (showAddressBar.value && !isEdgeMode.value) {
218+
iframe.style.borderTopLeftRadius = '0px'
219+
iframe.style.borderTopRightRadius = '0px'
215220
}
216221
else {
217-
holder.element.style.marginTop = '0px'
218-
holder.element.style.borderTopLeftRadius = ''
219-
holder.element.style.borderTopRightRadius = ''
222+
iframe.style.borderTopLeftRadius = ''
223+
iframe.style.borderTopRightRadius = ''
220224
}
221225
if (isEdgeMode.value) {
222-
holder.element.style.borderRadius = '0px'
223-
holder.element.style.border = 'none'
226+
iframe.style.borderRadius = '0px'
227+
iframe.style.border = 'none'
224228
}
225229
})
226230
227-
watch(
228-
() => props.context.panel,
229-
() => {
230-
holder.update()
231-
},
232-
{ deep: true },
233-
)
234-
235-
watchEffect(
236-
() => {
237-
holder.element.style.pointerEvents = (props.context.panel.isDragging || props.context.panel.isResizing) ? 'none' : 'auto'
238-
},
239-
{ flush: 'sync' },
240-
)
241-
242-
holder.mount(viewFrame.value!)
231+
pane.mount(viewFrame.value!)
243232
isLoading.value = false
244233
nextTick(() => {
245-
holder.update()
234+
pane.update()
246235
})
247236
})
248237
249238
onUnmounted(() => {
250-
const holder = props.persistedDoms.getHolder(props.entry.id, 'iframe')
251-
holder?.unmount()
239+
const pane = props.panes.get(props.entry.id)
240+
if (pane && onIframeLoad)
241+
pane.iframe?.removeEventListener('load', onIframeLoad)
242+
pane?.unmount()
252243
})
253244
</script>
254245

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
11
export * from './components/DockEmbedded'
22
export * from './components/DockStandalone'
33
export * from './state/docks'
4-
export * from './utils/PersistedDomViewsManager'

0 commit comments

Comments
 (0)