diff --git a/packages/core/package.json b/packages/core/package.json index 9eb91442..82712a10 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -79,6 +79,7 @@ "dompurify": "catalog:frontend", "fuse.js": "catalog:frontend", "human-id": "catalog:inlined", + "iframe-pane": "catalog:frontend", "tsdown": "catalog:build", "typescript": "catalog:devtools", "unplugin-vue": "catalog:build", @@ -103,6 +104,7 @@ "fast-wrap-ansi": "0.2.0", "fuse.js": "7.3.0", "get-port-please": "3.2.0", + "iframe-pane": "1.1.0", "sisteransi": "1.0.5", "zod": "4.3.6" } diff --git a/packages/core/src/client/webcomponents/components/dock/DockEdge.vue b/packages/core/src/client/webcomponents/components/dock/DockEdge.vue index 88bcdbbd..7ffacb80 100644 --- a/packages/core/src/client/webcomponents/components/dock/DockEdge.vue +++ b/packages/core/src/client/webcomponents/components/dock/DockEdge.vue @@ -1,10 +1,10 @@ diff --git a/packages/core/src/client/webcomponents/components/views/ViewEntry.vue b/packages/core/src/client/webcomponents/components/views/ViewEntry.vue index a85d6e5b..f732ba4e 100644 --- a/packages/core/src/client/webcomponents/components/views/ViewEntry.vue +++ b/packages/core/src/client/webcomponents/components/views/ViewEntry.vue @@ -1,8 +1,8 @@ diff --git a/packages/core/src/client/webcomponents/index.ts b/packages/core/src/client/webcomponents/index.ts index e9e765d0..da4da964 100644 --- a/packages/core/src/client/webcomponents/index.ts +++ b/packages/core/src/client/webcomponents/index.ts @@ -1,4 +1,3 @@ export * from './components/DockEmbedded' export * from './components/DockStandalone' export * from './state/docks' -export * from './utils/PersistedDomViewsManager' diff --git a/packages/core/src/client/webcomponents/utils/PersistedDomViewsManager.ts b/packages/core/src/client/webcomponents/utils/PersistedDomViewsManager.ts deleted file mode 100644 index 7e5b4154..00000000 --- a/packages/core/src/client/webcomponents/utils/PersistedDomViewsManager.ts +++ /dev/null @@ -1,103 +0,0 @@ -import type { ShallowRef } from 'vue' - -export interface TagNameToElementMap { - iframe: HTMLIFrameElement - div: HTMLDivElement -} - -export class PersistedDomViewsManager { - readonly holders: Record> = {} - - constructor( - public container: Readonly>, - ) { - } - - getHolder(id: string, _type: T): PersistedDomHolder | undefined { - return this.holders[id] as PersistedDomHolder - } - - getOrCreateHolder(id: string, type: T): PersistedDomHolder { - if (!this.container.value) { - throw new Error('[VITE DEVTOOLS] PersistedDomViewsManager: container is not set') - } - let holder: PersistedDomHolder - if (!this.holders[id]) { - const el = document.createElement(type) as TagNameToElementMap[T] - this.holders[id] = new PersistedDomHolder(id, el) - this.container.value.appendChild(el) - holder = this.holders[id] - } - else { - holder = this.holders[id] - } - return holder as PersistedDomHolder - } - - removeHolder(id: string) { - const holder = this.holders[id] - if (!holder) - return false - holder.unmount() - holder.element.remove() - delete this.holders[id] - return true - } -} - -export class PersistedDomHolder { - readonly element: ElementType - readonly id: string - anchor?: Element - - _cleanups: (() => void)[] = [] - - constructor(id: string, iframe: ElementType) { - this.id = id - this.element = iframe - } - - cleanup() { - this._cleanups.forEach(cleanup => cleanup()) - this._cleanups = [] - } - - mount(parent: Element) { - if (this.anchor === parent) { - this.show() - return - } - - this.cleanup() - this.anchor = parent - - const update = () => this.update() - window.addEventListener('resize', update) - this._cleanups.push(() => window.removeEventListener('resize', update)) - this.show() - } - - hide() { - this.element.style.display = 'none' - } - - show() { - this.element.style.display = 'block' - this.update() - } - - update() { - if (!this.anchor) - return - const rect = this.anchor.getBoundingClientRect() - this.element.style.position = 'absolute' - this.element.style.width = `${rect.width}px` - this.element.style.height = `${rect.height}px` - } - - unmount() { - this.cleanup() - this.hide() - this.anchor = undefined - } -} diff --git a/packages/core/src/client/webcomponents/utils/useIframePanes.ts b/packages/core/src/client/webcomponents/utils/useIframePanes.ts new file mode 100644 index 00000000..5e861cce --- /dev/null +++ b/packages/core/src/client/webcomponents/utils/useIframePanes.ts @@ -0,0 +1,62 @@ +import type { DocksPanelContext } from '@vitejs/devtools-kit/client' +import type { IframePanes } from 'iframe-pane' +import type { ShallowRef } from 'vue' +import { createIframePanes } from 'iframe-pane' +import { markRaw, onScopeDispose, shallowRef, watch } from 'vue' + +/** + * Own an {@link IframePanes} manager for a dock shell, parking its persistent + * iframe/element panes in the given overlay container. + * + * The manager is created lazily once the container element exists (it is a + * template ref that starts empty), and torn down when the owning scope is + * disposed. When a `panel` context is supplied, its geometry is watched so + * panes stay glued to their targets during moves the shared `ResizeObserver` + * can't see, and pointer events on every pane are locked while the panel is + * dragged or resized so iframes don't swallow the interaction. + */ +export function useIframePanes( + container: Readonly>, + panel?: DocksPanelContext, +): Readonly> { + const panes = shallowRef() + + const stop = watch( + container, + (el) => { + if (el && !panes.value) { + panes.value = markRaw(createIframePanes({ container: el })) + stop() + } + }, + { immediate: true, flush: 'post' }, + ) + + if (panel) { + // A panel move changes its box position without resizing the target, so the + // shared ResizeObserver/scroll listeners never fire — re-sync explicitly. + watch(() => panel, () => panes.value?.updateAll(), { deep: true }) + + // Iframes capture pointer events, which breaks dragging/resizing happening + // above them — lock them for the duration of the interaction. + let release: (() => void) | undefined + watch( + () => panel.isDragging || panel.isResizing, + (locked) => { + if (locked) { + release ??= panes.value?.lockPointerEvents() + } + else { + release?.() + release = undefined + } + }, + { flush: 'sync' }, + ) + onScopeDispose(() => release?.()) + } + + onScopeDispose(() => panes.value?.dispose()) + + return panes +} diff --git a/packages/core/tsdown.config.ts b/packages/core/tsdown.config.ts index 833fe36c..32a81cb4 100644 --- a/packages/core/tsdown.config.ts +++ b/packages/core/tsdown.config.ts @@ -39,6 +39,7 @@ const deps = { 'fuse.js', 'get-port-please', 'human-id', + 'iframe-pane', 'sisteransi', 'vue', 'zod', diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 318b0772..82f4928a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -229,6 +229,9 @@ catalogs: idb-keyval: specifier: ^6.2.6 version: 6.2.6 + iframe-pane: + specifier: ^1.1.0 + version: 1.1.0 modern-monaco: specifier: ^0.4.2 version: 0.4.2 @@ -773,6 +776,9 @@ importers: human-id: specifier: catalog:inlined version: 4.2.0 + iframe-pane: + specifier: catalog:frontend + version: 1.1.0 tsdown: specifier: catalog:build version: 0.22.3(@vitejs/devtools@0.3.4)(publint@0.3.21)(tsx@4.22.4)(typescript@6.0.3)(unrun@0.3.1(synckit@0.11.13))(vue-tsc@3.3.6(typescript@6.0.3)) @@ -5757,6 +5763,9 @@ packages: ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + iframe-pane@1.1.0: + resolution: {integrity: sha512-UV+PuLDiXZ05vMSNdssR/MRb6qIL4YM7Fxbx1OjZdvW2cfKyukKTx9Xn0YgfjlrFocOuFhk+m+ZPH3ZknmIE/w==} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -13728,6 +13737,8 @@ snapshots: ieee754@1.2.1: {} + iframe-pane@1.1.0: {} + ignore@5.3.2: {} ignore@7.0.5: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 94e66e0e..ce40dd2c 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -126,6 +126,7 @@ catalogs: floating-vue: ^5.2.2 fuse.js: ^7.4.2 idb-keyval: ^6.2.6 + iframe-pane: ^1.1.0 modern-monaco: ^0.4.2 nanovis: ^1.0.0 react: ^19.2.7 diff --git a/test/__snapshots__/tsnapi/@vitejs/devtools/client/webcomponents.snapshot.d.ts b/test/__snapshots__/tsnapi/@vitejs/devtools/client/webcomponents.snapshot.d.ts index c760938f..8e82716b 100644 --- a/test/__snapshots__/tsnapi/@vitejs/devtools/client/webcomponents.snapshot.d.ts +++ b/test/__snapshots__/tsnapi/@vitejs/devtools/client/webcomponents.snapshot.d.ts @@ -1,37 +1,6 @@ /** * Generated by tsnapi — public API snapshot of `@vitejs/devtools/client/webcomponents` */ -// #region Interfaces -export interface TagNameToElementMap { - iframe: HTMLIFrameElement; - div: HTMLDivElement; -} -// #endregion - -// #region Classes -export declare class PersistedDomHolder { - readonly element: ElementType; - readonly id: string; - anchor?: Element; - _cleanups: (() => void)[]; - constructor(_: string, _: ElementType); - cleanup(): void; - mount(_: Element): void; - hide(): void; - show(): void; - update(): void; - unmount(): void; -} -export declare class PersistedDomViewsManager { - container: Readonly>; - readonly holders: Record>; - constructor(_: Readonly>); - getHolder(_: string, _: T): PersistedDomHolder | undefined; - getOrCreateHolder(_: string, _: T): PersistedDomHolder; - removeHolder(_: string): boolean; -} -// #endregion - // #region Functions export declare function createDockEntryState(_: DevToolsDockEntry, _: Ref): DockEntryState; export declare function DEFAULT_DOCK_PANEL_STORE(): DockPanelStorage; diff --git a/test/__snapshots__/tsnapi/@vitejs/devtools/client/webcomponents.snapshot.js b/test/__snapshots__/tsnapi/@vitejs/devtools/client/webcomponents.snapshot.js index 09085266..f07f79d5 100644 --- a/test/__snapshots__/tsnapi/@vitejs/devtools/client/webcomponents.snapshot.js +++ b/test/__snapshots__/tsnapi/@vitejs/devtools/client/webcomponents.snapshot.js @@ -9,8 +9,6 @@ export var DockEmbedded /* const */ export { createDockEntryState } export { DEFAULT_DOCK_PANEL_STORE } export { DockStandalone } -export { PersistedDomHolder } -export { PersistedDomViewsManager } export { sharedStateToRef } export { useDocksEntries } // #endregion \ No newline at end of file