From bba22fedd49259ec852079f9e36bc659c624eb0c Mon Sep 17 00:00:00 2001 From: mathuo <6710312+mathuo@users.noreply.github.com> Date: Thu, 13 Mar 2025 21:04:40 +0000 Subject: [PATCH] feat: popout group resize events --- .../src/__tests__/events.spec.ts | 9 +-- .../dockview-core/src/api/component.api.ts | 11 ++- .../src/dockview/components/popupService.ts | 4 +- .../src/dockview/dockviewComponent.ts | 69 +++++++++++++--- packages/dockview-core/src/dom.ts | 80 ++++++++++++++----- packages/dockview-core/src/events.ts | 32 +++++--- packages/dockview-core/src/overlay/overlay.ts | 9 +-- packages/dockview-core/src/popoutWindow.ts | 6 +- 8 files changed, 161 insertions(+), 59 deletions(-) diff --git a/packages/dockview-core/src/__tests__/events.spec.ts b/packages/dockview-core/src/__tests__/events.spec.ts index 461b1518e..d4b4fb00b 100644 --- a/packages/dockview-core/src/__tests__/events.spec.ts +++ b/packages/dockview-core/src/__tests__/events.spec.ts @@ -3,7 +3,6 @@ import { Emitter, Event, addDisposableListener, - addDisposableWindowListener, } from '../events'; describe('events', () => { @@ -143,7 +142,7 @@ describe('events', () => { expect(value).toBe(3); }); - it('addDisposableWindowListener with capture options', () => { + it('addDisposableListener with capture options', () => { const element = { addEventListener: jest.fn(), removeEventListener: jest.fn(), @@ -151,7 +150,7 @@ describe('events', () => { const handler = jest.fn(); - const disposable = addDisposableWindowListener( + const disposable = addDisposableListener( element as any, 'pointerdown', handler, @@ -177,7 +176,7 @@ describe('events', () => { ); }); - it('addDisposableWindowListener without capture options', () => { + it('addDisposableListener without capture options', () => { const element = { addEventListener: jest.fn(), removeEventListener: jest.fn(), @@ -185,7 +184,7 @@ describe('events', () => { const handler = jest.fn(); - const disposable = addDisposableWindowListener( + const disposable = addDisposableListener( element as any, 'pointerdown', handler diff --git a/packages/dockview-core/src/api/component.api.ts b/packages/dockview-core/src/api/component.api.ts index 7f3c40299..13d108811 100644 --- a/packages/dockview-core/src/api/component.api.ts +++ b/packages/dockview-core/src/api/component.api.ts @@ -3,6 +3,8 @@ import { FloatingGroupOptions, IDockviewComponent, MovePanelEvent, + PopoutGroupChangePositionEvent, + PopoutGroupChangeSizeEvent, SerializedDockview, } from '../dockview/dockviewComponent'; import { @@ -629,7 +631,6 @@ export class DockviewApi implements CommonApi { return this.component.totalPanels; } - /** * Invoked when the active group changes. May be undefined if no group is active. */ @@ -740,6 +741,14 @@ export class DockviewApi implements CommonApi { return this.component.onUnhandledDragOverEvent; } + get onDidPopoutGroupSizeChange(): Event { + return this.component.onDidPopoutGroupSizeChange; + } + + get onDidPopoutGroupPositionChange(): Event { + return this.component.onDidPopoutGroupPositionChange; + } + /** * All panel objects. */ diff --git a/packages/dockview-core/src/dockview/components/popupService.ts b/packages/dockview-core/src/dockview/components/popupService.ts index 58f0b9853..0e5794041 100644 --- a/packages/dockview-core/src/dockview/components/popupService.ts +++ b/packages/dockview-core/src/dockview/components/popupService.ts @@ -1,4 +1,4 @@ -import { addDisposableWindowListener } from '../../events'; +import { addDisposableListener } from '../../events'; import { CompositeDisposable, Disposable, @@ -50,7 +50,7 @@ export class PopupService extends CompositeDisposable { this._active = wrapper; this._activeDisposable.value = new CompositeDisposable( - addDisposableWindowListener(window, 'pointerdown', (event) => { + addDisposableListener(window, 'pointerdown', (event) => { const target = event.target; if (!(target instanceof HTMLElement)) { diff --git a/packages/dockview-core/src/dockview/dockviewComponent.ts b/packages/dockview-core/src/dockview/dockviewComponent.ts index 13bcb0ec2..783b19803 100644 --- a/packages/dockview-core/src/dockview/dockviewComponent.ts +++ b/packages/dockview-core/src/dockview/dockviewComponent.ts @@ -14,7 +14,7 @@ import { import { tail, sequenceEquals, remove } from '../array'; import { DockviewPanel, IDockviewPanel } from './dockviewPanel'; import { CompositeDisposable, Disposable } from '../lifecycle'; -import { Event, Emitter, addDisposableWindowListener } from '../events'; +import { Event, Emitter, addDisposableListener } from '../events'; import { Watermark } from './components/watermark/watermark'; import { IWatermarkRenderer, GroupviewPanelState } from './types'; import { sequentialNumberGenerator } from '../math'; @@ -56,6 +56,8 @@ import { addTestId, Classnames, getDockviewTheme, + onDidWindowResizeEnd, + onDidWindowMoveEnd, toggleClass, watchElementResize, } from '../dom'; @@ -190,6 +192,18 @@ export interface DockviewMaximizedGroupChanged { isMaximized: boolean; } +export interface PopoutGroupChangeSizeEvent { + width: number; + height: number; + group: DockviewGroupPanel; +} + +export interface PopoutGroupChangePositionEvent { + screenX: number; + screenY: number; + group: DockviewGroupPanel; +} + export interface IDockviewComponent extends IBaseGrid { readonly activePanel: IDockviewPanel | undefined; readonly totalPanels: number; @@ -210,6 +224,8 @@ export interface IDockviewComponent extends IBaseGrid { readonly onUnhandledDragOverEvent: Event; readonly onDidMovePanel: Event; readonly onDidMaximizedGroupChange: Event; + readonly onDidPopoutGroupSizeChange: Event; + readonly onDidPopoutGroupPositionChange: Event; readonly options: DockviewComponentOptions; updateOptions(options: DockviewOptions): void; moveGroupOrPanel(options: MoveGroupOrPanelOptions): void; @@ -293,6 +309,16 @@ export class DockviewComponent private readonly _onDidAddPanel = new Emitter(); readonly onDidAddPanel: Event = this._onDidAddPanel.event; + private readonly _onDidPopoutGroupSizeChange = + new Emitter(); + readonly onDidPopoutGroupSizeChange: Event = + this._onDidPopoutGroupSizeChange.event; + + private readonly _onDidPopoutGroupPositionChange = + new Emitter(); + readonly onDidPopoutGroupPositionChange: Event = + this._onDidPopoutGroupPositionChange.event; + private readonly _onDidLayoutFromJSON = new Emitter(); readonly onDidLayoutFromJSON: Event = this._onDidLayoutFromJSON.event; @@ -427,6 +453,8 @@ export class DockviewComponent this._onUnhandledDragOverEvent, this._onDidMaximizedGroupChange, this._onDidOptionsChange, + this._onDidPopoutGroupSizeChange, + this._onDidPopoutGroupPositionChange, this.onDidViewVisibilityChangeMicroTaskQueue(() => { this.updateWatermark(); }), @@ -463,7 +491,9 @@ export class DockviewComponent this.onDidAddGroup, this.onDidRemove, this.onDidMovePanel, - this.onDidActivePanelChange + this.onDidActivePanelChange, + this.onDidPopoutGroupPositionChange, + this.onDidPopoutGroupSizeChange )(() => { this._bufferOnDidLayoutChange.fire(); }), @@ -832,22 +862,37 @@ export class DockviewComponent }, }; + const _onDidWindowPositionChange = onDidWindowMoveEnd( + _window.window! + ); + popoutWindowDisposable.addDisposables( + _onDidWindowPositionChange, + onDidWindowResizeEnd(_window.window!, () => { + this._onDidPopoutGroupSizeChange.fire({ + width: _window.window!.innerWidth, + height: _window.window!.innerHeight, + group, + }); + }), + _onDidWindowPositionChange.event(() => { + this._onDidPopoutGroupPositionChange.fire({ + screenX: _window.window!.screenX, + screenY: _window.window!.screenX, + group, + }); + }), /** * ResizeObserver seems slow here, I do not know why but we don't need it * since we can reply on the window resize event as we will occupy the full * window dimensions */ - addDisposableWindowListener( - _window.window!, - 'resize', - () => { - group.layout( - _window.window!.innerWidth, - _window.window!.innerHeight - ); - } - ), + addDisposableListener(_window.window!, 'resize', () => { + group.layout( + _window.window!.innerWidth, + _window.window!.innerHeight + ); + }), overlayRenderContainer, Disposable.from(() => { if (this.isDisposed) { diff --git a/packages/dockview-core/src/dom.ts b/packages/dockview-core/src/dom.ts index 3dc9000a9..c83b5b2e4 100644 --- a/packages/dockview-core/src/dom.ts +++ b/packages/dockview-core/src/dom.ts @@ -2,7 +2,6 @@ import { Event as DockviewEvent, Emitter, addDisposableListener, - addDisposableWindowListener, } from './events'; import { IDisposable, CompositeDisposable } from './lifecycle'; @@ -122,7 +121,7 @@ export interface IFocusTracker extends IDisposable { refreshState?(): void; } -export function trackFocus(element: HTMLElement | Window): IFocusTracker { +export function trackFocus(element: HTMLElement): IFocusTracker { return new FocusTracker(element); } @@ -138,7 +137,7 @@ class FocusTracker extends CompositeDisposable implements IFocusTracker { private readonly _refreshStateHandler: () => void; - constructor(element: HTMLElement | Window) { + constructor(element: HTMLElement) { super(); this.addDisposables(this._onDidFocus, this._onDidBlur); @@ -181,21 +180,12 @@ class FocusTracker extends CompositeDisposable implements IFocusTracker { } }; - if (element instanceof HTMLElement) { - this.addDisposables( - addDisposableListener(element, 'focus', onFocus, true) - ); - this.addDisposables( - addDisposableListener(element, 'blur', onBlur, true) - ); - } else { - this.addDisposables( - addDisposableWindowListener(element, 'focus', onFocus, true) - ); - this.addDisposables( - addDisposableWindowListener(element, 'blur', onBlur, true) - ); - } + this.addDisposables( + addDisposableListener(element, 'focus', onFocus, true) + ); + this.addDisposables( + addDisposableListener(element, 'blur', onBlur, true) + ); } refreshState(): void { @@ -358,6 +348,8 @@ export class Classnames { } } +const DEBOUCE_DELAY = 100; + export function isChildEntirelyVisibleWithinParent( child: HTMLElement, parent: HTMLElement @@ -379,3 +371,55 @@ export function isChildEntirelyVisibleWithinParent( return true; } + +export function onDidWindowMoveEnd(window: Window): Emitter { + const emitter = new Emitter(); + + let previousScreenX = window.screenX; + let previousScreenY = window.screenY; + + let timeout: any; + + const checkMovement = () => { + if (window.closed) { + return; + } + + const currentScreenX = window.screenX; + const currentScreenY = window.screenY; + + if ( + currentScreenX !== previousScreenX || + currentScreenY !== previousScreenY + ) { + clearTimeout(timeout); + timeout = setTimeout(() => { + emitter.fire(); + }, DEBOUCE_DELAY); + + previousScreenX = currentScreenX; + previousScreenY = currentScreenY; + } + + requestAnimationFrame(checkMovement); + }; + + checkMovement(); + + return emitter; +} + +export function onDidWindowResizeEnd(element: Window, cb: () => void) { + let resizeTimeout: any; + + const disposable = new CompositeDisposable( + addDisposableListener(element, 'resize', () => { + clearTimeout(resizeTimeout); + resizeTimeout = setTimeout(() => { + cb(); + }, DEBOUCE_DELAY); + }) + ); + + return disposable; +} diff --git a/packages/dockview-core/src/events.ts b/packages/dockview-core/src/events.ts index d9cbf32d6..b7e2821c6 100644 --- a/packages/dockview-core/src/events.ts +++ b/packages/dockview-core/src/events.ts @@ -193,32 +193,38 @@ export class Emitter implements IDisposable { } } -export function addDisposableWindowListener( +export function addDisposableListener( element: Window, type: K, listener: (this: Window, ev: WindowEventMap[K]) => any, options?: boolean | AddEventListenerOptions -): IDisposable { - element.addEventListener(type, listener, options); - - return { - dispose: () => { - element.removeEventListener(type, listener, options); - }, - }; -} - +): IDisposable; export function addDisposableListener( element: HTMLElement, type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions +): IDisposable; +export function addDisposableListener< + K extends keyof HTMLElementEventMap | keyof WindowEventMap +>( + element: HTMLElement | Window, + type: K, + listener: ( + this: K extends keyof HTMLElementEventMap ? HTMLElement : Window, + ev: K extends keyof HTMLElementEventMap + ? HTMLElementEventMap[K] + : K extends keyof WindowEventMap + ? WindowEventMap[K] + : never + ) => any, + options?: boolean | AddEventListenerOptions ): IDisposable { - element.addEventListener(type, listener, options); + element.addEventListener(type, listener, options); return { dispose: () => { - element.removeEventListener(type, listener, options); + element.removeEventListener(type, listener, options); }, }; } diff --git a/packages/dockview-core/src/overlay/overlay.ts b/packages/dockview-core/src/overlay/overlay.ts index 36fd00d96..03ecc2f17 100644 --- a/packages/dockview-core/src/overlay/overlay.ts +++ b/packages/dockview-core/src/overlay/overlay.ts @@ -7,7 +7,6 @@ import { Emitter, Event, addDisposableListener, - addDisposableWindowListener, } from '../events'; import { CompositeDisposable, MutableDisposable } from '../lifecycle'; import { clamp } from '../math'; @@ -258,7 +257,7 @@ export class Overlay extends CompositeDisposable { iframes.release(); }, }, - addDisposableWindowListener(window, 'pointermove', (e) => { + addDisposableListener(window, 'pointermove', (e) => { const containerRect = this.options.container.getBoundingClientRect(); const x = e.clientX - containerRect.left; @@ -344,7 +343,7 @@ export class Overlay extends CompositeDisposable { this.setBounds(bounds); }), - addDisposableWindowListener(window, 'pointerup', () => { + addDisposableListener(window, 'pointerup', () => { toggleClass( this._element, 'dv-resize-container-dragging', @@ -439,7 +438,7 @@ export class Overlay extends CompositeDisposable { const iframes = disableIframePointEvents(); move.value = new CompositeDisposable( - addDisposableWindowListener(window, 'pointermove', (e) => { + addDisposableListener(window, 'pointermove', (e) => { const containerRect = this.options.container.getBoundingClientRect(); const overlayRect = @@ -610,7 +609,7 @@ export class Overlay extends CompositeDisposable { iframes.release(); }, }, - addDisposableWindowListener(window, 'pointerup', () => { + addDisposableListener(window, 'pointerup', () => { move.dispose(); this._onDidChangeEnd.fire(); }) diff --git a/packages/dockview-core/src/popoutWindow.ts b/packages/dockview-core/src/popoutWindow.ts index e4bb3a093..870dec4aa 100644 --- a/packages/dockview-core/src/popoutWindow.ts +++ b/packages/dockview-core/src/popoutWindow.ts @@ -1,5 +1,5 @@ import { addStyles } from './dom'; -import { Emitter, addDisposableWindowListener } from './events'; +import { Emitter, addDisposableListener } from './events'; import { CompositeDisposable, Disposable, IDisposable } from './lifecycle'; import { Box } from './types'; @@ -101,7 +101,7 @@ export class PopoutWindow extends CompositeDisposable { Disposable.from(() => { externalWindow.close(); }), - addDisposableWindowListener(window, 'beforeunload', () => { + addDisposableListener(window, 'beforeunload', () => { /** * before the main window closes we should close this popup too * to be good citizens @@ -146,7 +146,7 @@ export class PopoutWindow extends CompositeDisposable { * beforeunload must be registered after load for reasons I could not determine * otherwise the beforeunload event will not fire when the window is closed */ - addDisposableWindowListener( + addDisposableListener( externalWindow, 'beforeunload', () => {