From 512a8d2c72d347191c618891ecb9c62892560999 Mon Sep 17 00:00:00 2001 From: mathuo <6710312+mathuo@users.noreply.github.com> Date: Wed, 17 Jan 2024 22:34:10 +0000 Subject: [PATCH 01/19] feat: dnd control changes --- .../dockview/dockviewComponent.spec.ts | 11 ++--- .../src/dockview/components/panel/content.ts | 7 +-- .../src/dockview/components/tab/tab.ts | 4 +- .../components/titlebar/voidContainer.ts | 7 +-- .../src/dockview/dockviewComponent.ts | 8 +--- .../src/dockview/dockviewGroupPanelModel.ts | 47 ++++++++++--------- packages/dockview-core/src/dockview/types.ts | 7 +-- 7 files changed, 37 insertions(+), 54 deletions(-) diff --git a/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts b/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts index 9dd25066d..8fdcd3d2d 100644 --- a/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts +++ b/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts @@ -1,6 +1,5 @@ import { DockviewComponent } from '../../dockview/dockviewComponent'; import { - DockviewDropTargets, GroupPanelPartInitParameters, IContentRenderer, ITabRenderer, @@ -2967,7 +2966,7 @@ describe('dockviewComponent', () => { expect(showDndOverlay).toHaveBeenCalledWith({ nativeEvent: eventLeft, position: 'left', - target: DockviewDropTargets.Edge, + target: 'edge', getData: getPanelData, }); expect(showDndOverlay).toBeCalledTimes(1); @@ -2986,7 +2985,7 @@ describe('dockviewComponent', () => { expect(showDndOverlay).toHaveBeenCalledWith({ nativeEvent: eventRight, position: 'right', - target: DockviewDropTargets.Edge, + target: 'edge', getData: getPanelData, }); expect(showDndOverlay).toBeCalledTimes(2); @@ -3005,7 +3004,7 @@ describe('dockviewComponent', () => { expect(showDndOverlay).toHaveBeenCalledWith({ nativeEvent: eventTop, position: 'top', - target: DockviewDropTargets.Edge, + target: 'edge', getData: getPanelData, }); expect(showDndOverlay).toBeCalledTimes(3); @@ -3024,7 +3023,7 @@ describe('dockviewComponent', () => { expect(showDndOverlay).toHaveBeenCalledWith({ nativeEvent: eventBottom, position: 'bottom', - target: DockviewDropTargets.Edge, + target: 'edge', getData: getPanelData, }); expect(showDndOverlay).toBeCalledTimes(4); @@ -3060,7 +3059,7 @@ describe('dockviewComponent', () => { expect(showDndOverlay).toHaveBeenCalledWith({ nativeEvent: eventTop, position: 'center', - target: DockviewDropTargets.Edge, + target: 'edge', getData: getPanelData, }); expect(showDndOverlay).toBeCalledTimes(5); diff --git a/packages/dockview-core/src/dockview/components/panel/content.ts b/packages/dockview-core/src/dockview/components/panel/content.ts index 91ea210cf..3b1a9534d 100644 --- a/packages/dockview-core/src/dockview/components/panel/content.ts +++ b/packages/dockview-core/src/dockview/components/panel/content.ts @@ -10,7 +10,6 @@ import { DockviewComponent } from '../../dockviewComponent'; import { Droptarget } from '../../../dnd/droptarget'; import { DockviewGroupPanelModel } from '../../dockviewGroupPanelModel'; import { getPanelData } from '../../../dnd/dataTransfer'; -import { DockviewDropTargets } from '../../types'; export interface IContentContainer extends IDisposable { readonly dropTarget: Droptarget; @@ -95,11 +94,7 @@ export class ContentContainer return !groupHasOnePanelAndIsActiveDragElement; } - return this.group.canDisplayOverlay( - event, - position, - DockviewDropTargets.Panel - ); + return this.group.canDisplayOverlay(event, position, 'panel'); }, }); diff --git a/packages/dockview-core/src/dockview/components/tab/tab.ts b/packages/dockview-core/src/dockview/components/tab/tab.ts index 48da83a85..b26c754c7 100644 --- a/packages/dockview-core/src/dockview/components/tab/tab.ts +++ b/packages/dockview-core/src/dockview/components/tab/tab.ts @@ -7,7 +7,7 @@ import { } from '../../../dnd/dataTransfer'; import { toggleClass } from '../../../dom'; import { DockviewComponent } from '../../dockviewComponent'; -import { DockviewDropTargets, ITabRenderer } from '../../types'; +import { ITabRenderer } from '../../types'; import { DockviewGroupPanel } from '../../dockviewGroupPanel'; import { DroptargetEvent, Droptarget } from '../../../dnd/droptarget'; import { DragHandler } from '../../../dnd/abstractDragHandler'; @@ -112,7 +112,7 @@ export class Tab extends CompositeDisposable implements ITab { return this.group.model.canDisplayOverlay( event, position, - DockviewDropTargets.Tab + 'tab' ); }, }); diff --git a/packages/dockview-core/src/dockview/components/titlebar/voidContainer.ts b/packages/dockview-core/src/dockview/components/titlebar/voidContainer.ts index e1d3cd9c7..659a1d3a8 100644 --- a/packages/dockview-core/src/dockview/components/titlebar/voidContainer.ts +++ b/packages/dockview-core/src/dockview/components/titlebar/voidContainer.ts @@ -6,7 +6,6 @@ import { DockviewComponent } from '../../dockviewComponent'; import { addDisposableListener, Emitter, Event } from '../../../events'; import { CompositeDisposable } from '../../../lifecycle'; import { DockviewGroupPanel } from '../../dockviewGroupPanel'; -import { DockviewDropTargets } from '../../types'; export class VoidContainer extends CompositeDisposable { private readonly _element: HTMLElement; @@ -62,11 +61,7 @@ export class VoidContainer extends CompositeDisposable { return last(this.group.panels)?.id !== data.panelId; } - return group.model.canDisplayOverlay( - event, - position, - DockviewDropTargets.Panel - ); + return group.model.canDisplayOverlay(event, position, 'panel'); }, }); diff --git a/packages/dockview-core/src/dockview/dockviewComponent.ts b/packages/dockview-core/src/dockview/dockviewComponent.ts index fd3139105..f36aeaf9d 100644 --- a/packages/dockview-core/src/dockview/dockviewComponent.ts +++ b/packages/dockview-core/src/dockview/dockviewComponent.ts @@ -10,11 +10,7 @@ import { DockviewPanel, IDockviewPanel } from './dockviewPanel'; import { CompositeDisposable, Disposable } from '../lifecycle'; import { Event, Emitter } from '../events'; import { Watermark } from './components/watermark/watermark'; -import { - IWatermarkRenderer, - GroupviewPanelState, - DockviewDropTargets, -} from './types'; +import { IWatermarkRenderer, GroupviewPanelState } from './types'; import { sequentialNumberGenerator } from '../math'; import { DefaultDockviewDeserialzier } from './deserializer'; import { createComponent } from '../panel/componentFactory'; @@ -455,7 +451,7 @@ export class DockviewComponent return this.options.showDndOverlay({ nativeEvent: event, position: position, - target: DockviewDropTargets.Edge, + target: 'edge', getData: getPanelData, }); } diff --git a/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts b/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts index 9d8c9b18c..6837fe618 100644 --- a/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts +++ b/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts @@ -22,26 +22,6 @@ import { DockviewGroupPanel } from './dockviewGroupPanel'; import { IDockviewPanel } from './dockviewPanel'; import { IHeaderActionsRenderer } from './options'; -export interface DndService { - canDisplayOverlay( - group: IDockviewGroupPanelModel, - event: DragEvent, - target: DockviewDropTargets - ): boolean; - onDrop( - group: IDockviewGroupPanelModel, - event: DragEvent, - position: Position, - index?: number - ): void; -} - -export interface IGroupItem { - id: string; - header: { element: HTMLElement }; - body: { element: HTMLElement }; -} - interface GroupMoveEvent { groupId: string; itemId?: string; @@ -321,7 +301,12 @@ export class DockviewGroupPanelModel this._onGroupDragStart.fire(event); }), this.tabsContainer.onDrop((event) => { - this.handleDropEvent(event.event, 'center', event.index); + this.handleDropEvent( + 'header', + event.event, + 'center', + event.index + ); }), this.contentContainer.onDidFocus(() => { this.accessor.doSetGroupActive(this.groupPanel, true); @@ -330,7 +315,11 @@ export class DockviewGroupPanelModel // noop }), this.contentContainer.dropTarget.onDrop((event) => { - this.handleDropEvent(event.nativeEvent, event.position); + this.handleDropEvent( + 'content', + event.nativeEvent, + event.position + ); }), this._onMove, this._onDidChange, @@ -775,6 +764,7 @@ export class DockviewGroupPanelModel } private handleDropEvent( + type: 'header' | 'content', event: DragEvent, position: Position, index?: number @@ -783,6 +773,17 @@ export class DockviewGroupPanelModel return; } + function getKind(): 'tab' | 'header_space' | 'content' { + switch (type) { + case 'header': + return typeof index === 'number' ? 'tab' : 'header_space'; + case 'content': + return 'content'; + } + } + + const kind = getKind(); + const data = getPanelData(); if (data && data.viewId === this.accessor.id) { @@ -790,6 +791,7 @@ export class DockviewGroupPanelModel // this is a group move dnd event const { groupId } = data; + // TODO: intercept this._onMove.fire({ target: position, groupId: groupId, @@ -814,6 +816,7 @@ export class DockviewGroupPanelModel } } + // TODO: intercept this._onMove.fire({ target: position, groupId: data.groupId, diff --git a/packages/dockview-core/src/dockview/types.ts b/packages/dockview-core/src/dockview/types.ts index 28cbcc557..0059a3952 100644 --- a/packages/dockview-core/src/dockview/types.ts +++ b/packages/dockview-core/src/dockview/types.ts @@ -7,12 +7,7 @@ import { Optional } from '../types'; import { DockviewGroupPanel, IDockviewGroupPanel } from './dockviewGroupPanel'; import { DockviewPanelRenderer } from '../overlayRenderContainer'; -export enum DockviewDropTargets { - Tab, - Panel, - TabContainer, - Edge, -} +export type DockviewDropTargets = 'tab' | 'panel' | 'tabContainer' | 'edge'; export interface HeaderPartInitParameters { title: string; From 6a1f47d4daf6bb372037328c82e200180804cad5 Mon Sep 17 00:00:00 2001 From: mathuo <6710312+mathuo@users.noreply.github.com> Date: Thu, 18 Jan 2024 21:57:11 +0000 Subject: [PATCH 02/19] feat: expose onWillDrop --- .../dockview-core/src/api/component.api.ts | 25 ++- packages/dockview-core/src/dnd/droptarget.ts | 68 +++++-- .../src/dockview/components/tab/tab.ts | 30 +-- .../components/titlebar/tabsContainer.ts | 102 +++++----- .../components/titlebar/voidContainer.ts | 18 +- .../src/dockview/dockviewComponent.ts | 77 ++++++-- .../src/dockview/dockviewGroupPanelModel.ts | 181 ++++++++++++++---- .../dockview-core/src/dockview/options.ts | 1 + packages/dockview-core/src/events.ts | 12 ++ packages/dockview/src/dockview/dockview.tsx | 35 +++- 10 files changed, 413 insertions(+), 136 deletions(-) diff --git a/packages/dockview-core/src/api/component.api.ts b/packages/dockview-core/src/api/component.api.ts index d4a8aae02..7f5097f37 100644 --- a/packages/dockview-core/src/api/component.api.ts +++ b/packages/dockview-core/src/api/component.api.ts @@ -1,5 +1,4 @@ import { - DockviewDropEvent, IDockviewComponent, SerializedDockview, } from '../dockview/dockviewComponent'; @@ -43,6 +42,11 @@ import { TabDragEvent, } from '../dockview/components/titlebar/tabsContainer'; import { Box } from '../types'; +import { + DockviewDidDropEvent, + DockviewWillDropEvent, + WillShowOverlayLocationEvent, +} from '../dockview/dockviewGroupPanelModel'; export interface CommonApi { readonly height: number; @@ -648,10 +652,27 @@ export class DockviewApi implements CommonApi { /** * Invoked when a Drag'n'Drop event occurs that the component was unable to handle. Exposed for custom Drag'n'Drop functionality. */ - get onDidDrop(): Event { + get onDidDrop(): Event { return this.component.onDidDrop; } + /** + * Invoked when a Drag'n'Drop event occurs but before dockview handles it giving the user an opportunity to intecept and + * prevent the event from occuring using the standard `preventDefault()` syntax. + * + * Preventing certain events may causes unexpected behaviours, use carefully. + */ + get onWillDrop(): Event { + return this.component.onWillDrop; + } + + /** + * + */ + get onWillShowOverlay(): Event { + return this.component.onWillShowOverlay; + } + /** * Invoked before a group is dragged. Exposed for custom Drag'n'Drop functionality. */ diff --git a/packages/dockview-core/src/dnd/droptarget.ts b/packages/dockview-core/src/dnd/droptarget.ts index f607f2cc7..e7c22cfdc 100644 --- a/packages/dockview-core/src/dnd/droptarget.ts +++ b/packages/dockview-core/src/dnd/droptarget.ts @@ -1,10 +1,37 @@ import { toggleClass } from '../dom'; -import { Emitter, Event } from '../events'; +import { DockviewEvent, Emitter, Event } from '../events'; import { CompositeDisposable } from '../lifecycle'; import { DragAndDropObserver } from './dnd'; import { clamp } from '../math'; import { Direction } from '../gridview/baseComponentGridview'; +export interface DroptargetEvent { + readonly position: Position; + readonly nativeEvent: DragEvent; +} + +export class WillShowOverlayEvent + extends DockviewEvent + implements DroptargetEvent +{ + get nativeEvent(): DragEvent { + return this.options.nativeEvent; + } + + get position(): Position { + return this.options.position; + } + + constructor( + private readonly options: { + nativeEvent: DragEvent; + position: Position; + } + ) { + super(); + } +} + export function directionToPosition(direction: Direction): Position { switch (direction) { case 'above': @@ -39,11 +66,6 @@ export function positionToDirection(position: Position): Direction { } } -export interface DroptargetEvent { - readonly position: Position; - readonly nativeEvent: DragEvent; -} - export type Position = 'top' | 'bottom' | 'left' | 'right' | 'center'; export type CanDisplayOverlay = @@ -70,6 +92,12 @@ const DEFAULT_SIZE: MeasuredValue = { const SMALL_WIDTH_BOUNDARY = 100; const SMALL_HEIGHT_BOUNDARY = 100; +export interface DroptargetOptions { + canDisplayOverlay: CanDisplayOverlay; + acceptedTargetZones: Position[]; + overlayModel?: DroptargetOverlayModel; +} + export class Droptarget extends CompositeDisposable { private targetElement: HTMLElement | undefined; private overlayElement: HTMLElement | undefined; @@ -79,6 +107,10 @@ export class Droptarget extends CompositeDisposable { private readonly _onDrop = new Emitter(); readonly onDrop: Event = this._onDrop.event; + private readonly _onWillShowOverlay = new Emitter(); + readonly onWillShowOverlay: Event = + this._onWillShowOverlay.event; + readonly dnd: DragAndDropObserver; private static USED_EVENT_ID = '__dockview_droptarget_event_is_used__'; @@ -89,11 +121,7 @@ export class Droptarget extends CompositeDisposable { constructor( private readonly element: HTMLElement, - private readonly options: { - canDisplayOverlay: CanDisplayOverlay; - acceptedTargetZones: Position[]; - overlayModel?: DroptargetOverlayModel; - } + private readonly options: DroptargetOptions ) { super(); @@ -142,6 +170,22 @@ export class Droptarget extends CompositeDisposable { return; } + const willShowOverlayEvent = new WillShowOverlayEvent({ + nativeEvent: e, + position: quadrant, + }); + + /** + * Provide an opportunity to prevent the overlay appearing and in turn + * any dnd behaviours + */ + this._onWillShowOverlay.fire(willShowOverlayEvent); + + if (willShowOverlayEvent.defaultPrevented) { + this.removeDropTarget(); + return; + } + if (typeof this.options.canDisplayOverlay === 'boolean') { if (!this.options.canDisplayOverlay) { this.removeDropTarget(); @@ -192,7 +236,7 @@ export class Droptarget extends CompositeDisposable { }, }); - this.addDisposables(this._onDrop, this.dnd); + this.addDisposables(this._onDrop, this._onWillShowOverlay, this.dnd); } setTargetZones(acceptedTargetZones: Position[]): void { diff --git a/packages/dockview-core/src/dockview/components/tab/tab.ts b/packages/dockview-core/src/dockview/components/tab/tab.ts index b26c754c7..946da58b8 100644 --- a/packages/dockview-core/src/dockview/components/tab/tab.ts +++ b/packages/dockview-core/src/dockview/components/tab/tab.ts @@ -9,7 +9,12 @@ import { toggleClass } from '../../../dom'; import { DockviewComponent } from '../../dockviewComponent'; import { ITabRenderer } from '../../types'; import { DockviewGroupPanel } from '../../dockviewGroupPanel'; -import { DroptargetEvent, Droptarget } from '../../../dnd/droptarget'; +import { + DroptargetEvent, + Droptarget, + Position, + WillShowOverlayEvent, +} from '../../../dnd/droptarget'; import { DragHandler } from '../../../dnd/abstractDragHandler'; import { IDockviewPanel } from '../../dockviewPanel'; @@ -40,18 +45,9 @@ class TabDragHandler extends DragHandler { } } -export interface ITab extends IDisposable { - readonly panel: IDockviewPanel; - readonly element: HTMLElement; - setContent: (element: ITabRenderer) => void; - onChanged: Event; - onDrop: Event; - setActive(isActive: boolean): void; -} - -export class Tab extends CompositeDisposable implements ITab { +export class Tab extends CompositeDisposable { private readonly _element: HTMLElement; - private readonly droptarget: Droptarget; + private readonly dropTarget: Droptarget; private content: ITabRenderer | undefined = undefined; private readonly _onChanged = new Emitter(); @@ -63,6 +59,8 @@ export class Tab extends CompositeDisposable implements ITab { private readonly _onDragStart = new Emitter(); readonly onDragStart = this._onDragStart.event; + readonly onWillShowOverlay: Event; + public get element(): HTMLElement { return this._element; } @@ -88,7 +86,7 @@ export class Tab extends CompositeDisposable implements ITab { this.panel ); - this.droptarget = new Droptarget(this._element, { + this.dropTarget = new Droptarget(this._element, { acceptedTargetZones: ['center'], canDisplayOverlay: (event, position) => { if (this.group.locked) { @@ -117,6 +115,8 @@ export class Tab extends CompositeDisposable implements ITab { }, }); + this.onWillShowOverlay = this.dropTarget.onWillShowOverlay; + this.addDisposables( this._onChanged, this._onDropped, @@ -132,10 +132,10 @@ export class Tab extends CompositeDisposable implements ITab { this._onChanged.fire(event); }), - this.droptarget.onDrop((event) => { + this.dropTarget.onDrop((event) => { this._onDropped.fire(event); }), - this.droptarget + this.dropTarget ); } diff --git a/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts b/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts index 123608573..d506bd3c6 100644 --- a/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts +++ b/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts @@ -4,12 +4,14 @@ import { IValueDisposable, } from '../../../lifecycle'; import { addDisposableListener, Emitter, Event } from '../../../events'; -import { ITab, Tab } from '../tab/tab'; -import { DockviewComponent } from '../../dockviewComponent'; +import { Tab } from '../tab/tab'; import { DockviewGroupPanel } from '../../dockviewGroupPanel'; import { VoidContainer } from './voidContainer'; import { toggleClass } from '../../../dom'; import { DockviewPanel, IDockviewPanel } from '../../dockviewPanel'; +import { DockviewComponent } from '../../dockviewComponent'; +import { WillShowOverlayEvent } from '../../../dnd/droptarget'; +import { DockviewGroupDropLocation } from '../../dockviewGroupPanelModel'; export interface TabDropIndexEvent { readonly event: DragEvent; @@ -30,17 +32,21 @@ export interface ITabsContainer extends IDisposable { readonly element: HTMLElement; readonly panels: string[]; readonly size: number; + readonly onDrop: Event; + readonly onTabDragStart: Event; + readonly onGroupDragStart: Event; + readonly onWillShowOverlay: Event<{ + event: WillShowOverlayEvent; + kind: DockviewGroupDropLocation; + }>; hidden: boolean; - delete: (id: string) => void; - indexOf: (id: string) => number; - onDrop: Event; - onTabDragStart: Event; - onGroupDragStart: Event; - setActive: (isGroupActive: boolean) => void; - setActivePanel: (panel: IDockviewPanel) => void; - isActive: (tab: ITab) => boolean; - closePanel: (panel: IDockviewPanel) => void; - openPanel: (panel: IDockviewPanel, index?: number) => void; + delete(id: string): void; + indexOf(id: string): number; + setActive(isGroupActive: boolean): void; + setActivePanel(panel: IDockviewPanel): void; + isActive(tab: Tab): boolean; + closePanel(panel: IDockviewPanel): void; + openPanel(panel: IDockviewPanel, index?: number): void; setRightActionsElement(element: HTMLElement | undefined): void; setLeftActionsElement(element: HTMLElement | undefined): void; setPrefixActionsElement(element: HTMLElement | undefined): void; @@ -59,7 +65,7 @@ export class TabsContainer private readonly preActionsContainer: HTMLElement; private readonly voidContainer: VoidContainer; - private tabs: IValueDisposable[] = []; + private tabs: IValueDisposable[] = []; private selectedIndex = -1; private rightActions: HTMLElement | undefined; private leftActions: HTMLElement | undefined; @@ -77,6 +83,15 @@ export class TabsContainer readonly onGroupDragStart: Event = this._onGroupDragStart.event; + private readonly _onWillShowOverlay = new Emitter<{ + event: WillShowOverlayEvent; + kind: DockviewGroupDropLocation; + }>(); + readonly onWillShowOverlay: Event<{ + event: WillShowOverlayEvent; + kind: DockviewGroupDropLocation; + }> = this._onWillShowOverlay.event; + get panels(): string[] { return this.tabs.map((_) => _.value.panel.id); } @@ -150,7 +165,7 @@ export class TabsContainer return this._element; } - public isActive(tab: ITab): boolean { + public isActive(tab: Tab): boolean { return ( this.selectedIndex > -1 && this.tabs[this.selectedIndex].value === tab @@ -167,12 +182,6 @@ export class TabsContainer ) { super(); - this.addDisposables( - this._onDrop, - this._onTabDragStart, - this._onGroupDragStart - ); - this._element = document.createElement('div'); this._element.className = 'tabs-and-actions-container'; @@ -182,27 +191,6 @@ export class TabsContainer this.accessor.options.singleTabMode === 'fullwidth' ); - this.addDisposables( - this.accessor.onDidAddPanel((e) => { - if (e.api.group === this.group) { - toggleClass( - this._element, - 'dv-single-tab', - this.size === 1 - ); - } - }), - this.accessor.onDidRemovePanel((e) => { - if (e.api.group === this.group) { - toggleClass( - this._element, - 'dv-single-tab', - this.size === 1 - ); - } - }) - ); - this.rightActionsContainer = document.createElement('div'); this.rightActionsContainer.className = 'right-actions-container'; @@ -224,6 +212,28 @@ export class TabsContainer this._element.appendChild(this.rightActionsContainer); this.addDisposables( + this.accessor.onDidAddPanel((e) => { + if (e.api.group === this.group) { + toggleClass( + this._element, + 'dv-single-tab', + this.size === 1 + ); + } + }), + this.accessor.onDidRemovePanel((e) => { + if (e.api.group === this.group) { + toggleClass( + this._element, + 'dv-single-tab', + this.size === 1 + ); + } + }), + this._onWillShowOverlay, + this._onDrop, + this._onTabDragStart, + this._onGroupDragStart, this.voidContainer, this.voidContainer.onDragStart((event) => { this._onGroupDragStart.fire({ @@ -237,6 +247,9 @@ export class TabsContainer index: this.tabs.length, }); }), + this.voidContainer.onWillShowOverlay((event) => { + this._onWillShowOverlay.fire({ event, kind: 'header_space' }); + }), addDisposableListener( this.voidContainer.element, 'mousedown', @@ -286,7 +299,7 @@ export class TabsContainer } private addTab( - tab: IValueDisposable, + tab: IValueDisposable, index: number = this.tabs.length ): void { if (index < 0 || index > this.tabs.length) { @@ -395,10 +408,13 @@ export class TabsContainer event: event.nativeEvent, index: this.tabs.findIndex((x) => x.value === tab), }); + }), + tab.onWillShowOverlay((event) => { + this._onWillShowOverlay.fire({ event, kind: 'tab' }); }) ); - const value: IValueDisposable = { value: tab, disposable }; + const value: IValueDisposable = { value: tab, disposable }; this.addTab(value, index); } diff --git a/packages/dockview-core/src/dockview/components/titlebar/voidContainer.ts b/packages/dockview-core/src/dockview/components/titlebar/voidContainer.ts index 659a1d3a8..3ab3652f4 100644 --- a/packages/dockview-core/src/dockview/components/titlebar/voidContainer.ts +++ b/packages/dockview-core/src/dockview/components/titlebar/voidContainer.ts @@ -1,6 +1,10 @@ import { last } from '../../../array'; import { getPanelData } from '../../../dnd/dataTransfer'; -import { Droptarget, DroptargetEvent } from '../../../dnd/droptarget'; +import { + Droptarget, + DroptargetEvent, + WillShowOverlayEvent, +} from '../../../dnd/droptarget'; import { GroupDragHandler } from '../../../dnd/groupDragHandler'; import { DockviewComponent } from '../../dockviewComponent'; import { addDisposableListener, Emitter, Event } from '../../../events'; @@ -9,7 +13,7 @@ import { DockviewGroupPanel } from '../../dockviewGroupPanel'; export class VoidContainer extends CompositeDisposable { private readonly _element: HTMLElement; - private readonly voidDropTarget: Droptarget; + private readonly dropTraget: Droptarget; private readonly _onDrop = new Emitter(); readonly onDrop: Event = this._onDrop.event; @@ -17,6 +21,8 @@ export class VoidContainer extends CompositeDisposable { private readonly _onDragStart = new Emitter(); readonly onDragStart = this._onDragStart.event; + readonly onWillShowOverlay: Event; + get element(): HTMLElement { return this._element; } @@ -43,7 +49,7 @@ export class VoidContainer extends CompositeDisposable { const handler = new GroupDragHandler(this._element, accessor, group); - this.voidDropTarget = new Droptarget(this._element, { + this.dropTraget = new Droptarget(this._element, { acceptedTargetZones: ['center'], canDisplayOverlay: (event, position) => { const data = getPanelData(); @@ -65,15 +71,17 @@ export class VoidContainer extends CompositeDisposable { }, }); + this.onWillShowOverlay = this.dropTraget.onWillShowOverlay; + this.addDisposables( handler, handler.onDragStart((event) => { this._onDragStart.fire(event); }), - this.voidDropTarget.onDrop((event) => { + this.dropTraget.onDrop((event) => { this._onDrop.fire(event); }), - this.voidDropTarget + this.dropTraget ); } } diff --git a/packages/dockview-core/src/dockview/dockviewComponent.ts b/packages/dockview-core/src/dockview/dockviewComponent.ts index d71bfa1dd..adb15cf95 100644 --- a/packages/dockview-core/src/dockview/dockviewComponent.ts +++ b/packages/dockview-core/src/dockview/dockviewComponent.ts @@ -40,7 +40,9 @@ import { Orientation, Sizing } from '../splitview/splitview'; import { GroupOptions, GroupPanelViewState, - GroupviewDropEvent, + DockviewDidDropEvent, + DockviewWillDropEvent, + WillShowOverlayLocationEvent, } from './dockviewGroupPanelModel'; import { DockviewGroupPanel } from './dockviewGroupPanel'; import { DockviewPanelModel } from './dockviewPanelModel'; @@ -226,18 +228,16 @@ export type DockviewComponentUpdateOptions = Pick< | 'disableFloatingGroups' | 'floatingGroupBounds' | 'rootOverlayModel' + | 'disableDnd' >; -export interface DockviewDropEvent extends GroupviewDropEvent { - api: DockviewApi; - group: DockviewGroupPanel | null; -} - export interface IDockviewComponent extends IBaseGrid { readonly activePanel: IDockviewPanel | undefined; readonly totalPanels: number; readonly panels: IDockviewPanel[]; - readonly onDidDrop: Event; + readonly onDidDrop: Event; + readonly onWillDrop: Event; + readonly onWillShowOverlay: Event; readonly orientation: Orientation; updateOptions(options: DockviewComponentUpdateOptions): void; moveGroupOrPanel( @@ -305,8 +305,16 @@ export class DockviewComponent readonly onWillDragGroup: Event = this._onWillDragGroup.event; - private readonly _onDidDrop = new Emitter(); - readonly onDidDrop: Event = this._onDidDrop.event; + private readonly _onDidDrop = new Emitter(); + readonly onDidDrop: Event = this._onDidDrop.event; + + private readonly _onWillDrop = new Emitter(); + readonly onWillDrop: Event = this._onWillDrop.event; + + private readonly _onWillShowOverlay = + new Emitter(); + readonly onWillShowOverlay: Event = + this._onWillShowOverlay.event; private readonly _onDidRemovePanel = new Emitter(); readonly onDidRemovePanel: Event = @@ -380,11 +388,13 @@ export class DockviewComponent this.overlayRenderContainer, this._onWillDragPanel, this._onWillDragGroup, + this._onWillShowOverlay, this._onDidActivePanelChange, this._onDidAddPanel, this._onDidRemovePanel, this._onDidLayoutFromJSON, this._onDidDrop, + this._onWillDrop, Event.any( this.onDidAddGroup, this.onDidRemoveGroup @@ -477,6 +487,22 @@ export class DockviewComponent this.addDisposables( this._rootDropTarget.onDrop((event) => { + const willDropEvent = new DockviewWillDropEvent({ + nativeEvent: event.nativeEvent, + position: event.position, + panel: undefined, + api: this._api, + group: undefined, + getData: getPanelData, + kind: 'content', + }); + + this._onWillDrop.fire(willDropEvent); + + if (willDropEvent.defaultPrevented) { + return; + } + const data = getPanelData(); if (data) { @@ -487,12 +513,16 @@ export class DockviewComponent 'center' ); } else { - this._onDidDrop.fire({ - ...event, - api: this._api, - group: null, - getData: getPanelData, - }); + this._onDidDrop.fire( + new DockviewDidDropEvent({ + nativeEvent: event.nativeEvent, + position: event.position, + panel: undefined, + api: this._api, + group: undefined, + getData: getPanelData, + }) + ); } }), this._rootDropTarget @@ -1652,11 +1682,18 @@ export class DockviewComponent this.moveGroupOrPanel(view, groupId, itemId, target, index); }), view.model.onDidDrop((event) => { - this._onDidDrop.fire({ - ...event, - api: this._api, - group: view, - }); + this._onDidDrop.fire(event); + }), + view.model.onWillDrop((event) => { + this._onWillDrop.fire(event); + }), + view.model.onWillShowOverlay((event) => { + if (this.options.disableDnd) { + event.event.preventDefault(); + return; + } + + this._onWillShowOverlay.fire(event); }), view.model.onDidAddPanel((event) => { this._onDidAddPanel.fire(event.panel); diff --git a/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts b/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts index 6837fe618..5b741c3c4 100644 --- a/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts +++ b/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts @@ -1,9 +1,14 @@ import { DockviewApi } from '../api/component.api'; import { getPanelData, PanelTransfer } from '../dnd/dataTransfer'; -import { Position } from '../dnd/droptarget'; +import { Position, WillShowOverlayEvent } from '../dnd/droptarget'; import { DockviewComponent } from './dockviewComponent'; import { isAncestor, toggleClass } from '../dom'; -import { addDisposableListener, Emitter, Event } from '../events'; +import { + addDisposableListener, + DockviewEvent, + Emitter, + Event, +} from '../events'; import { IViewSize } from '../gridview/gridview'; import { CompositeDisposable } from '../lifecycle'; import { IPanel, PanelInitParameters, PanelUpdateEvent } from '../panel/types'; @@ -46,15 +51,69 @@ export interface GroupPanelViewState extends CoreGroupOptions { id: string; } -export interface GroupviewChangeEvent { +export interface DockviewGroupChangeEvent { readonly panel: IDockviewPanel; } -export interface GroupviewDropEvent { - readonly nativeEvent: DragEvent; - readonly position: Position; - readonly index?: number; - getData(): PanelTransfer | undefined; +export class DockviewDidDropEvent extends DockviewEvent { + get nativeEvent(): DragEvent { + return this.options.nativeEvent; + } + + get position(): Position { + return this.options.position; + } + + get panel(): IDockviewPanel | undefined { + return this.options.panel; + } + + get group(): DockviewGroupPanel | undefined { + return this.options.group; + } + + get api(): DockviewApi { + return this.options.api; + } + + constructor( + private readonly options: { + readonly nativeEvent: DragEvent; + readonly position: Position; + readonly panel?: IDockviewPanel; + getData(): PanelTransfer | undefined; + group?: DockviewGroupPanel; + api: DockviewApi; + } + ) { + super(); + } + + getData(): PanelTransfer | undefined { + return this.options.getData(); + } +} + +export class DockviewWillDropEvent extends DockviewDidDropEvent { + private readonly _kind: DockviewGroupDropLocation; + + get kind(): DockviewGroupDropLocation { + return this._kind; + } + + constructor(options: { + readonly nativeEvent: DragEvent; + readonly position: Position; + readonly panel?: IDockviewPanel; + getData(): PanelTransfer | undefined; + kind: DockviewGroupDropLocation; + group?: DockviewGroupPanel; + api: DockviewApi; + }) { + super(options); + + this._kind = options.kind; + } } export interface IHeader { @@ -63,6 +122,8 @@ export interface IHeader { export type DockviewGroupPanelLocked = boolean | 'no-drop-target'; +export type DockviewGroupDropLocation = 'tab' | 'header_space' | 'content'; + export interface IDockviewGroupPanelModel extends IPanel { readonly isActive: boolean; readonly size: number; @@ -70,10 +131,11 @@ export interface IDockviewGroupPanelModel extends IPanel { readonly activePanel: IDockviewPanel | undefined; readonly header: IHeader; readonly isContentFocused: boolean; - readonly onDidDrop: Event; - readonly onDidAddPanel: Event; - readonly onDidRemovePanel: Event; - readonly onDidActivePanelChange: Event; + readonly onDidDrop: Event; + readonly onWillDrop: Event; + readonly onDidAddPanel: Event; + readonly onDidRemovePanel: Event; + readonly onDidActivePanelChange: Event; readonly onMove: Event; locked: DockviewGroupPanelLocked; setActive(isActive: boolean): void; @@ -112,13 +174,17 @@ export interface IDockviewGroupPanelModel extends IPanel { export type DockviewGroupLocation = 'grid' | 'floating' | 'popout'; +export interface WillShowOverlayLocationEvent { + event: WillShowOverlayEvent; + kind: DockviewGroupDropLocation; +} + export class DockviewGroupPanelModel extends CompositeDisposable implements IDockviewGroupPanelModel { private readonly tabsContainer: ITabsContainer; private readonly contentContainer: IContentContainer; - // private readonly dropTarget: Droptarget; private _activePanel: IDockviewPanel | undefined; private watermark?: IWatermarkRenderer; private _isGroupActive = false; @@ -143,8 +209,16 @@ export class DockviewGroupPanelModel private readonly _onMove = new Emitter(); readonly onMove: Event = this._onMove.event; - private readonly _onDidDrop = new Emitter(); - readonly onDidDrop: Event = this._onDidDrop.event; + private readonly _onDidDrop = new Emitter(); + readonly onDidDrop: Event = this._onDidDrop.event; + + private readonly _onWillDrop = new Emitter(); + readonly onWillDrop: Event = this._onWillDrop.event; + + private readonly _onWillShowOverlay = + new Emitter(); + readonly onWillShowOverlay: Event = + this._onWillShowOverlay.event; private readonly _onTabDragStart = new Emitter(); readonly onTabDragStart: Event = this._onTabDragStart.event; @@ -153,19 +227,22 @@ export class DockviewGroupPanelModel readonly onGroupDragStart: Event = this._onGroupDragStart.event; - private readonly _onDidAddPanel = new Emitter(); - readonly onDidAddPanel: Event = + private readonly _onDidAddPanel = new Emitter(); + readonly onDidAddPanel: Event = this._onDidAddPanel.event; - private readonly _onDidRemovePanel = new Emitter(); - readonly onDidRemovePanel: Event = + private readonly _onDidRemovePanel = + new Emitter(); + readonly onDidRemovePanel: Event = this._onDidRemovePanel.event; private readonly _onDidActivePanelChange = - new Emitter(); - readonly onDidActivePanelChange: Event = + new Emitter(); + readonly onDidActivePanelChange: Event = this._onDidActivePanelChange.event; + private readonly _api: DockviewApi; + get element(): HTMLElement { throw new Error('not supported'); } @@ -279,6 +356,8 @@ export class DockviewGroupPanelModel toggleClass(this.container, 'groupview', true); + this._api = new DockviewApi(this.accessor); + this.tabsContainer = new TabsContainer(this.accessor, this.groupPanel); this.contentContainer = new ContentContainer(this.accessor, this); @@ -294,6 +373,7 @@ export class DockviewGroupPanelModel this.addDisposables( this._onTabDragStart, this._onGroupDragStart, + this._onWillShowOverlay, this.tabsContainer.onTabDragStart((event) => { this._onTabDragStart.fire(event); }), @@ -308,6 +388,7 @@ export class DockviewGroupPanelModel event.index ); }), + this.contentContainer.onDidFocus(() => { this.accessor.doSetGroupActive(this.groupPanel, true); }), @@ -321,9 +402,16 @@ export class DockviewGroupPanelModel event.position ); }), + this.tabsContainer.onWillShowOverlay((event) => { + this._onWillShowOverlay.fire(event); + }), + this.contentContainer.dropTarget.onWillShowOverlay((event) => { + this._onWillShowOverlay.fire({ event, kind: 'content' }); + }), this._onMove, this._onDidChange, this._onDidDrop, + this._onWillDrop, this._onDidAddPanel, this._onDidRemovePanel, this._onDidActivePanelChange @@ -331,13 +419,13 @@ export class DockviewGroupPanelModel } initialize(): void { - if (this.options?.panels) { + if (this.options.panels) { this.options.panels.forEach((panel) => { this.doAddPanel(panel); }); } - if (this.options?.activePanel) { + if (this.options.activePanel) { this.openPanel(this.options.activePanel); } @@ -353,7 +441,7 @@ export class DockviewGroupPanelModel ); this.addDisposables(this._rightHeaderActions); this._rightHeaderActions.init({ - containerApi: new DockviewApi(this.accessor), + containerApi: this._api, api: this.groupPanel.api, }); this.tabsContainer.setRightActionsElement( @@ -368,7 +456,7 @@ export class DockviewGroupPanelModel ); this.addDisposables(this._leftHeaderActions); this._leftHeaderActions.init({ - containerApi: new DockviewApi(this.accessor), + containerApi: this._api, api: this.groupPanel.api, }); this.tabsContainer.setLeftActionsElement( @@ -383,7 +471,7 @@ export class DockviewGroupPanelModel ); this.addDisposables(this._prefixHeaderActions); this._prefixHeaderActions.init({ - containerApi: new DockviewApi(this.accessor), + containerApi: this._api, api: this.groupPanel.api, }); this.tabsContainer.setPrefixActionsElement( @@ -721,7 +809,7 @@ export class DockviewGroupPanelModel if (this.isEmpty && !this.watermark) { const watermark = this.accessor.createWatermarkComponent(); watermark.init({ - containerApi: new DockviewApi(this.accessor), + containerApi: this._api, group: this.groupPanel, }); this.watermark = watermark; @@ -773,7 +861,7 @@ export class DockviewGroupPanelModel return; } - function getKind(): 'tab' | 'header_space' | 'content' { + function getKind(): DockviewGroupDropLocation { switch (type) { case 'header': return typeof index === 'number' ? 'tab' : 'header_space'; @@ -782,7 +870,24 @@ export class DockviewGroupPanelModel } } - const kind = getKind(); + const panel = + typeof index === 'number' ? this.panels[index] : undefined; + + const willDropEvent = new DockviewWillDropEvent({ + nativeEvent: event, + position, + panel, + getData: () => getPanelData(), + kind: getKind(), + group: this.groupPanel, + api: this._api, + }); + + this._onWillDrop.fire(willDropEvent); + + if (willDropEvent.defaultPrevented) { + return; + } const data = getPanelData(); @@ -791,7 +896,6 @@ export class DockviewGroupPanelModel // this is a group move dnd event const { groupId } = data; - // TODO: intercept this._onMove.fire({ target: position, groupId: groupId, @@ -816,7 +920,6 @@ export class DockviewGroupPanelModel } } - // TODO: intercept this._onMove.fire({ target: position, groupId: data.groupId, @@ -824,12 +927,16 @@ export class DockviewGroupPanelModel index, }); } else { - this._onDidDrop.fire({ - nativeEvent: event, - position, - index, - getData: () => getPanelData(), - }); + this._onDidDrop.fire( + new DockviewDidDropEvent({ + nativeEvent: event, + position, + panel, + getData: () => getPanelData(), + group: this.groupPanel, + api: this._api, + }) + ); } } diff --git a/packages/dockview-core/src/dockview/options.ts b/packages/dockview-core/src/dockview/options.ts index 34ab98989..78514dfe7 100644 --- a/packages/dockview-core/src/dockview/options.ts +++ b/packages/dockview-core/src/dockview/options.ts @@ -101,6 +101,7 @@ export interface DockviewComponentOptions extends DockviewRenderFunctions { defaultRenderer?: DockviewPanelRenderer; debug?: boolean; rootOverlayModel?: DroptargetOverlayModel; + disableDnd?: boolean; } export interface PanelOptions

{ diff --git a/packages/dockview-core/src/events.ts b/packages/dockview-core/src/events.ts index 9474ad317..27c515460 100644 --- a/packages/dockview-core/src/events.ts +++ b/packages/dockview-core/src/events.ts @@ -24,6 +24,18 @@ export namespace Event { }; } +export class DockviewEvent { + private _defaultPrevented = false; + + get defaultPrevented(): boolean { + return this._defaultPrevented; + } + + preventDefault(): void { + this._defaultPrevented = true; + } +} + class LeakageMonitor { readonly events = new Map, Stacktrace>(); diff --git a/packages/dockview/src/dockview/dockview.tsx b/packages/dockview/src/dockview/dockview.tsx index 91aaa0562..2289e107f 100644 --- a/packages/dockview/src/dockview/dockview.tsx +++ b/packages/dockview/src/dockview/dockview.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { DockviewComponent, - DockviewDropEvent, + DockviewWillDropEvent, DockviewDndOverlayEvent, GroupPanelFrameworkComponentFactory, DockviewPanelApi, @@ -12,6 +12,7 @@ import { IHeaderActionsRenderer, DockviewPanelRenderer, DroptargetOverlayModel, + DockviewDidDropEvent, } from 'dockview-core'; import { ReactPanelContentPart } from './reactContentPart'; import { ReactPanelHeaderPart } from './reactHeaderPart'; @@ -61,7 +62,8 @@ export interface IDockviewReactProps { components: PanelCollection; tabComponents?: PanelCollection; watermarkComponent?: React.FunctionComponent; - onDidDrop?: (event: DockviewDropEvent) => void; + onDidDrop?: (event: DockviewDidDropEvent) => void; + onWillDrop?: (event: DockviewWillDropEvent) => void; showDndOverlay?: (event: DockviewDndOverlayEvent) => boolean; hideBorders?: boolean; className?: string; @@ -81,6 +83,7 @@ export interface IDockviewReactProps { debug?: boolean; defaultRenderer?: DockviewPanelRenderer; rootOverlayModel?: DroptargetOverlayModel; + disableDnd?: boolean; } const DEFAULT_REACT_TAB = 'props.defaultTabComponent'; @@ -183,6 +186,7 @@ export const DockviewReact = React.forwardRef( defaultRenderer: props.defaultRenderer, debug: props.debug, rootOverlayModel: props.rootOverlayModel, + disableDnd: props.disableDnd, }); const { clientWidth, clientHeight } = domRef.current; @@ -199,6 +203,15 @@ export const DockviewReact = React.forwardRef( }; }, []); + React.useEffect(() => { + if (!dockviewRef.current) { + return; + } + dockviewRef.current.updateOptions({ + disableDnd: props.disableDnd, + }); + }, [props.disableDnd]); + React.useEffect(() => { if (!dockviewRef.current) { return () => { @@ -217,6 +230,24 @@ export const DockviewReact = React.forwardRef( }; }, [props.onDidDrop]); + React.useEffect(() => { + if (!dockviewRef.current) { + return () => { + // noop + }; + } + + const disposable = dockviewRef.current.onWillDrop((event) => { + if (props.onWillDrop) { + props.onWillDrop(event); + } + }); + + return () => { + disposable.dispose(); + }; + }, [props.onWillDrop]); + React.useEffect(() => { if (!dockviewRef.current) { return; From 22ba48e81ee29bd40ce4a5a80352b35c133b9b7f Mon Sep 17 00:00:00 2001 From: mathuo <6710312+mathuo@users.noreply.github.com> Date: Fri, 19 Jan 2024 14:17:37 +0000 Subject: [PATCH 03/19] feat: locked mode --- .../src/dockview/dockviewComponent.ts | 1 + .../dockview-core/src/dockview/options.ts | 1 + .../src/gridview/baseComponentGridview.ts | 11 +++++ .../dockview-core/src/gridview/branchNode.ts | 14 +++++- .../dockview-core/src/gridview/gridview.ts | 44 ++++++++++++++++--- .../src/splitview/splitview.scss | 6 +++ .../dockview-core/src/splitview/splitview.ts | 11 +++++ packages/dockview/src/dockview/dockview.tsx | 10 +++++ 8 files changed, 90 insertions(+), 8 deletions(-) diff --git a/packages/dockview-core/src/dockview/dockviewComponent.ts b/packages/dockview-core/src/dockview/dockviewComponent.ts index 82e5283a4..2ec7b7272 100644 --- a/packages/dockview-core/src/dockview/dockviewComponent.ts +++ b/packages/dockview-core/src/dockview/dockviewComponent.ts @@ -369,6 +369,7 @@ export class DockviewComponent styles: options.styles, parentElement: options.parentElement, disableAutoResizing: options.disableAutoResizing, + locked: options.locked, }); const gready = document.createElement('div'); diff --git a/packages/dockview-core/src/dockview/options.ts b/packages/dockview-core/src/dockview/options.ts index 34ab98989..ade5f7529 100644 --- a/packages/dockview-core/src/dockview/options.ts +++ b/packages/dockview-core/src/dockview/options.ts @@ -101,6 +101,7 @@ export interface DockviewComponentOptions extends DockviewRenderFunctions { defaultRenderer?: DockviewPanelRenderer; debug?: boolean; rootOverlayModel?: DroptargetOverlayModel; + locked?: boolean; } export interface PanelOptions

{ diff --git a/packages/dockview-core/src/gridview/baseComponentGridview.ts b/packages/dockview-core/src/gridview/baseComponentGridview.ts index 2e4159a9b..0e152de00 100644 --- a/packages/dockview-core/src/gridview/baseComponentGridview.ts +++ b/packages/dockview-core/src/gridview/baseComponentGridview.ts @@ -34,6 +34,7 @@ export interface BaseGridOptions { readonly styles?: ISplitviewStyles; readonly parentElement?: HTMLElement; readonly disableAutoResizing?: boolean; + readonly locked?: boolean; } export interface IGridPanelView extends IGridView, IPanel { @@ -133,6 +134,14 @@ export abstract class BaseGrid return this._activeGroup; } + get locked(): boolean { + return this.gridview.locked; + } + + set locked(value: boolean) { + this.gridview.locked = value; + } + constructor(options: BaseGridOptions) { super(options.parentElement, options.disableAutoResizing); @@ -142,6 +151,8 @@ export abstract class BaseGrid options.orientation ); + this.gridview.locked = !!options.locked; + this.element.appendChild(this.gridview.element); this.layout(0, 0, true); // set some elements height/widths diff --git a/packages/dockview-core/src/gridview/branchNode.ts b/packages/dockview-core/src/gridview/branchNode.ts index 8ba96ef05..d0e449e01 100644 --- a/packages/dockview-core/src/gridview/branchNode.ts +++ b/packages/dockview-core/src/gridview/branchNode.ts @@ -131,17 +131,27 @@ export class BranchNode extends CompositeDisposable implements IView { return LayoutPriority.Normal; } + get locked(): boolean { + return this.splitview.locked; + } + + set locked(value: boolean) { + this.splitview.locked = value; + } + constructor( readonly orientation: Orientation, readonly proportionalLayout: boolean, readonly styles: ISplitviewStyles | undefined, size: number, orthogonalSize: number, + disabled: boolean, childDescriptors?: INodeDescriptor[] ) { super(); this._orthogonalSize = orthogonalSize; this._size = size; + this.element = document.createElement('div'); this.element.className = 'branch-node'; @@ -177,6 +187,8 @@ export class BranchNode extends CompositeDisposable implements IView { }); } + this.locked = disabled; + this.addDisposables( this._onDidChange, this._onDidVisibilityChange, @@ -202,7 +214,7 @@ export class BranchNode extends CompositeDisposable implements IView { return this.splitview.isViewVisible(index); } - setChildVisible(index: number, visible: boolean): void { + setChildVisible(index: number, visible: boolean): void { if (index < 0 || index >= this.children.length) { throw new Error('Invalid index'); } diff --git a/packages/dockview-core/src/gridview/gridview.ts b/packages/dockview-core/src/gridview/gridview.ts index f34fe2672..e7f54637d 100644 --- a/packages/dockview-core/src/gridview/gridview.ts +++ b/packages/dockview-core/src/gridview/gridview.ts @@ -41,7 +41,8 @@ function flipNode( node.proportionalLayout, node.styles, size, - orthogonalSize + orthogonalSize, + node.locked ); let totalSize = 0; @@ -273,6 +274,7 @@ export class Gridview implements IDisposable { readonly element: HTMLElement; private _root: BranchNode | undefined; + private _locked = false; private _maximizedNode: LeafNode | undefined = undefined; private readonly disposable: MutableDisposable = new MutableDisposable(); @@ -328,6 +330,30 @@ export class Gridview implements IDisposable { return this.root.maximumHeight; } + get locked(): boolean { + return this._locked; + } + + set locked(value: boolean) { + this._locked = value; + + const branch: Node[] = [this.root]; + + /** + * simple depth-first-search to cover all nodes + * + * @see https://en.wikipedia.org/wiki/Depth-first_search + */ + while (branch.length > 0) { + const node = branch.pop(); + + if (node instanceof BranchNode) { + node.locked = value; + branch.push(...node.children); + } + } + } + maximizedView(): IGridView | undefined { return this._maximizedNode?.view; } @@ -427,7 +453,8 @@ export class Gridview implements IDisposable { this.proportionalLayout, this.styles, this.root.size, - this.root.orthogonalSize + this.root.orthogonalSize, + this._locked ); } @@ -487,8 +514,8 @@ export class Gridview implements IDisposable { this.proportionalLayout, this.styles, node.size, // <- orthogonal size - flips at each depth - orthogonalSize, // <- size - flips at each depth - + orthogonalSize, // <- size - flips at each depth, + this._locked, children ); } else { @@ -540,7 +567,8 @@ export class Gridview implements IDisposable { this.proportionalLayout, this.styles, this.root.orthogonalSize, - this.root.size + this.root.size, + this._locked ); if (oldRoot.children.length === 0) { @@ -655,7 +683,8 @@ export class Gridview implements IDisposable { proportionalLayout, styles, 0, - 0 + 0, + this._locked ); } @@ -739,7 +768,8 @@ export class Gridview implements IDisposable { this.proportionalLayout, this.styles, parent.size, - parent.orthogonalSize + parent.orthogonalSize, + this._locked ); grandParent.addChild(newParent, parent.size, parentIndex); diff --git a/packages/dockview-core/src/splitview/splitview.scss b/packages/dockview-core/src/splitview/splitview.scss index b12b0969a..82c8a7ced 100644 --- a/packages/dockview-core/src/splitview/splitview.scss +++ b/packages/dockview-core/src/splitview/splitview.scss @@ -25,6 +25,12 @@ height: 100%; width: 100%; + &.dv-splitview-locked { + & > .sash-container > .sash { + pointer-events: none; + } + } + &.animation { .view, .sash { diff --git a/packages/dockview-core/src/splitview/splitview.ts b/packages/dockview-core/src/splitview/splitview.ts index c8b3fdf66..4c5ef3707 100644 --- a/packages/dockview-core/src/splitview/splitview.ts +++ b/packages/dockview-core/src/splitview/splitview.ts @@ -109,6 +109,7 @@ export class Splitview { private proportionalLayout: boolean; private _startSnappingEnabled = true; private _endSnappingEnabled = true; + private _disabled = false; private readonly _onDidSashEnd = new Emitter(); readonly onDidSashEnd = this._onDidSashEnd.event; @@ -200,6 +201,16 @@ export class Splitview { this.updateSashEnablement(); } + get locked(): boolean { + return this._disabled; + } + + set locked(value: boolean) { + this._disabled = value; + + toggleClass(this.element, 'dv-splitview-locked', value); + } + constructor( private readonly container: HTMLElement, options: SplitViewOptions diff --git a/packages/dockview/src/dockview/dockview.tsx b/packages/dockview/src/dockview/dockview.tsx index 91aaa0562..f0012c1ca 100644 --- a/packages/dockview/src/dockview/dockview.tsx +++ b/packages/dockview/src/dockview/dockview.tsx @@ -81,6 +81,7 @@ export interface IDockviewReactProps { debug?: boolean; defaultRenderer?: DockviewPanelRenderer; rootOverlayModel?: DroptargetOverlayModel; + locked?: boolean; } const DEFAULT_REACT_TAB = 'props.defaultTabComponent'; @@ -183,6 +184,7 @@ export const DockviewReact = React.forwardRef( defaultRenderer: props.defaultRenderer, debug: props.debug, rootOverlayModel: props.rootOverlayModel, + locked: props.locked, }); const { clientWidth, clientHeight } = domRef.current; @@ -199,6 +201,14 @@ export const DockviewReact = React.forwardRef( }; }, []); + React.useEffect(() => { + if (!dockviewRef.current) { + return; + } + + dockviewRef.current.locked = !!props.locked; + }, [props.locked]); + React.useEffect(() => { if (!dockviewRef.current) { return () => { From 23b1edb0033941fb9bdd57b1104ae97993a22e1c Mon Sep 17 00:00:00 2001 From: sachnk <5503945+sachnk@users.noreply.github.com> Date: Mon, 22 Jan 2024 19:02:53 +0000 Subject: [PATCH 04/19] add window-lifecycle callbacks --- packages/dockview-core/src/api/component.api.ts | 2 ++ .../dockview-core/src/dockview/dockviewComponent.ts | 6 ++++++ .../src/dockview/dockviewPopoutGroupPanel.ts | 4 ++++ packages/dockview-core/src/popoutWindow.ts | 10 ++++++++++ 4 files changed, 22 insertions(+) diff --git a/packages/dockview-core/src/api/component.api.ts b/packages/dockview-core/src/api/component.api.ts index d4a8aae02..02f4292a7 100644 --- a/packages/dockview-core/src/api/component.api.ts +++ b/packages/dockview-core/src/api/component.api.ts @@ -830,6 +830,8 @@ export class DockviewApi implements CommonApi { options?: { position?: Box; popoutUrl?: string; + onOpened?: (id: string, window: Window) => void; + onClosing?: (id: string, window: Window) => void; } ): void { this.component.addPopoutGroup(item, options); diff --git a/packages/dockview-core/src/dockview/dockviewComponent.ts b/packages/dockview-core/src/dockview/dockviewComponent.ts index 82e5283a4..0080c93a3 100644 --- a/packages/dockview-core/src/dockview/dockviewComponent.ts +++ b/packages/dockview-core/src/dockview/dockviewComponent.ts @@ -286,6 +286,8 @@ export interface IDockviewComponent extends IBaseGrid { options?: { position?: Box; popoutUrl?: string; + onOpened?: (id: string, window: Window) => void; + onClosing?: (id: string, window: Window) => void; } ): void; } @@ -513,6 +515,8 @@ export class DockviewComponent skipRemoveGroup?: boolean; position?: Box; popoutUrl?: string; + onOpened?: (id: string, window: Window) => void; + onClosing?: (id: string, window: Window) => void; } ): void { let group: DockviewGroupPanel; @@ -561,6 +565,8 @@ export class DockviewComponent width: box.width, height: box.height, }, + onOpened: options?.onOpened, + onClosing: options?.onClosing } ); diff --git a/packages/dockview-core/src/dockview/dockviewPopoutGroupPanel.ts b/packages/dockview-core/src/dockview/dockviewPopoutGroupPanel.ts index 803fa5411..6d8e7d1b0 100644 --- a/packages/dockview-core/src/dockview/dockviewPopoutGroupPanel.ts +++ b/packages/dockview-core/src/dockview/dockviewPopoutGroupPanel.ts @@ -13,6 +13,8 @@ export class DockviewPopoutGroupPanel extends CompositeDisposable { className: string; popoutUrl: string; box: Box; + onOpened?: (id: string, window: Window) => void; + onClosing?: (id: string, window: Window) => void; } ) { super(); @@ -23,6 +25,8 @@ export class DockviewPopoutGroupPanel extends CompositeDisposable { top: this.options.box.top, width: this.options.box.width, height: this.options.box.height, + onOpened: this.options.onOpened, + onClosing: this.options.onClosing, }); group.model.location = 'popout'; diff --git a/packages/dockview-core/src/popoutWindow.ts b/packages/dockview-core/src/popoutWindow.ts index c73334549..1e26e4257 100644 --- a/packages/dockview-core/src/popoutWindow.ts +++ b/packages/dockview-core/src/popoutWindow.ts @@ -5,6 +5,8 @@ import { Box } from './types'; export type PopoutWindowOptions = { url: string; + onOpened?: (id: string, window: Window) => void; + onClosing?: (id: string, window: Window) => void; } & Box; export class PopoutWindow extends CompositeDisposable { @@ -42,6 +44,10 @@ export class PopoutWindow extends CompositeDisposable { close(): void { if (this._window) { + if (this.options.onClosing) { + this.options.onClosing(this.id, this._window.value); + } + this._window.disposable.dispose(); this._window.value.close(); this._window = null; @@ -114,5 +120,9 @@ export class PopoutWindow extends CompositeDisposable { cleanUp(); }); }); + + if (this.options.onOpened) { + this.options.onOpened(this.id, externalWindow); + } } } From a24dd21ca2b22d2eb87f230975ef1bfc3ef766f9 Mon Sep 17 00:00:00 2001 From: mathuo <6710312+mathuo@users.noreply.github.com> Date: Sat, 27 Jan 2024 14:11:47 +0000 Subject: [PATCH 05/19] feat: provide means to obtain popoutWindow document --- .../__tests__/dnd/groupDragHandler.spec.ts | 6 +- .../components/titlebar/tabsContainer.spec.ts | 40 ++- .../dockview/dockviewComponent.spec.ts | 244 +++++++++--------- .../__tests__/overlayRenderContainer.spec.ts | 4 +- .../src/api/dockviewGroupPanelApi.ts | 15 +- .../dockview-core/src/api/dockviewPanelApi.ts | 48 +++- .../dockview-core/src/dnd/groupDragHandler.ts | 2 +- .../src/dockview/components/panel/content.ts | 2 +- .../components/titlebar/tabsContainer.ts | 4 +- .../src/dockview/dockviewComponent.ts | 20 +- .../src/dockview/dockviewGroupPanelModel.ts | 9 +- .../src/dockview/dockviewPopoutGroupPanel.ts | 7 +- packages/dockview-core/src/index.ts | 2 + .../src/overlayRenderContainer.ts | 2 +- packages/dockview-core/src/popoutWindow.ts | 74 ++++-- yarn.lock | 9 +- 16 files changed, 288 insertions(+), 200 deletions(-) diff --git a/packages/dockview-core/src/__tests__/dnd/groupDragHandler.spec.ts b/packages/dockview-core/src/__tests__/dnd/groupDragHandler.spec.ts index b2cd3d44d..0ea0f7f5f 100644 --- a/packages/dockview-core/src/__tests__/dnd/groupDragHandler.spec.ts +++ b/packages/dockview-core/src/__tests__/dnd/groupDragHandler.spec.ts @@ -11,7 +11,7 @@ describe('groupDragHandler', () => { const groupMock = jest.fn(() => { const partial: Partial = { id: 'test_group_id', - api: { location: 'grid' } as any, + api: { location: { type: 'grid' } } as any, }; return partial as DockviewGroupPanel; }); @@ -53,7 +53,7 @@ describe('groupDragHandler', () => { const groupMock = jest.fn(() => { const partial: Partial = { - api: { location: 'floating' } as any, + api: { location: { type: 'floating' } } as any, }; return partial as DockviewGroupPanel; }); @@ -85,7 +85,7 @@ describe('groupDragHandler', () => { const groupMock = jest.fn(() => { const partial: Partial = { - api: { location: 'grid' } as any, + api: { location: { type: 'grid' } } as any, }; return partial as DockviewGroupPanel; }); diff --git a/packages/dockview-core/src/__tests__/dockview/components/titlebar/tabsContainer.spec.ts b/packages/dockview-core/src/__tests__/dockview/components/titlebar/tabsContainer.spec.ts index 58a007393..266781742 100644 --- a/packages/dockview-core/src/__tests__/dockview/components/titlebar/tabsContainer.spec.ts +++ b/packages/dockview-core/src/__tests__/dockview/components/titlebar/tabsContainer.spec.ts @@ -9,6 +9,7 @@ import { DockviewGroupPanelModel } from '../../../../dockview/dockviewGroupPanel import { fireEvent } from '@testing-library/dom'; import { TestPanel } from '../../dockviewGroupPanelModel.spec'; import { IDockviewPanel } from '../../../../dockview/dockviewPanel'; +import { fromPartial } from '@total-typescript/shoehorn'; describe('tabsContainer', () => { test('that an external event does not render a drop target and calls through to the group mode', () => { @@ -478,7 +479,7 @@ describe('tabsContainer', () => { const groupPanelMock = jest.fn(() => { return (>{ - api: { location: 'grid' } as any, + api: { location: { type: 'grid' } } as any, }) as DockviewGroupPanel; }); @@ -538,7 +539,7 @@ describe('tabsContainer', () => { const groupPanelMock = jest.fn(() => { return (>{ - api: { location: 'floating' } as any, + api: { location: { type: 'floating' } } as any, }) as DockviewGroupPanel; }); @@ -591,7 +592,7 @@ describe('tabsContainer', () => { const groupPanelMock = jest.fn(() => { return (>{ - api: { location: 'floating' } as any, + api: { location: { type: 'floating' } } as any, model: {} as any, }) as DockviewGroupPanel; }); @@ -601,23 +602,20 @@ describe('tabsContainer', () => { const cut = new TabsContainer(accessor, groupPanel); - const panelMock = jest.fn((id: string) => { - const partial: Partial = { + const createPanel = (id: string) => + fromPartial({ id, - view: { tab: { element: document.createElement('div'), - } as any, + }, content: { element: document.createElement('div'), - } as any, - } as any, - }; - return partial as IDockviewPanel; - }); + }, + }, + }); - const panel = new panelMock('test_id'); + const panel = createPanel('test_id'); cut.openPanel(panel); const el = cut.element.querySelector('.tab')!; @@ -628,15 +626,15 @@ describe('tabsContainer', () => { fireEvent(el, event); // a floating group with a single tab shouldn't be eligible - expect(preventDefaultSpy).toBeCalledTimes(0); - expect(accessor.addFloatingGroup).toBeCalledTimes(0); + expect(preventDefaultSpy).toHaveBeenCalledTimes(0); + expect(accessor.addFloatingGroup).toHaveBeenCalledTimes(0); - const panel2 = new panelMock('test_id_2'); + const panel2 = createPanel('test_id_2'); cut.openPanel(panel2); fireEvent(el, event); - expect(preventDefaultSpy).toBeCalledTimes(1); - expect(accessor.addFloatingGroup).toBeCalledTimes(1); + expect(preventDefaultSpy).toHaveBeenCalledTimes(1); + expect(accessor.addFloatingGroup).toHaveBeenCalledTimes(1); }); test('pre header actions', () => { @@ -653,7 +651,7 @@ describe('tabsContainer', () => { const groupPanelMock = jest.fn(() => { return (>{ - api: { location: 'grid' } as any, + api: { location: { type: 'grid' } } as any, model: {} as any, }) as DockviewGroupPanel; }); @@ -723,7 +721,7 @@ describe('tabsContainer', () => { const groupPanelMock = jest.fn(() => { return (>{ - api: { location: 'grid' } as any, + api: { location: { type: 'grid' } } as any, model: {} as any, }) as DockviewGroupPanel; }); @@ -793,7 +791,7 @@ describe('tabsContainer', () => { const groupPanelMock = jest.fn(() => { return (>{ - api: { location: 'grid' } as any, + api: { location: { type: 'grid' } } as any, model: {} as any, }) as DockviewGroupPanel; }); diff --git a/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts b/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts index 9dd25066d..15e38dd61 100644 --- a/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts +++ b/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts @@ -3452,8 +3452,8 @@ describe('dockviewComponent', () => { floating: true, }); - expect(panel1.group.api.location).toBe('grid'); - expect(panel2.group.api.location).toBe('floating'); + expect(panel1.group.api.location.type).toBe('grid'); + expect(panel2.group.api.location.type).toBe('floating'); expect(dockview.groups.length).toBe(2); expect(dockview.panels.length).toBe(2); @@ -3464,8 +3464,8 @@ describe('dockviewComponent', () => { 'right' ); - expect(panel1.group.api.location).toBe('grid'); - expect(panel2.group.api.location).toBe('grid'); + expect(panel1.group.api.location.type).toBe('grid'); + expect(panel2.group.api.location.type).toBe('grid'); expect(dockview.groups.length).toBe(2); expect(dockview.panels.length).toBe(2); }); @@ -3497,8 +3497,8 @@ describe('dockviewComponent', () => { floating: true, }); - expect(panel1.group.api.location).toBe('grid'); - expect(panel2.group.api.location).toBe('floating'); + expect(panel1.group.api.location.type).toBe('grid'); + expect(panel2.group.api.location.type).toBe('floating'); expect(dockview.groups.length).toBe(2); expect(dockview.panels.length).toBe(2); @@ -3509,8 +3509,8 @@ describe('dockviewComponent', () => { 'center' ); - expect(panel1.group.api.location).toBe('grid'); - expect(panel2.group.api.location).toBe('grid'); + expect(panel1.group.api.location.type).toBe('grid'); + expect(panel2.group.api.location.type).toBe('grid'); expect(dockview.groups.length).toBe(1); expect(dockview.panels.length).toBe(2); }); @@ -3548,9 +3548,9 @@ describe('dockviewComponent', () => { floating: true, }); - expect(panel1.group.api.location).toBe('grid'); - expect(panel2.group.api.location).toBe('floating'); - expect(panel3.group.api.location).toBe('floating'); + expect(panel1.group.api.location.type).toBe('grid'); + expect(panel2.group.api.location.type).toBe('floating'); + expect(panel3.group.api.location.type).toBe('floating'); expect(dockview.groups.length).toBe(3); expect(dockview.panels.length).toBe(3); @@ -3561,9 +3561,9 @@ describe('dockviewComponent', () => { 'center' ); - expect(panel1.group.api.location).toBe('grid'); - expect(panel2.group.api.location).toBe('floating'); - expect(panel3.group.api.location).toBe('floating'); + expect(panel1.group.api.location.type).toBe('grid'); + expect(panel2.group.api.location.type).toBe('floating'); + expect(panel3.group.api.location.type).toBe('floating'); expect(dockview.groups.length).toBe(2); expect(dockview.panels.length).toBe(3); }); @@ -3601,9 +3601,9 @@ describe('dockviewComponent', () => { position: { referencePanel: panel2 }, }); - expect(panel1.group.api.location).toBe('grid'); - expect(panel2.group.api.location).toBe('floating'); - expect(panel3.group.api.location).toBe('floating'); + expect(panel1.group.api.location.type).toBe('grid'); + expect(panel2.group.api.location.type).toBe('floating'); + expect(panel3.group.api.location.type).toBe('floating'); expect(dockview.groups.length).toBe(2); expect(dockview.panels.length).toBe(3); @@ -3614,9 +3614,9 @@ describe('dockviewComponent', () => { 'right' ); - expect(panel1.group.api.location).toBe('grid'); - expect(panel2.group.api.location).toBe('grid'); - expect(panel3.group.api.location).toBe('grid'); + expect(panel1.group.api.location.type).toBe('grid'); + expect(panel2.group.api.location.type).toBe('grid'); + expect(panel3.group.api.location.type).toBe('grid'); expect(dockview.groups.length).toBe(2); expect(dockview.panels.length).toBe(3); }); @@ -3654,9 +3654,9 @@ describe('dockviewComponent', () => { position: { referencePanel: panel2 }, }); - expect(panel1.group.api.location).toBe('grid'); - expect(panel2.group.api.location).toBe('floating'); - expect(panel3.group.api.location).toBe('floating'); + expect(panel1.group.api.location.type).toBe('grid'); + expect(panel2.group.api.location.type).toBe('floating'); + expect(panel3.group.api.location.type).toBe('floating'); expect(dockview.groups.length).toBe(2); expect(dockview.panels.length).toBe(3); @@ -3667,9 +3667,9 @@ describe('dockviewComponent', () => { 'center' ); - expect(panel1.group.api.location).toBe('grid'); - expect(panel2.group.api.location).toBe('grid'); - expect(panel3.group.api.location).toBe('grid'); + expect(panel1.group.api.location.type).toBe('grid'); + expect(panel2.group.api.location.type).toBe('grid'); + expect(panel3.group.api.location.type).toBe('grid'); expect(dockview.groups.length).toBe(1); expect(dockview.panels.length).toBe(3); }); @@ -3713,10 +3713,10 @@ describe('dockviewComponent', () => { floating: true, }); - expect(panel1.group.api.location).toBe('grid'); - expect(panel2.group.api.location).toBe('floating'); - expect(panel3.group.api.location).toBe('floating'); - expect(panel4.group.api.location).toBe('floating'); + expect(panel1.group.api.location.type).toBe('grid'); + expect(panel2.group.api.location.type).toBe('floating'); + expect(panel3.group.api.location.type).toBe('floating'); + expect(panel4.group.api.location.type).toBe('floating'); expect(dockview.groups.length).toBe(3); expect(dockview.panels.length).toBe(4); @@ -3727,10 +3727,10 @@ describe('dockviewComponent', () => { 'center' ); - expect(panel1.group.api.location).toBe('grid'); - expect(panel2.group.api.location).toBe('floating'); - expect(panel3.group.api.location).toBe('floating'); - expect(panel4.group.api.location).toBe('floating'); + expect(panel1.group.api.location.type).toBe('grid'); + expect(panel2.group.api.location.type).toBe('floating'); + expect(panel3.group.api.location.type).toBe('floating'); + expect(panel4.group.api.location.type).toBe('floating'); expect(dockview.groups.length).toBe(2); expect(dockview.panels.length).toBe(4); }); @@ -3762,8 +3762,8 @@ describe('dockviewComponent', () => { floating: true, }); - expect(panel1.group.api.location).toBe('grid'); - expect(panel2.group.api.location).toBe('floating'); + expect(panel1.group.api.location.type).toBe('grid'); + expect(panel2.group.api.location.type).toBe('floating'); expect(dockview.groups.length).toBe(2); expect(dockview.panels.length).toBe(2); @@ -3774,8 +3774,8 @@ describe('dockviewComponent', () => { 'right' ); - expect(panel1.group.api.location).toBe('grid'); - expect(panel2.group.api.location).toBe('grid'); + expect(panel1.group.api.location.type).toBe('grid'); + expect(panel2.group.api.location.type).toBe('grid'); expect(dockview.groups.length).toBe(2); expect(dockview.panels.length).toBe(2); }); @@ -3807,8 +3807,8 @@ describe('dockviewComponent', () => { floating: true, }); - expect(panel1.group.api.location).toBe('grid'); - expect(panel2.group.api.location).toBe('floating'); + expect(panel1.group.api.location.type).toBe('grid'); + expect(panel2.group.api.location.type).toBe('floating'); expect(dockview.groups.length).toBe(2); expect(dockview.panels.length).toBe(2); @@ -3819,8 +3819,8 @@ describe('dockviewComponent', () => { 'center' ); - expect(panel1.group.api.location).toBe('grid'); - expect(panel2.group.api.location).toBe('grid'); + expect(panel1.group.api.location.type).toBe('grid'); + expect(panel2.group.api.location.type).toBe('grid'); expect(dockview.groups.length).toBe(1); expect(dockview.panels.length).toBe(2); }); @@ -3858,9 +3858,9 @@ describe('dockviewComponent', () => { floating: true, }); - expect(panel1.group.api.location).toBe('grid'); - expect(panel2.group.api.location).toBe('floating'); - expect(panel3.group.api.location).toBe('floating'); + expect(panel1.group.api.location.type).toBe('grid'); + expect(panel2.group.api.location.type).toBe('floating'); + expect(panel3.group.api.location.type).toBe('floating'); expect(dockview.groups.length).toBe(3); expect(dockview.panels.length).toBe(3); @@ -3871,9 +3871,9 @@ describe('dockviewComponent', () => { 'center' ); - expect(panel1.group.api.location).toBe('grid'); - expect(panel2.group.api.location).toBe('floating'); - expect(panel3.group.api.location).toBe('floating'); + expect(panel1.group.api.location.type).toBe('grid'); + expect(panel2.group.api.location.type).toBe('floating'); + expect(panel3.group.api.location.type).toBe('floating'); expect(dockview.groups.length).toBe(2); expect(dockview.panels.length).toBe(3); }); @@ -3911,9 +3911,9 @@ describe('dockviewComponent', () => { position: { referencePanel: panel2 }, }); - expect(panel1.group.api.location).toBe('grid'); - expect(panel2.group.api.location).toBe('floating'); - expect(panel3.group.api.location).toBe('floating'); + expect(panel1.group.api.location.type).toBe('grid'); + expect(panel2.group.api.location.type).toBe('floating'); + expect(panel3.group.api.location.type).toBe('floating'); expect(dockview.groups.length).toBe(2); expect(dockview.panels.length).toBe(3); @@ -3924,9 +3924,9 @@ describe('dockviewComponent', () => { 'right' ); - expect(panel1.group.api.location).toBe('grid'); - expect(panel2.group.api.location).toBe('grid'); - expect(panel3.group.api.location).toBe('floating'); + expect(panel1.group.api.location.type).toBe('grid'); + expect(panel2.group.api.location.type).toBe('grid'); + expect(panel3.group.api.location.type).toBe('floating'); expect(dockview.groups.length).toBe(3); expect(dockview.panels.length).toBe(3); }); @@ -3964,9 +3964,9 @@ describe('dockviewComponent', () => { position: { referencePanel: panel2 }, }); - expect(panel1.group.api.location).toBe('grid'); - expect(panel2.group.api.location).toBe('floating'); - expect(panel3.group.api.location).toBe('floating'); + expect(panel1.group.api.location.type).toBe('grid'); + expect(panel2.group.api.location.type).toBe('floating'); + expect(panel3.group.api.location.type).toBe('floating'); expect(dockview.groups.length).toBe(2); expect(dockview.panels.length).toBe(3); @@ -3977,9 +3977,9 @@ describe('dockviewComponent', () => { 'center' ); - expect(panel1.group.api.location).toBe('grid'); - expect(panel2.group.api.location).toBe('grid'); - expect(panel3.group.api.location).toBe('floating'); + expect(panel1.group.api.location.type).toBe('grid'); + expect(panel2.group.api.location.type).toBe('grid'); + expect(panel3.group.api.location.type).toBe('floating'); expect(dockview.groups.length).toBe(2); expect(dockview.panels.length).toBe(3); }); @@ -4023,10 +4023,10 @@ describe('dockviewComponent', () => { floating: true, }); - expect(panel1.group.api.location).toBe('grid'); - expect(panel2.group.api.location).toBe('floating'); - expect(panel3.group.api.location).toBe('floating'); - expect(panel4.group.api.location).toBe('floating'); + expect(panel1.group.api.location.type).toBe('grid'); + expect(panel2.group.api.location.type).toBe('floating'); + expect(panel3.group.api.location.type).toBe('floating'); + expect(panel4.group.api.location.type).toBe('floating'); expect(dockview.groups.length).toBe(3); expect(dockview.panels.length).toBe(4); @@ -4037,10 +4037,10 @@ describe('dockviewComponent', () => { 'center' ); - expect(panel1.group.api.location).toBe('grid'); - expect(panel2.group.api.location).toBe('floating'); - expect(panel3.group.api.location).toBe('floating'); - expect(panel4.group.api.location).toBe('floating'); + expect(panel1.group.api.location.type).toBe('grid'); + expect(panel2.group.api.location.type).toBe('floating'); + expect(panel3.group.api.location.type).toBe('floating'); + expect(panel4.group.api.location.type).toBe('floating'); expect(dockview.groups.length).toBe(3); expect(dockview.panels.length).toBe(4); }); @@ -4078,9 +4078,9 @@ describe('dockviewComponent', () => { floating: true, }); - expect(panel1.group.api.location).toBe('grid'); - expect(panel2.group.api.location).toBe('grid'); - expect(panel3.group.api.location).toBe('floating'); + expect(panel1.group.api.location.type).toBe('grid'); + expect(panel2.group.api.location.type).toBe('grid'); + expect(panel3.group.api.location.type).toBe('floating'); expect(dockview.groups.length).toBe(3); expect(dockview.panels.length).toBe(3); @@ -4091,9 +4091,9 @@ describe('dockviewComponent', () => { 'center' ); - expect(panel1.group.api.location).toBe('floating'); - expect(panel2.group.api.location).toBe('grid'); - expect(panel3.group.api.location).toBe('floating'); + expect(panel1.group.api.location.type).toBe('floating'); + expect(panel2.group.api.location.type).toBe('grid'); + expect(panel3.group.api.location.type).toBe('floating'); expect(dockview.groups.length).toBe(2); expect(dockview.panels.length).toBe(3); }); @@ -4130,9 +4130,9 @@ describe('dockviewComponent', () => { floating: true, }); - expect(panel1.group.api.location).toBe('grid'); - expect(panel2.group.api.location).toBe('grid'); - expect(panel3.group.api.location).toBe('floating'); + expect(panel1.group.api.location.type).toBe('grid'); + expect(panel2.group.api.location.type).toBe('grid'); + expect(panel3.group.api.location.type).toBe('floating'); expect(dockview.groups.length).toBe(2); expect(dockview.panels.length).toBe(3); @@ -4143,9 +4143,9 @@ describe('dockviewComponent', () => { 'center' ); - expect(panel1.group.api.location).toBe('floating'); - expect(panel2.group.api.location).toBe('grid'); - expect(panel3.group.api.location).toBe('floating'); + expect(panel1.group.api.location.type).toBe('floating'); + expect(panel2.group.api.location.type).toBe('grid'); + expect(panel3.group.api.location.type).toBe('floating'); expect(dockview.groups.length).toBe(2); expect(dockview.panels.length).toBe(3); }); @@ -4183,9 +4183,9 @@ describe('dockviewComponent', () => { floating: true, }); - expect(panel1.group.api.location).toBe('grid'); - expect(panel2.group.api.location).toBe('grid'); - expect(panel3.group.api.location).toBe('floating'); + expect(panel1.group.api.location.type).toBe('grid'); + expect(panel2.group.api.location.type).toBe('grid'); + expect(panel3.group.api.location.type).toBe('floating'); expect(dockview.groups.length).toBe(3); expect(dockview.panels.length).toBe(3); @@ -4196,9 +4196,9 @@ describe('dockviewComponent', () => { 'center' ); - expect(panel1.group.api.location).toBe('floating'); - expect(panel2.group.api.location).toBe('grid'); - expect(panel3.group.api.location).toBe('floating'); + expect(panel1.group.api.location.type).toBe('floating'); + expect(panel2.group.api.location.type).toBe('grid'); + expect(panel3.group.api.location.type).toBe('floating'); expect(dockview.groups.length).toBe(2); expect(dockview.panels.length).toBe(3); }); @@ -4235,9 +4235,9 @@ describe('dockviewComponent', () => { floating: true, }); - expect(panel1.group.api.location).toBe('grid'); - expect(panel2.group.api.location).toBe('grid'); - expect(panel3.group.api.location).toBe('floating'); + expect(panel1.group.api.location.type).toBe('grid'); + expect(panel2.group.api.location.type).toBe('grid'); + expect(panel3.group.api.location.type).toBe('floating'); expect(dockview.groups.length).toBe(2); expect(dockview.panels.length).toBe(3); @@ -4248,9 +4248,9 @@ describe('dockviewComponent', () => { 'center' ); - expect(panel1.group.api.location).toBe('floating'); - expect(panel2.group.api.location).toBe('floating'); - expect(panel3.group.api.location).toBe('floating'); + expect(panel1.group.api.location.type).toBe('floating'); + expect(panel2.group.api.location.type).toBe('floating'); + expect(panel3.group.api.location.type).toBe('floating'); expect(dockview.groups.length).toBe(1); expect(dockview.panels.length).toBe(3); }); @@ -4282,15 +4282,15 @@ describe('dockviewComponent', () => { position: { direction: 'right' }, }); - expect(panel1.group.api.location).toBe('grid'); - expect(panel2.group.api.location).toBe('grid'); + expect(panel1.group.api.location.type).toBe('grid'); + expect(panel2.group.api.location.type).toBe('grid'); expect(dockview.groups.length).toBe(2); expect(dockview.panels.length).toBe(2); dockview.addFloatingGroup(panel2); - expect(panel1.group.api.location).toBe('grid'); - expect(panel2.group.api.location).toBe('floating'); + expect(panel1.group.api.location.type).toBe('grid'); + expect(panel2.group.api.location.type).toBe('floating'); expect(dockview.groups.length).toBe(2); expect(dockview.panels.length).toBe(2); }); @@ -4321,15 +4321,15 @@ describe('dockviewComponent', () => { component: 'default', }); - expect(panel1.group.api.location).toBe('grid'); - expect(panel2.group.api.location).toBe('grid'); + expect(panel1.group.api.location.type).toBe('grid'); + expect(panel2.group.api.location.type).toBe('grid'); expect(dockview.groups.length).toBe(1); expect(dockview.panels.length).toBe(2); dockview.addFloatingGroup(panel2); - expect(panel1.group.api.location).toBe('grid'); - expect(panel2.group.api.location).toBe('floating'); + expect(panel1.group.api.location.type).toBe('grid'); + expect(panel2.group.api.location.type).toBe('floating'); expect(dockview.groups.length).toBe(2); expect(dockview.panels.length).toBe(2); }); @@ -4361,15 +4361,15 @@ describe('dockviewComponent', () => { position: { direction: 'right' }, }); - expect(panel1.group.api.location).toBe('grid'); - expect(panel2.group.api.location).toBe('grid'); + expect(panel1.group.api.location.type).toBe('grid'); + expect(panel2.group.api.location.type).toBe('grid'); expect(dockview.groups.length).toBe(2); expect(dockview.panels.length).toBe(2); dockview.addFloatingGroup(panel2.group); - expect(panel1.group.api.location).toBe('grid'); - expect(panel2.group.api.location).toBe('floating'); + expect(panel1.group.api.location.type).toBe('grid'); + expect(panel2.group.api.location.type).toBe('floating'); expect(dockview.groups.length).toBe(2); expect(dockview.panels.length).toBe(2); }); @@ -4400,15 +4400,15 @@ describe('dockviewComponent', () => { component: 'default', }); - expect(panel1.group.api.location).toBe('grid'); - expect(panel2.group.api.location).toBe('grid'); + expect(panel1.group.api.location.type).toBe('grid'); + expect(panel2.group.api.location.type).toBe('grid'); expect(dockview.groups.length).toBe(1); expect(dockview.panels.length).toBe(2); dockview.addFloatingGroup(panel2.group); - expect(panel1.group.api.location).toBe('floating'); - expect(panel2.group.api.location).toBe('floating'); + expect(panel1.group.api.location.type).toBe('floating'); + expect(panel2.group.api.location.type).toBe('floating'); expect(dockview.groups.length).toBe(1); expect(dockview.panels.length).toBe(2); }); @@ -4440,7 +4440,7 @@ describe('dockviewComponent', () => { expect(dockview.panels.length).toBe(1); expect(dockview.groups.length).toBe(1); - expect(panel1.api.group.api.location).toBe('popout'); + expect(panel1.api.group.api.location.type).toBe('popout'); dockview.removePanel(panel1); @@ -4474,15 +4474,15 @@ describe('dockviewComponent', () => { component: 'default', }); - expect(panel1.group.api.location).toBe('grid'); - expect(panel2.group.api.location).toBe('grid'); + expect(panel1.group.api.location.type).toBe('grid'); + expect(panel2.group.api.location.type).toBe('grid'); expect(dockview.groups.length).toBe(1); expect(dockview.panels.length).toBe(2); dockview.addPopoutGroup(panel2.group); - expect(panel1.group.api.location).toBe('popout'); - expect(panel2.group.api.location).toBe('popout'); + expect(panel1.group.api.location.type).toBe('popout'); + expect(panel2.group.api.location.type).toBe('popout'); expect(dockview.groups.length).toBe(1); expect(dockview.panels.length).toBe(2); }); @@ -4521,17 +4521,17 @@ describe('dockviewComponent', () => { }, }); - expect(panel1.group.api.location).toBe('grid'); - expect(panel2.group.api.location).toBe('grid'); - expect(panel3.group.api.location).toBe('grid'); + expect(panel1.group.api.location.type).toBe('grid'); + expect(panel2.group.api.location.type).toBe('grid'); + expect(panel3.group.api.location.type).toBe('grid'); expect(dockview.groups.length).toBe(2); expect(dockview.panels.length).toBe(3); dockview.addPopoutGroup(panel2.group); - expect(panel1.group.api.location).toBe('popout'); - expect(panel2.group.api.location).toBe('popout'); - expect(panel3.group.api.location).toBe('grid'); + expect(panel1.group.api.location.type).toBe('popout'); + expect(panel2.group.api.location.type).toBe('popout'); + expect(panel3.group.api.location.type).toBe('grid'); expect(dockview.groups.length).toBe(2); expect(dockview.panels.length).toBe(3); @@ -4542,9 +4542,9 @@ describe('dockviewComponent', () => { 'right' ); - expect(panel1.group.api.location).toBe('popout'); - expect(panel2.group.api.location).toBe('grid'); - expect(panel3.group.api.location).toBe('grid'); + expect(panel1.group.api.location.type).toBe('popout'); + expect(panel2.group.api.location.type).toBe('grid'); + expect(panel3.group.api.location.type).toBe('grid'); expect(dockview.groups.length).toBe(3); expect(dockview.panels.length).toBe(3); }); diff --git a/packages/dockview-core/src/__tests__/overlayRenderContainer.spec.ts b/packages/dockview-core/src/__tests__/overlayRenderContainer.spec.ts index db61d319a..81310060b 100644 --- a/packages/dockview-core/src/__tests__/overlayRenderContainer.spec.ts +++ b/packages/dockview-core/src/__tests__/overlayRenderContainer.spec.ts @@ -41,7 +41,7 @@ describe('overlayRenderContainer', () => { }, group: { api: { - location: 'grid', + location: { type: 'grid' }, }, }, }); @@ -77,7 +77,7 @@ describe('overlayRenderContainer', () => { }, group: { api: { - location: 'grid', + location: { type: 'grid' }, }, }, }); diff --git a/packages/dockview-core/src/api/dockviewGroupPanelApi.ts b/packages/dockview-core/src/api/dockviewGroupPanelApi.ts index c4b349bdf..1a999301d 100644 --- a/packages/dockview-core/src/api/dockviewGroupPanelApi.ts +++ b/packages/dockview-core/src/api/dockviewGroupPanelApi.ts @@ -8,6 +8,13 @@ import { GridviewPanelApi, GridviewPanelApiImpl } from './gridviewPanelApi'; export interface DockviewGroupPanelApi extends GridviewPanelApi { readonly onDidLocationChange: Event; readonly location: DockviewGroupLocation; + /** + * + * If you require the documents Window object you can call `document.defaultView`. + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/Document/defaultView + */ + getDocument(): Document; moveTo(options: { group?: DockviewGroupPanel; position?: Position }): void; maximize(): void; isMaximized(): boolean; @@ -42,6 +49,12 @@ export class DockviewGroupPanelApiImpl extends GridviewPanelApiImpl { this.addDisposables(this._onDidLocationChange); } + getDocument(): Document { + return this.location.type === 'popout' + ? this.location.getWindow().document + : window.document; + } + moveTo(options: { group?: DockviewGroupPanel; position?: Position }): void { if (!this._group) { throw new Error(NOT_INITIALIZED_MESSAGE); @@ -66,7 +79,7 @@ export class DockviewGroupPanelApiImpl extends GridviewPanelApiImpl { throw new Error(NOT_INITIALIZED_MESSAGE); } - if (this.location !== 'grid') { + if (this.location.type !== 'grid') { // only grid groups can be maximized return; } diff --git a/packages/dockview-core/src/api/dockviewPanelApi.ts b/packages/dockview-core/src/api/dockviewPanelApi.ts index 6ef8d824a..dffca9297 100644 --- a/packages/dockview-core/src/api/dockviewPanelApi.ts +++ b/packages/dockview-core/src/api/dockviewPanelApi.ts @@ -1,11 +1,13 @@ import { Emitter, Event } from '../events'; import { GridviewPanelApiImpl, GridviewPanelApi } from './gridviewPanelApi'; import { DockviewGroupPanel } from '../dockview/dockviewGroupPanel'; -import { MutableDisposable } from '../lifecycle'; +import { CompositeDisposable, MutableDisposable } from '../lifecycle'; import { DockviewPanel } from '../dockview/dockviewPanel'; import { DockviewComponent } from '../dockview/dockviewComponent'; import { Position } from '../dnd/droptarget'; import { DockviewPanelRenderer } from '../overlayRenderContainer'; +import { DockviewGroupPanelFloatingChangeEvent } from './dockviewGroupPanelApi'; +import { DockviewGroupLocation } from '../dockview/dockviewGroupPanelModel'; export interface TitleEvent { readonly title: string; @@ -28,6 +30,8 @@ export interface DockviewPanelApi readonly onDidActiveGroupChange: Event; readonly onDidGroupChange: Event; readonly onDidRendererChange: Event; + readonly location: DockviewGroupLocation; + readonly onDidLocationChange: Event; close(): void; setTitle(title: string): void; setRenderer(renderer: DockviewPanelRenderer): void; @@ -39,6 +43,13 @@ export interface DockviewPanelApi maximize(): void; isMaximized(): boolean; exitMaximized(): void; + /** + * + * If you require the documents Window object you can call `document.defaultView`. + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/Document/defaultView + */ + getDocument(): Document; } export class DockviewPanelApiImpl @@ -59,7 +70,16 @@ export class DockviewPanelApiImpl readonly _onDidRendererChange = new Emitter(); readonly onDidRendererChange = this._onDidRendererChange.event; - private readonly disposable = new MutableDisposable(); + private readonly _onDidLocationChange = + new Emitter(); + readonly onDidLocationChange: Event = + this._onDidLocationChange.event; + + private readonly groupEventsDisposable = new MutableDisposable(); + + get location(): DockviewGroupLocation { + return this.group.api.location; + } get title(): string | undefined { return this.panel.title; @@ -81,13 +101,22 @@ export class DockviewPanelApiImpl this._onDidGroupChange.fire(); if (this._group) { - this.disposable.value = this._group.api.onDidActiveChange(() => { - this._onDidActiveGroupChange.fire(); - }); + this.groupEventsDisposable.value = new CompositeDisposable( + this.group.api.onDidLocationChange((event) => { + this._onDidLocationChange.fire(event); + }), + this.group.api.onDidActiveChange(() => { + this._onDidActiveGroupChange.fire(); + }) + ); if (this.isGroupActive !== isOldGroupActive) { this._onDidActiveGroupChange.fire(); } + + this._onDidLocationChange.fire({ + location: this.group.api.location, + }); } } @@ -107,14 +136,19 @@ export class DockviewPanelApiImpl this._group = group; this.addDisposables( - this.disposable, + this.groupEventsDisposable, this._onDidRendererChange, this._onDidTitleChange, this._onDidGroupChange, - this._onDidActiveGroupChange + this._onDidActiveGroupChange, + this._onDidLocationChange ); } + getDocument(): Document { + return this.group.api.getDocument(); + } + moveTo(options: { group: DockviewGroupPanel; position?: Position; diff --git a/packages/dockview-core/src/dnd/groupDragHandler.ts b/packages/dockview-core/src/dnd/groupDragHandler.ts index de4a3ef04..7138437bf 100644 --- a/packages/dockview-core/src/dnd/groupDragHandler.ts +++ b/packages/dockview-core/src/dnd/groupDragHandler.ts @@ -38,7 +38,7 @@ export class GroupDragHandler extends DragHandler { } override isCancelled(_event: DragEvent): boolean { - if (this.group.api.location === 'floating' && !_event.shiftKey) { + if (this.group.api.location.type === 'floating' && !_event.shiftKey) { return true; } return false; diff --git a/packages/dockview-core/src/dockview/components/panel/content.ts b/packages/dockview-core/src/dockview/components/panel/content.ts index b98b12289..dd762873a 100644 --- a/packages/dockview-core/src/dockview/components/panel/content.ts +++ b/packages/dockview-core/src/dockview/components/panel/content.ts @@ -71,7 +71,7 @@ export class ContentContainer if ( !data && event.shiftKey && - this.group.location !== 'floating' + this.group.location.type !== 'floating' ) { return false; } diff --git a/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts b/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts index 123608573..6a7caac29 100644 --- a/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts +++ b/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts @@ -247,7 +247,7 @@ export class TabsContainer if ( isFloatingGroupsEnabled && event.shiftKey && - this.group.api.location !== 'floating' + this.group.api.location.type !== 'floating' ) { event.preventDefault(); @@ -350,7 +350,7 @@ export class TabsContainer !this.accessor.options.disableFloatingGroups; const isFloatingWithOnePanel = - this.group.api.location === 'floating' && this.size === 1; + this.group.api.location.type === 'floating' && this.size === 1; if ( isFloatingGroupsEnabled && diff --git a/packages/dockview-core/src/dockview/dockviewComponent.ts b/packages/dockview-core/src/dockview/dockviewComponent.ts index 7595d723a..a46177a95 100644 --- a/packages/dockview-core/src/dockview/dockviewComponent.ts +++ b/packages/dockview-core/src/dockview/dockviewComponent.ts @@ -67,6 +67,7 @@ import { DockviewPanelRenderer, OverlayRenderContainer, } from '../overlayRenderContainer'; +import { PopoutWindow } from '../popoutWindow'; const DEFAULT_ROOT_OVERLAY_MODEL: DroptargetOverlayModel = { activationSize: { type: 'pixels', value: 10 }, @@ -608,7 +609,7 @@ export class DockviewComponent } } - group.model.location = 'floating'; + group.model.location = { type: 'floating' }; const overlayLeft = typeof coord?.x === 'number' @@ -683,7 +684,7 @@ export class DockviewComponent dispose: () => { disposable.dispose(); - group.model.location = 'grid'; + group.model.location = { type: 'grid' }; remove(this._floatingGroups, floatingGroupPanel); this.updateWatermark(); }, @@ -1173,7 +1174,7 @@ export class DockviewComponent group.model.openPanel(panel); this.doSetGroupAndPanelActive(group); } else if ( - referenceGroup.api.location === 'floating' || + referenceGroup.api.location.type === 'floating' || target === 'center' ) { panel = this.createPanel(options, referenceGroup); @@ -1259,7 +1260,10 @@ export class DockviewComponent } private updateWatermark(): void { - if (this.groups.filter((x) => x.api.location === 'grid').length === 0) { + if ( + this.groups.filter((x) => x.api.location.type === 'grid').length === + 0 + ) { if (!this.watermark) { this.watermark = this.createWatermarkComponent(); @@ -1377,7 +1381,7 @@ export class DockviewComponent } | undefined ): DockviewGroupPanel { - if (group.api.location === 'floating') { + if (group.api.location.type === 'floating') { const floatingGroup = this._floatingGroups.find( (_) => _.group === group ); @@ -1406,7 +1410,7 @@ export class DockviewComponent throw new Error('failed to find floating group'); } - if (group.api.location === 'popout') { + if (group.api.location.type === 'popout') { const selectedGroup = this._popoutGroups.find( (_) => _.group === group ); @@ -1486,7 +1490,7 @@ export class DockviewComponent if (sourceGroup && sourceGroup.size < 2) { const [targetParentLocation, to] = tail(targetLocation); - if (sourceGroup.api.location === 'grid') { + if (sourceGroup.api.location.type === 'grid') { const sourceLocation = getGridLocation(sourceGroup.element); const [sourceParentLocation, from] = tail(sourceLocation); @@ -1562,7 +1566,7 @@ export class DockviewComponent }); } } else { - switch (sourceGroup.api.location) { + switch (sourceGroup.api.location.type) { case 'grid': this.gridview.removeView( getGridLocation(sourceGroup.element) diff --git a/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts b/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts index 4462add04..a80a7baa0 100644 --- a/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts +++ b/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts @@ -130,7 +130,10 @@ export interface IDockviewGroupPanelModel extends IPanel { ): boolean; } -export type DockviewGroupLocation = 'grid' | 'floating' | 'popout'; +export type DockviewGroupLocation = + | { type: 'grid' } + | { type: 'floating' } + | { type: 'popout'; getWindow: () => Window }; export class DockviewGroupPanelModel extends CompositeDisposable @@ -146,7 +149,7 @@ export class DockviewGroupPanelModel private _leftHeaderActions: IHeaderActionsRenderer | undefined; private _prefixHeaderActions: IHeaderActionsRenderer | undefined; - private _location: DockviewGroupLocation = 'grid'; + private _location: DockviewGroupLocation = { type: 'grid' }; private mostRecentlyUsed: IDockviewPanel[] = []; @@ -253,7 +256,7 @@ export class DockviewGroupPanelModel toggleClass(this.container, 'dv-groupview-floating', false); toggleClass(this.container, 'dv-groupview-popout', false); - switch (value) { + switch (value.type) { case 'grid': this.contentContainer.dropTarget.setTargetZones([ 'top', diff --git a/packages/dockview-core/src/dockview/dockviewPopoutGroupPanel.ts b/packages/dockview-core/src/dockview/dockviewPopoutGroupPanel.ts index 803fa5411..c95fdce1f 100644 --- a/packages/dockview-core/src/dockview/dockviewPopoutGroupPanel.ts +++ b/packages/dockview-core/src/dockview/dockviewPopoutGroupPanel.ts @@ -25,13 +25,16 @@ export class DockviewPopoutGroupPanel extends CompositeDisposable { height: this.options.box.height, }); - group.model.location = 'popout'; + group.model.location = { + type: 'popout', + getWindow: () => this.window.window!, + }; this.addDisposables( this.window, { dispose: () => { - group.model.location = 'grid'; + group.model.location = { type: 'grid' }; }, }, this.window.onDidClose(() => { diff --git a/packages/dockview-core/src/index.ts b/packages/dockview-core/src/index.ts index 62d415174..3f5c8bf70 100644 --- a/packages/dockview-core/src/index.ts +++ b/packages/dockview-core/src/index.ts @@ -11,6 +11,8 @@ export { CompositeDisposable as DockviewCompositeDisposable, } from './lifecycle'; +export { PopoutWindow } from './popoutWindow'; + export * from './panel/types'; export * from './panel/componentFactory'; diff --git a/packages/dockview-core/src/overlayRenderContainer.ts b/packages/dockview-core/src/overlayRenderContainer.ts index 5fea8cee0..62095e595 100644 --- a/packages/dockview-core/src/overlayRenderContainer.ts +++ b/packages/dockview-core/src/overlayRenderContainer.ts @@ -93,7 +93,7 @@ export class OverlayRenderContainer extends CompositeDisposable { toggleClass( focusContainer, 'dv-render-overlay-float', - panel.group.api.location === 'floating' + panel.group.api.location.type === 'floating' ); }; diff --git a/packages/dockview-core/src/popoutWindow.ts b/packages/dockview-core/src/popoutWindow.ts index c73334549..672a6d4e5 100644 --- a/packages/dockview-core/src/popoutWindow.ts +++ b/packages/dockview-core/src/popoutWindow.ts @@ -8,19 +8,26 @@ export type PopoutWindowOptions = { } & Box; export class PopoutWindow extends CompositeDisposable { + private readonly _onWillClose = new Emitter(); + readonly onWillClose = this._onWillClose.event; + private readonly _onDidClose = new Emitter(); readonly onDidClose = this._onDidClose.event; private _window: { value: Window; disposable: IDisposable } | null = null; + get window(): Window | null { + return this._window?.value ?? null; + } + constructor( - private readonly id: string, + private readonly target: string, private readonly className: string, private readonly options: PopoutWindowOptions ) { super(); - this.addDisposables(this._onDidClose, { + this.addDisposables(this._onWillClose, this._onDidClose, { dispose: () => { this.close(); }, @@ -42,9 +49,13 @@ export class PopoutWindow extends CompositeDisposable { close(): void { if (this._window) { + this._onWillClose.fire(); + this._window.disposable.dispose(); this._window.value.close(); this._window = null; + + this._onDidClose.fire(); } } @@ -64,8 +75,10 @@ export class PopoutWindow extends CompositeDisposable { .map(([key, value]) => `${key}=${value}`) .join(','); - // https://developer.mozilla.org/en-US/docs/Web/API/Window/open - const externalWindow = window.open(url, this.id, features); + /** + * @see https://developer.mozilla.org/en-US/docs/Web/API/Window/open + */ + const externalWindow = window.open(url, this.target, features); if (!externalWindow) { return; @@ -75,44 +88,55 @@ export class PopoutWindow extends CompositeDisposable { this._window = { value: externalWindow, disposable }; - const cleanUp = () => { - this._onDidClose.fire(); - this._window = null; - }; - - // prevent any default content from loading - // externalWindow.document.body.replaceWith(document.createElement('div')); - disposable.addDisposables( addDisposableWindowListener(window, 'beforeunload', () => { - cleanUp(); + /** + * before the main window closes we should close this popup too + * to be good citizens + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event + */ this.close(); }) ); externalWindow.addEventListener('load', () => { + /** + * @see https://developer.mozilla.org/en-US/docs/Web/API/Window/load_event + */ + const externalDocument = externalWindow.document; externalDocument.title = document.title; - const div = document.createElement('div'); - div.classList.add('dv-popout-window'); - div.style.position = 'absolute'; - div.style.width = '100%'; - div.style.height = '100%'; - div.style.top = '0px'; - div.style.left = '0px'; - div.classList.add(this.className); - div.appendChild(content); + const container = this.createPopoutWindowContainer(); + container.classList.add(this.className); + container.appendChild(content); - externalDocument.body.replaceChildren(div); + // externalDocument.body.replaceChildren(container); + externalDocument.body.appendChild(container); externalDocument.body.classList.add(this.className); addStyles(externalDocument, window.document.styleSheets); externalWindow.addEventListener('beforeunload', () => { - // TODO: indicate external window is closing - cleanUp(); + /** + * @see https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event + */ + this.close(); }); }); } + + private createPopoutWindowContainer(): HTMLElement { + const el = document.createElement('div'); + el.classList.add('dv-popout-window'); + el.id = 'dv-popout-window'; + el.style.position = 'absolute'; + el.style.width = '100%'; + el.style.height = '100%'; + el.style.top = '0px'; + el.style.left = '0px'; + + return el; + } } diff --git a/yarn.lock b/yarn.lock index 7844d2be6..448c87069 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13882,6 +13882,13 @@ react-json-view-lite@^1.2.0: resolved "https://registry.yarnpkg.com/react-json-view-lite/-/react-json-view-lite-1.2.1.tgz#c59a0bea4ede394db331d482ee02e293d38f8218" integrity sha512-Itc0g86fytOmKZoIoJyGgvNqohWSbh3NXIKNgH6W6FT9PC1ck4xas1tT3Rr/b3UlFXyA9Jjaw9QSXdZy2JwGMQ== +react-laag@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/react-laag/-/react-laag-2.0.5.tgz#549f1035b761b9ba09ac98fd128ccad63464c877" + integrity sha512-RCvublJhdcgGRHU1wMYJ8kRtnYsKUgYusLvVhMuftg65POnnOB4+fwXvnETm6adc0cMnc1spujlrK6bGIz6aug== + dependencies: + tiny-warning "^1.0.3" + react-loadable-ssr-addon-v5-slorber@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/react-loadable-ssr-addon-v5-slorber/-/react-loadable-ssr-addon-v5-slorber-1.0.1.tgz#2cdc91e8a744ffdf9e3556caabeb6e4278689883" @@ -15678,7 +15685,7 @@ tiny-invariant@^1.0.2: resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.1.tgz#8560808c916ef02ecfd55e66090df23a4b7aa642" integrity sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw== -tiny-warning@^1.0.0: +tiny-warning@^1.0.0, tiny-warning@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754" integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== From 0fd3a669c7ea27dffef622e125be73208c9a1353 Mon Sep 17 00:00:00 2001 From: mathuo <6710312+mathuo@users.noreply.github.com> Date: Sat, 27 Jan 2024 19:21:09 +0000 Subject: [PATCH 06/19] test --- .../docs/sandboxes/demo-dockview/src/app.tsx | 4 +- .../floatinggroup-dockview/src/app.tsx | 4 +- .../popoutgroup-dockview/package.json | 5 +- .../popoutgroup-dockview/src/app.tsx | 99 ++++++++++++++----- .../popoutgroup-dockview/src/popover.tsx | 61 ++++++++++++ 5 files changed, 144 insertions(+), 29 deletions(-) create mode 100644 packages/docs/sandboxes/popoutgroup-dockview/src/popover.tsx diff --git a/packages/docs/sandboxes/demo-dockview/src/app.tsx b/packages/docs/sandboxes/demo-dockview/src/app.tsx index 79885e0bf..975f3f6be 100644 --- a/packages/docs/sandboxes/demo-dockview/src/app.tsx +++ b/packages/docs/sandboxes/demo-dockview/src/app.tsx @@ -87,7 +87,7 @@ const RightControls = (props: IDockviewHeaderActionsProps) => { ); const [isPopout, setIsPopout] = React.useState( - props.api.location === 'popout' + props.api.location.type === 'popout' ); React.useEffect(() => { @@ -96,7 +96,7 @@ const RightControls = (props: IDockviewHeaderActionsProps) => { }); const disposable2 = props.api.onDidLocationChange(() => { - setIsPopout(props.api.location === 'popout'); + setIsPopout(props.api.location.type === 'popout'); }); return () => { diff --git a/packages/docs/sandboxes/floatinggroup-dockview/src/app.tsx b/packages/docs/sandboxes/floatinggroup-dockview/src/app.tsx index e3c066bbd..c90e32cec 100644 --- a/packages/docs/sandboxes/floatinggroup-dockview/src/app.tsx +++ b/packages/docs/sandboxes/floatinggroup-dockview/src/app.tsx @@ -255,13 +255,13 @@ const LeftComponent = (props: IDockviewHeaderActionsProps) => { const RightComponent = (props: IDockviewHeaderActionsProps) => { const [floating, setFloating] = React.useState( - props.api.location === 'floating' + props.api.location.type === 'floating' ); React.useEffect(() => { const disposable = props.group.api.onDidLocationChange( (event) => { - setFloating(event.location === 'floating'); + setFloating(event.location.type === 'floating'); } ); diff --git a/packages/docs/sandboxes/popoutgroup-dockview/package.json b/packages/docs/sandboxes/popoutgroup-dockview/package.json index 9a533bff7..9b8601416 100644 --- a/packages/docs/sandboxes/popoutgroup-dockview/package.json +++ b/packages/docs/sandboxes/popoutgroup-dockview/package.json @@ -9,7 +9,8 @@ "dependencies": { "dockview": "*", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "react-laag": "^2.0.5" }, "devDependencies": { "@types/react": "^18.0.28", @@ -29,4 +30,4 @@ "not ie <= 11", "not op_mini all" ] -} +} \ No newline at end of file diff --git a/packages/docs/sandboxes/popoutgroup-dockview/src/app.tsx b/packages/docs/sandboxes/popoutgroup-dockview/src/app.tsx index df732b7c0..57fe7e8be 100644 --- a/packages/docs/sandboxes/popoutgroup-dockview/src/app.tsx +++ b/packages/docs/sandboxes/popoutgroup-dockview/src/app.tsx @@ -5,12 +5,53 @@ import { IDockviewHeaderActionsProps, IDockviewPanelProps, SerializedDockview, + DockviewPanelApi, + DockviewGroupLocation, } from 'dockview'; import * as React from 'react'; +import * as ReactDOM from 'react-dom'; import { Icon } from './utils'; +import { PopoverMenu } from './popover'; + +function usePopoutWindowContext(api: DockviewPanelApi): Window { + const [location, setLocation] = React.useState( + api.location + ); + + React.useEffect(() => { + const disposable = api.onDidLocationChange((event) => { + setLocation(event.location); + }); + + return () => { + disposable.dispose(); + }; + }); + + const windowContext = React.useMemo(() => { + if (location.type === 'popout') { + return location.getWindow(); + } + return window; + }, [location]); + + return windowContext; +} const components = { default: (props: IDockviewPanelProps<{ title: string }>) => { + const windowContext = usePopoutWindowContext(props.api); + + React.useEffect(() => { + setTimeout(() => { + const a = windowContext.document.createElement('div'); + a.className = 'aaa'; + windowContext.document.body.appendChild(a); + }, 5000); + }, [windowContext]); + + const [reset, setReset] = React.useState(false); + return (

- {props.params.title} + + {!reset && } + {props.api.title}
); }, @@ -31,31 +84,31 @@ function loadDefaultLayout(api: DockviewApi) { component: 'default', }); - api.addPanel({ - id: 'panel_2', - component: 'default', - }); + // api.addPanel({ + // id: 'panel_2', + // component: 'default', + // }); - api.addPanel({ - id: 'panel_3', - component: 'default', - }); + // api.addPanel({ + // id: 'panel_3', + // component: 'default', + // }); - api.addPanel({ - id: 'panel_4', - component: 'default', - }); + // api.addPanel({ + // id: 'panel_4', + // component: 'default', + // }); - api.addPanel({ - id: 'panel_5', - component: 'default', - position: { direction: 'right' }, - }); + // api.addPanel({ + // id: 'panel_5', + // component: 'default', + // position: { direction: 'right' }, + // }); - api.addPanel({ - id: 'panel_6', - component: 'default', - }); + // api.addPanel({ + // id: 'panel_6', + // component: 'default', + // }); } let panelCount = 0; @@ -223,7 +276,7 @@ const RightComponent = (props: IDockviewHeaderActionsProps) => { const group = props.containerApi.addGroup(); props.group.api.moveTo({ group }); } else { - props.containerApi.addPopoutGroup(props.group, { + const window = props.containerApi.addPopoutGroup(props.group, { popoutUrl: '/popout/index.html', }); } diff --git a/packages/docs/sandboxes/popoutgroup-dockview/src/popover.tsx b/packages/docs/sandboxes/popoutgroup-dockview/src/popover.tsx new file mode 100644 index 000000000..3b0b8d1f4 --- /dev/null +++ b/packages/docs/sandboxes/popoutgroup-dockview/src/popover.tsx @@ -0,0 +1,61 @@ +import { useLayer, Arrow } from 'react-laag'; +import { motion, AnimatePresence } from 'framer-motion'; +import * as React from 'react'; +import { DockviewPanelApi } from 'dockview'; + +export function PopoverMenu(props: { api: DockviewPanelApi }) { + const [isOpen, setOpen] = React.useState(false); + + // helper function to close the menu + function close() { + setOpen(false); + } + + const _window = + props.api.location.type === 'popout' + ? props.api.location.getWindow() + : undefined; + + const { renderLayer, triggerProps, layerProps, arrowProps } = useLayer({ + isOpen, + onOutsideClick: close, // close the menu when the user clicks outside + onDisappear: close, // close the menu when the menu gets scrolled out of sight + overflowContainer: false, // keep the menu positioned inside the container + auto: true, // automatically find the best placement + placement: 'top-end', // we prefer to place the menu "top-end" + triggerOffset: 12, // keep some distance to the trigger + containerOffset: 16, // give the menu some room to breath relative to the container + arrowOffset: 16, // let the arrow have some room to breath also, + environment: _window, + container: _window + ? () => { + const el = _window.document.body; + Object.setPrototypeOf(el, HTMLElement.prototype); + return el; + } + : undefined, + // container: props.window.document.body + }); + + // Again, we're using framer-motion for the transition effect + return ( + <> + + {renderLayer( + + {isOpen && ( + +
  • Item 1
  • +
  • Item 2
  • +
  • Item 3
  • +
  • Item 4
  • + +
    + )} +
    + )} + + ); +} From 6274708acb8981dce02da39c16f4545676348c6d Mon Sep 17 00:00:00 2001 From: mathuo <6710312+mathuo@users.noreply.github.com> Date: Sun, 28 Jan 2024 13:43:09 +0000 Subject: [PATCH 07/19] feat: align event names --- packages/dockview-core/src/api/component.api.ts | 4 ++-- .../src/dockview/dockviewComponent.ts | 12 ++++++------ .../src/dockview/dockviewPopoutGroupPanel.ts | 8 ++++---- packages/dockview-core/src/popoutWindow.ts | 15 +++++++-------- 4 files changed, 19 insertions(+), 20 deletions(-) diff --git a/packages/dockview-core/src/api/component.api.ts b/packages/dockview-core/src/api/component.api.ts index 02f4292a7..f57fa9fa4 100644 --- a/packages/dockview-core/src/api/component.api.ts +++ b/packages/dockview-core/src/api/component.api.ts @@ -830,8 +830,8 @@ export class DockviewApi implements CommonApi { options?: { position?: Box; popoutUrl?: string; - onOpened?: (id: string, window: Window) => void; - onClosing?: (id: string, window: Window) => void; + onDidOpen?: (event: { id: string; window: Window }) => void; + onWillClose?: (event: { id: string; window: Window }) => void; } ): void { this.component.addPopoutGroup(item, options); diff --git a/packages/dockview-core/src/dockview/dockviewComponent.ts b/packages/dockview-core/src/dockview/dockviewComponent.ts index b28dde011..e808bdb79 100644 --- a/packages/dockview-core/src/dockview/dockviewComponent.ts +++ b/packages/dockview-core/src/dockview/dockviewComponent.ts @@ -287,8 +287,8 @@ export interface IDockviewComponent extends IBaseGrid { options?: { position?: Box; popoutUrl?: string; - onOpened?: (id: string, window: Window) => void; - onClosing?: (id: string, window: Window) => void; + onDidOpen?: (event: { id: string; window: Window }) => void; + onWillClose?: (event: { id: string; window: Window }) => void; } ): void; } @@ -516,8 +516,8 @@ export class DockviewComponent skipRemoveGroup?: boolean; position?: Box; popoutUrl?: string; - onOpened?: (id: string, window: Window) => void; - onClosing?: (id: string, window: Window) => void; + onDidOpen?: (event: { id: string; window: Window }) => void; + onWillClose?: (event: { id: string; window: Window }) => void; } ): void { let group: DockviewGroupPanel; @@ -566,8 +566,8 @@ export class DockviewComponent width: box.width, height: box.height, }, - onOpened: options?.onOpened, - onClosing: options?.onClosing + onDidOpen: options?.onDidOpen, + onWillClose: options?.onWillClose, } ); diff --git a/packages/dockview-core/src/dockview/dockviewPopoutGroupPanel.ts b/packages/dockview-core/src/dockview/dockviewPopoutGroupPanel.ts index 371fda793..858178280 100644 --- a/packages/dockview-core/src/dockview/dockviewPopoutGroupPanel.ts +++ b/packages/dockview-core/src/dockview/dockviewPopoutGroupPanel.ts @@ -13,8 +13,8 @@ export class DockviewPopoutGroupPanel extends CompositeDisposable { className: string; popoutUrl: string; box: Box; - onOpened?: (id: string, window: Window) => void; - onClosing?: (id: string, window: Window) => void; + onDidOpen?: (event: { id: string; window: Window }) => void; + onWillClose?: (event: { id: string; window: Window }) => void; } ) { super(); @@ -25,8 +25,8 @@ export class DockviewPopoutGroupPanel extends CompositeDisposable { top: this.options.box.top, width: this.options.box.width, height: this.options.box.height, - onOpened: this.options.onOpened, - onClosing: this.options.onClosing, + onDidOpen: this.options.onDidOpen, + onWillClose: this.options.onWillClose, }); group.model.location = { diff --git a/packages/dockview-core/src/popoutWindow.ts b/packages/dockview-core/src/popoutWindow.ts index 68dfc6736..3f1531cac 100644 --- a/packages/dockview-core/src/popoutWindow.ts +++ b/packages/dockview-core/src/popoutWindow.ts @@ -5,8 +5,8 @@ import { Box } from './types'; export type PopoutWindowOptions = { url: string; - onOpened?: (id: string, window: Window) => void; - onClosing?: (id: string, window: Window) => void; + onDidOpen?: (event: { id: string; window: Window }) => void; + onWillClose?: (event: { id: string; window: Window }) => void; } & Box; export class PopoutWindow extends CompositeDisposable { @@ -53,9 +53,10 @@ export class PopoutWindow extends CompositeDisposable { if (this._window) { this._onWillClose.fire(); - if (this.options.onClosing) { - this.options.onClosing(this.target, this._window.value); - } + this.options.onWillClose?.({ + id: this.target, + window: this._window.value, + }); this._window.disposable.dispose(); this._window.value.close(); @@ -132,9 +133,7 @@ export class PopoutWindow extends CompositeDisposable { }); }); - if (this.options.onOpened) { - this.options.onOpened(this.target, externalWindow); - } + this.options.onDidOpen?.({ id: this.target, window: externalWindow }); } private createPopoutWindowContainer(): HTMLElement { From 0127857544e458c3168f19d9f22926939925cd77 Mon Sep 17 00:00:00 2001 From: mathuo <6710312+mathuo@users.noreply.github.com> Date: Wed, 17 Jan 2024 22:09:30 +0000 Subject: [PATCH 08/19] feat: group gaps --- .../__tests__/api/dockviewPanelApi.spec.ts | 6 ++-- .../components/titlebar/tabsContainer.spec.ts | 34 +++++++++---------- .../dockview/dockviewGroupPanelModel.spec.ts | 7 +++- .../dockview/dockviewPanelModel.spec.ts | 7 ++++ .../src/dockview/dockviewComponent.scss | 26 ++++++++++++++ .../dockview-core/src/dockview/options.ts | 2 +- .../src/gridview/baseComponentGridview.scss | 4 +++ .../src/gridview/baseComponentGridview.ts | 11 ++++-- .../dockview-core/src/gridview/options.ts | 2 +- .../dockview-core/src/paneview/options.ts | 2 +- packages/dockview-core/src/resizable.ts | 11 ++---- .../dockview-core/src/splitview/options.ts | 2 +- packages/dockview-core/src/theme.scss | 9 ++--- .../resizecontainer-dockview/src/app.tsx | 4 +-- 14 files changed, 81 insertions(+), 46 deletions(-) create mode 100644 packages/dockview-core/src/gridview/baseComponentGridview.scss diff --git a/packages/dockview-core/src/__tests__/api/dockviewPanelApi.spec.ts b/packages/dockview-core/src/__tests__/api/dockviewPanelApi.spec.ts index 1d610ba99..e2474bc63 100644 --- a/packages/dockview-core/src/__tests__/api/dockviewPanelApi.spec.ts +++ b/packages/dockview-core/src/__tests__/api/dockviewPanelApi.spec.ts @@ -8,7 +8,7 @@ describe('groupPanelApi', () => { const accessor: Partial = { onDidAddPanel: jest.fn(), onDidRemovePanel: jest.fn(), - options: {}, + options: { parentElement: document.createElement('div') }, }; const panelMock = jest.fn(() => { @@ -44,7 +44,7 @@ describe('groupPanelApi', () => { const accessor: Partial = { onDidAddPanel: jest.fn(), onDidRemovePanel: jest.fn(), - options: {}, + options: { parentElement: document.createElement('div') }, }; const groupViewPanel = new DockviewGroupPanel( accessor, @@ -74,7 +74,7 @@ describe('groupPanelApi', () => { const accessor: Partial = { onDidAddPanel: jest.fn(), onDidRemovePanel: jest.fn(), - options: {}, + options: { parentElement: document.createElement('div') }, }; const groupViewPanel = new DockviewGroupPanel( accessor, diff --git a/packages/dockview-core/src/__tests__/dockview/components/titlebar/tabsContainer.spec.ts b/packages/dockview-core/src/__tests__/dockview/components/titlebar/tabsContainer.spec.ts index 58a007393..8d3a16b19 100644 --- a/packages/dockview-core/src/__tests__/dockview/components/titlebar/tabsContainer.spec.ts +++ b/packages/dockview-core/src/__tests__/dockview/components/titlebar/tabsContainer.spec.ts @@ -16,7 +16,7 @@ describe('tabsContainer', () => { return { onDidAddPanel: jest.fn(), onDidRemovePanel: jest.fn(), - options: {}, + options: { parentElement: document.createElement('div') }, }; }); const groupviewMock = jest.fn, []>( @@ -71,7 +71,7 @@ describe('tabsContainer', () => { id: 'testcomponentid', onDidAddPanel: jest.fn(), onDidRemovePanel: jest.fn(), - options: {}, + options: { parentElement: document.createElement('div') }, }; }); const groupviewMock = jest.fn, []>( @@ -139,7 +139,7 @@ describe('tabsContainer', () => { id: 'testcomponentid', onDidAddPanel: jest.fn(), onDidRemovePanel: jest.fn(), - options: {}, + options: { parentElement: document.createElement('div') }, }; }); const groupviewMock = jest.fn, []>( @@ -204,7 +204,7 @@ describe('tabsContainer', () => { id: 'testcomponentid', onDidAddPanel: jest.fn(), onDidRemovePanel: jest.fn(), - options: {}, + options: { parentElement: document.createElement('div') }, }; }); const groupviewMock = jest.fn, []>( @@ -269,7 +269,7 @@ describe('tabsContainer', () => { id: 'testcomponentid', onDidAddPanel: jest.fn(), onDidRemovePanel: jest.fn(), - options: {}, + options: { parentElement: document.createElement('div') }, }; }); const groupviewMock = jest.fn, []>( @@ -336,7 +336,7 @@ describe('tabsContainer', () => { test('left actions', () => { const accessorMock = jest.fn(() => { return (>{ - options: {}, + options: { parentElement: document.createElement('div') }, onDidAddPanel: jest.fn(), onDidRemovePanel: jest.fn(), }) as DockviewComponent; @@ -402,7 +402,7 @@ describe('tabsContainer', () => { test('right actions', () => { const accessorMock = jest.fn(() => { return (>{ - options: {}, + options: { parentElement: document.createElement('div') }, onDidAddPanel: jest.fn(), onDidRemovePanel: jest.fn(), }) as DockviewComponent; @@ -468,7 +468,7 @@ describe('tabsContainer', () => { test('that a tab will become floating when clicked if not floating and shift is selected', () => { const accessorMock = jest.fn(() => { return (>{ - options: {}, + options: { parentElement: document.createElement('div') }, onDidAddPanel: jest.fn(), onDidRemovePanel: jest.fn(), element: document.createElement('div'), @@ -514,21 +514,21 @@ describe('tabsContainer', () => { }, { inDragMode: true } ); - expect(accessor.addFloatingGroup).toBeCalledTimes(1); - expect(eventPreventDefaultSpy).toBeCalledTimes(1); + expect(accessor.addFloatingGroup).toHaveBeenCalledTimes(1); + expect(eventPreventDefaultSpy).toHaveBeenCalledTimes(1); const event2 = new KeyboardEvent('mousedown', { shiftKey: false }); const eventPreventDefaultSpy2 = jest.spyOn(event2, 'preventDefault'); fireEvent(container, event2); - expect(accessor.addFloatingGroup).toBeCalledTimes(1); - expect(eventPreventDefaultSpy2).toBeCalledTimes(0); + expect(accessor.addFloatingGroup).toHaveBeenCalledTimes(1); + expect(eventPreventDefaultSpy2).toHaveBeenCalledTimes(0); }); test('that a tab that is already floating cannot be floated again', () => { const accessorMock = jest.fn(() => { return (>{ - options: {}, + options: { parentElement: document.createElement('div') }, onDidAddPanel: jest.fn(), onDidRemovePanel: jest.fn(), element: document.createElement('div'), @@ -580,7 +580,7 @@ describe('tabsContainer', () => { test('that selecting a tab with shift down will move that tab into a new floating group', () => { const accessorMock = jest.fn(() => { return (>{ - options: {}, + options: { parentElement: document.createElement('div') }, onDidAddPanel: jest.fn(), onDidRemovePanel: jest.fn(), element: document.createElement('div'), @@ -642,7 +642,7 @@ describe('tabsContainer', () => { test('pre header actions', () => { const accessorMock = jest.fn(() => { return (>{ - options: {}, + options: { parentElement: document.createElement('div') }, onDidAddPanel: jest.fn(), onDidRemovePanel: jest.fn(), element: document.createElement('div'), @@ -712,7 +712,7 @@ describe('tabsContainer', () => { test('left header actions', () => { const accessorMock = jest.fn(() => { return (>{ - options: {}, + options: { parentElement: document.createElement('div') }, onDidAddPanel: jest.fn(), onDidRemovePanel: jest.fn(), element: document.createElement('div'), @@ -782,7 +782,7 @@ describe('tabsContainer', () => { test('right header actions', () => { const accessorMock = jest.fn(() => { return (>{ - options: {}, + options: { parentElement: document.createElement('div') }, onDidAddPanel: jest.fn(), onDidRemovePanel: jest.fn(), element: document.createElement('div'), diff --git a/packages/dockview-core/src/__tests__/dockview/dockviewGroupPanelModel.spec.ts b/packages/dockview-core/src/__tests__/dockview/dockviewGroupPanelModel.spec.ts index 6c58659dd..704a26009 100644 --- a/packages/dockview-core/src/__tests__/dockview/dockviewGroupPanelModel.spec.ts +++ b/packages/dockview-core/src/__tests__/dockview/dockviewGroupPanelModel.spec.ts @@ -243,7 +243,7 @@ describe('dockviewGroupPanelModel', () => { options = {}; dockview = (>{ - options: {}, + options: { parentElement: document.createElement('div') }, createWatermarkComponent: () => new Watermark(), doSetGroupActive: jest.fn(), id: 'dockview-1', @@ -639,6 +639,7 @@ describe('dockviewGroupPanelModel', () => { id: 'testcomponentid', options: { showDndOverlay: jest.fn(), + parentElement: document.createElement('div'), }, getPanel: jest.fn(), onDidAddPanel: jest.fn(), @@ -699,6 +700,7 @@ describe('dockviewGroupPanelModel', () => { id: 'testcomponentid', options: { showDndOverlay: () => true, + parentElement: document.createElement('div'), }, getPanel: jest.fn(), onDidAddPanel: jest.fn(), @@ -790,6 +792,7 @@ describe('dockviewGroupPanelModel', () => { id: 'testcomponentid', options: { showDndOverlay: jest.fn(), + parentElement: document.createElement('div'), }, getPanel: jest.fn(), doSetGroupActive: jest.fn(), @@ -863,6 +866,7 @@ describe('dockviewGroupPanelModel', () => { id: 'testcomponentid', options: { showDndOverlay: jest.fn(), + parentElement: document.createElement('div'), }, getPanel: jest.fn(), doSetGroupActive: jest.fn(), @@ -941,6 +945,7 @@ describe('dockviewGroupPanelModel', () => { id: 'testcomponentid', options: { showDndOverlay: jest.fn(), + parentElement: document.createElement('div'), }, getPanel: jest.fn(), doSetGroupActive: jest.fn(), diff --git a/packages/dockview-core/src/__tests__/dockview/dockviewPanelModel.spec.ts b/packages/dockview-core/src/__tests__/dockview/dockviewPanelModel.spec.ts index aacf0ca93..01fb2813f 100644 --- a/packages/dockview-core/src/__tests__/dockview/dockviewPanelModel.spec.ts +++ b/packages/dockview-core/src/__tests__/dockview/dockviewPanelModel.spec.ts @@ -38,6 +38,7 @@ describe('dockviewGroupPanel', () => { accessorMock = jest.fn(() => { const partial: Partial = { options: { + parentElement: document.createElement('div'), components: { contentComponent: contentMock, }, @@ -131,6 +132,7 @@ describe('dockviewGroupPanel', () => { accessorMock = jest.fn(() => { const partial: Partial = { options: { + parentElement: document.createElement('div'), components: { contentComponent: contentMock, }, @@ -159,6 +161,7 @@ describe('dockviewGroupPanel', () => { accessorMock = jest.fn(() => { const partial: Partial = { options: { + parentElement: document.createElement('div'), components: { contentComponent: contentMock, }, @@ -190,6 +193,7 @@ describe('dockviewGroupPanel', () => { accessorMock = jest.fn(() => { const partial: Partial = { options: { + parentElement: document.createElement('div'), components: { contentComponent: contentMock, }, @@ -222,6 +226,7 @@ describe('dockviewGroupPanel', () => { accessorMock = jest.fn(() => { const partial: Partial = { options: { + parentElement: document.createElement('div'), components: { contentComponent: contentMock, }, @@ -244,6 +249,7 @@ describe('dockviewGroupPanel', () => { accessorMock = jest.fn(() => { const partial: Partial = { options: { + parentElement: document.createElement('div'), components: { contentComponent: jest.fn().mockImplementation(() => { return contentMock; @@ -271,6 +277,7 @@ describe('dockviewGroupPanel', () => { accessorMock = jest.fn(() => { const partial: Partial = { options: { + parentElement: document.createElement('div'), frameworkComponents: { contentComponent: contentMock, }, diff --git a/packages/dockview-core/src/dockview/dockviewComponent.scss b/packages/dockview-core/src/dockview/dockviewComponent.scss index 54a0e290d..22261c1f4 100644 --- a/packages/dockview-core/src/dockview/dockviewComponent.scss +++ b/packages/dockview-core/src/dockview/dockviewComponent.scss @@ -14,6 +14,32 @@ .dv-overlay-render-container { position: relative; } + + .split-view-container { + &.horizontal { + > .view-container > .view { + &:not(:last-child) { + border-right: var(--dv-group-gap-size) solid transparent; + } + + &:not(:first-child) { + border-left: var(--dv-group-gap-size) solid transparent; + } + } + } + + &.vertical { + > .view-container > .view { + &:not(:last-child) { + border-bottom: var(--dv-group-gap-size) solid transparent; + } + + &:not(:first-child) { + border-top: var(--dv-group-gap-size) solid transparent; + } + } + } + } } .groupview { diff --git a/packages/dockview-core/src/dockview/options.ts b/packages/dockview-core/src/dockview/options.ts index 96493354b..4bfc8da06 100644 --- a/packages/dockview-core/src/dockview/options.ts +++ b/packages/dockview-core/src/dockview/options.ts @@ -89,7 +89,7 @@ export interface DockviewComponentOptions extends DockviewRenderFunctions { group: DockviewGroupPanel ) => IHeaderActionsRenderer; singleTabMode?: 'fullwidth' | 'default'; - parentElement?: HTMLElement; + parentElement: HTMLElement; disableFloatingGroups?: boolean; floatingGroupBounds?: | 'boundedWithinViewport' diff --git a/packages/dockview-core/src/gridview/baseComponentGridview.scss b/packages/dockview-core/src/gridview/baseComponentGridview.scss new file mode 100644 index 000000000..4b12e2437 --- /dev/null +++ b/packages/dockview-core/src/gridview/baseComponentGridview.scss @@ -0,0 +1,4 @@ +.dv-root-wrapper { + height: 100%; + width: 100%; +} diff --git a/packages/dockview-core/src/gridview/baseComponentGridview.ts b/packages/dockview-core/src/gridview/baseComponentGridview.ts index 2e4159a9b..12e877d2f 100644 --- a/packages/dockview-core/src/gridview/baseComponentGridview.ts +++ b/packages/dockview-core/src/gridview/baseComponentGridview.ts @@ -1,7 +1,7 @@ import { Emitter, Event, TickDelayedEvent } from '../events'; import { getGridLocation, Gridview, IGridView } from './gridview'; import { Position } from '../dnd/droptarget'; -import { IValueDisposable } from '../lifecycle'; +import { Disposable, IValueDisposable } from '../lifecycle'; import { sequentialNumberGenerator } from '../math'; import { ISplitviewStyles, Orientation, Sizing } from '../splitview/splitview'; import { IPanel } from '../panel/types'; @@ -32,7 +32,7 @@ export interface BaseGridOptions { readonly proportionalLayout: boolean; readonly orientation: Orientation; readonly styles?: ISplitviewStyles; - readonly parentElement?: HTMLElement; + readonly parentElement: HTMLElement; readonly disableAutoResizing?: boolean; } @@ -134,7 +134,9 @@ export abstract class BaseGrid } constructor(options: BaseGridOptions) { - super(options.parentElement, options.disableAutoResizing); + super(document.createElement('div'), options.disableAutoResizing); + + options.parentElement.appendChild(this.element); this.gridview = new Gridview( !!options.proportionalLayout, @@ -147,6 +149,9 @@ export abstract class BaseGrid this.layout(0, 0, true); // set some elements height/widths this.addDisposables( + Disposable.from(() => { + this.element.parentElement?.removeChild(this.element); + }), this.gridview.onDidChange(() => { this._bufferOnDidLayoutChange.fire(); }), diff --git a/packages/dockview-core/src/gridview/options.ts b/packages/dockview-core/src/gridview/options.ts index be37ce4d2..c40429358 100644 --- a/packages/dockview-core/src/gridview/options.ts +++ b/packages/dockview-core/src/gridview/options.ts @@ -17,5 +17,5 @@ export interface GridviewComponentOptions { }; frameworkComponentFactory?: FrameworkFactory; styles?: ISplitviewStyles; - parentElement?: HTMLElement; + parentElement: HTMLElement; } diff --git a/packages/dockview-core/src/paneview/options.ts b/packages/dockview-core/src/paneview/options.ts index 3523528d1..1d784e4f7 100644 --- a/packages/dockview-core/src/paneview/options.ts +++ b/packages/dockview-core/src/paneview/options.ts @@ -25,5 +25,5 @@ export interface PaneviewComponentOptions { }; disableDnd?: boolean; showDndOverlay?: (event: PaneviewDndOverlayEvent) => boolean; - parentElement?: HTMLElement; + parentElement: HTMLElement; } diff --git a/packages/dockview-core/src/resizable.ts b/packages/dockview-core/src/resizable.ts index d06418528..a40b76990 100644 --- a/packages/dockview-core/src/resizable.ts +++ b/packages/dockview-core/src/resizable.ts @@ -17,19 +17,12 @@ export abstract class Resizable extends CompositeDisposable { this._disableResizing = value; } - constructor(parentElement?: HTMLElement, disableResizing = false) { + constructor(parentElement: HTMLElement, disableResizing = false) { super(); this._disableResizing = disableResizing; - if (parentElement) { - this._element = parentElement; - } else { - this._element = document.createElement('div'); - this._element.style.height = '100%'; - this._element.style.width = '100%'; - this._element.className = 'dv-resizable-container'; - } + this._element = parentElement; this.addDisposables( watchElementResize(this._element, (entry) => { diff --git a/packages/dockview-core/src/splitview/options.ts b/packages/dockview-core/src/splitview/options.ts index a0c179e97..823f5a688 100644 --- a/packages/dockview-core/src/splitview/options.ts +++ b/packages/dockview-core/src/splitview/options.ts @@ -28,5 +28,5 @@ export interface SplitviewComponentOptions extends SplitViewOptions { [componentName: string]: any; }; frameworkWrapper?: FrameworkFactory; - parentElement?: HTMLElement; + parentElement: HTMLElement; } diff --git a/packages/dockview-core/src/theme.scss b/packages/dockview-core/src/theme.scss index 6a10a1a35..007fc426e 100644 --- a/packages/dockview-core/src/theme.scss +++ b/packages/dockview-core/src/theme.scss @@ -12,6 +12,7 @@ @mixin dockview-theme-dark-mixin { @include dockview-theme-core-mixin(); + // --dv-group-view-background-color: #1e1e1e; // @@ -228,13 +229,7 @@ } @mixin dockview-design-replit-mixin { - &.dv-dockview { - padding: 3px; - } - - .view:has(> .groupview) { - padding: 3px; - } + --dv-group-gap-size: 3px; .dv-resize-container:has(> .groupview) { border-radius: 8px; diff --git a/packages/docs/sandboxes/resizecontainer-dockview/src/app.tsx b/packages/docs/sandboxes/resizecontainer-dockview/src/app.tsx index 95fb87945..25c397bc2 100644 --- a/packages/docs/sandboxes/resizecontainer-dockview/src/app.tsx +++ b/packages/docs/sandboxes/resizecontainer-dockview/src/app.tsx @@ -93,7 +93,7 @@ export const App: React.FC = (props: { theme?: string }) => { ); }; -const Container = () => { +const Container = (props: any) => { const [value, setValue] = React.useState('50'); return ( @@ -108,7 +108,7 @@ const Container = () => { value={value} />
    - +
    ); From 8f9d225c61b1d325ae555e285d4c4f76e553d2cf Mon Sep 17 00:00:00 2001 From: mathuo <6710312+mathuo@users.noreply.github.com> Date: Sun, 28 Jan 2024 14:23:22 +0000 Subject: [PATCH 09/19] feat: window popout enhancements --- .../dockview/dockviewComponent.spec.ts | 16 ++ .../dockview-core/src/api/component.api.ts | 4 +- .../src/api/dockviewGroupPanelApi.ts | 13 +- .../dockview-core/src/api/dockviewPanelApi.ts | 11 +- .../src/dockview/dockviewComponent.ts | 167 +++++++++++------- .../src/dockview/dockviewPopoutGroupPanel.ts | 18 +- packages/dockview-core/src/popoutWindow.ts | 61 ++++--- packages/docs/package.json | 1 + .../popoutgroup-dockview/src/app.tsx | 39 ++-- .../popoutgroup-dockview/src/popover.tsx | 14 +- 10 files changed, 190 insertions(+), 154 deletions(-) diff --git a/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts b/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts index 15e38dd61..af7b18617 100644 --- a/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts +++ b/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts @@ -111,6 +111,13 @@ describe('dockviewComponent', () => { }); describe('memory leakage', () => { + beforeEach(() => { + window.open = () => fromPartial({ + addEventListener: jest.fn(), + close: jest.fn(), + }); + }); + test('event leakage', () => { Emitter.setLeakageMonitorEnabled(true); @@ -4415,6 +4422,15 @@ describe('dockviewComponent', () => { }); describe('popout group', () => { + beforeEach(() => { + jest.spyOn(window, 'open').mockReturnValue( + fromPartial({ + addEventListener: jest.fn(), + close: jest.fn(), + }) + ); + }); + test('that can remove a popout group', () => { const container = document.createElement('div'); diff --git a/packages/dockview-core/src/api/component.api.ts b/packages/dockview-core/src/api/component.api.ts index f57fa9fa4..49d82f98f 100644 --- a/packages/dockview-core/src/api/component.api.ts +++ b/packages/dockview-core/src/api/component.api.ts @@ -833,7 +833,7 @@ export class DockviewApi implements CommonApi { onDidOpen?: (event: { id: string; window: Window }) => void; onWillClose?: (event: { id: string; window: Window }) => void; } - ): void { - this.component.addPopoutGroup(item, options); + ): Promise { + return this.component.addPopoutGroup(item, options); } } diff --git a/packages/dockview-core/src/api/dockviewGroupPanelApi.ts b/packages/dockview-core/src/api/dockviewGroupPanelApi.ts index 1a999301d..a5a8bb371 100644 --- a/packages/dockview-core/src/api/dockviewGroupPanelApi.ts +++ b/packages/dockview-core/src/api/dockviewGroupPanelApi.ts @@ -9,12 +9,9 @@ export interface DockviewGroupPanelApi extends GridviewPanelApi { readonly onDidLocationChange: Event; readonly location: DockviewGroupLocation; /** - * - * If you require the documents Window object you can call `document.defaultView`. - * - * @see https://developer.mozilla.org/en-US/docs/Web/API/Document/defaultView + * If you require the Window object */ - getDocument(): Document; + getWindow(): Window; moveTo(options: { group?: DockviewGroupPanel; position?: Position }): void; maximize(): void; isMaximized(): boolean; @@ -49,10 +46,10 @@ export class DockviewGroupPanelApiImpl extends GridviewPanelApiImpl { this.addDisposables(this._onDidLocationChange); } - getDocument(): Document { + getWindow(): Window { return this.location.type === 'popout' - ? this.location.getWindow().document - : window.document; + ? this.location.getWindow() + : window; } moveTo(options: { group?: DockviewGroupPanel; position?: Position }): void { diff --git a/packages/dockview-core/src/api/dockviewPanelApi.ts b/packages/dockview-core/src/api/dockviewPanelApi.ts index dffca9297..66772bd1d 100644 --- a/packages/dockview-core/src/api/dockviewPanelApi.ts +++ b/packages/dockview-core/src/api/dockviewPanelApi.ts @@ -44,12 +44,9 @@ export interface DockviewPanelApi isMaximized(): boolean; exitMaximized(): void; /** - * - * If you require the documents Window object you can call `document.defaultView`. - * - * @see https://developer.mozilla.org/en-US/docs/Web/API/Document/defaultView + * If you require the Window object */ - getDocument(): Document; + getWindow(): Window; } export class DockviewPanelApiImpl @@ -145,8 +142,8 @@ export class DockviewPanelApiImpl ); } - getDocument(): Document { - return this.group.api.getDocument(); + getWindow(): Window { + return this.group.api.getWindow(); } moveTo(options: { diff --git a/packages/dockview-core/src/dockview/dockviewComponent.ts b/packages/dockview-core/src/dockview/dockviewComponent.ts index e808bdb79..0f6ef22c0 100644 --- a/packages/dockview-core/src/dockview/dockviewComponent.ts +++ b/packages/dockview-core/src/dockview/dockviewComponent.ts @@ -12,7 +12,7 @@ import { } from '../dnd/droptarget'; import { tail, sequenceEquals, remove } from '../array'; import { DockviewPanel, IDockviewPanel } from './dockviewPanel'; -import { CompositeDisposable, Disposable } from '../lifecycle'; +import { CompositeDisposable, Disposable, IDisposable } from '../lifecycle'; import { Event, Emitter } from '../events'; import { Watermark } from './components/watermark/watermark'; import { @@ -74,7 +74,7 @@ const DEFAULT_ROOT_OVERLAY_MODEL: DroptargetOverlayModel = { size: { type: 'pixels', value: 20 }, }; -function getTheme(element: HTMLElement): string | undefined { +function getDockviewTheme(element: HTMLElement): string | undefined { function toClassList(element: HTMLElement) { const list: string[] = []; @@ -290,7 +290,7 @@ export interface IDockviewComponent extends IBaseGrid { onDidOpen?: (event: { id: string; window: Window }) => void; onWillClose?: (event: { id: string; window: Window }) => void; } - ): void; + ): Promise; } export class DockviewComponent @@ -332,7 +332,11 @@ export class DockviewComponent this._onDidActivePanelChange.event; private readonly _floatingGroups: DockviewFloatingGroupPanel[] = []; - private readonly _popoutGroups: DockviewPopoutGroupPanel[] = []; + private readonly _popoutGroups: { + window: PopoutWindow; + group: DockviewGroupPanel; + disposable: IDisposable; + }[] = []; private readonly _rootDropTarget: Droptarget; get orientation(): Orientation { @@ -413,7 +417,7 @@ export class DockviewComponent // iterate over a copy of the array since .dispose() mutates the original array for (const group of [...this._popoutGroups]) { - group.dispose(); + group.disposable.dispose(); } }) ); @@ -510,7 +514,7 @@ export class DockviewComponent this.updateWatermark(); } - addPopoutGroup( + async addPopoutGroup( item: DockviewPanel | DockviewGroupPanel, options?: { skipRemoveGroup?: boolean; @@ -519,72 +523,108 @@ export class DockviewComponent onDidOpen?: (event: { id: string; window: Window }) => void; onWillClose?: (event: { id: string; window: Window }) => void; } - ): void { - let group: DockviewGroupPanel; - let box: Box | undefined = options?.position; + ): Promise { + const theme = getDockviewTheme(this.gridview.element); - if (item instanceof DockviewPanel) { - group = this.createGroup(); - - this.removePanel(item, { - removeEmptyGroup: true, - skipDispose: true, - }); - - group.model.openPanel(item); - - if (!box) { - box = this.element.getBoundingClientRect(); - } - } else { - group = item; - - if (!box) { - box = group.element.getBoundingClientRect(); + const getBox: () => Box = () => { + if (options?.position) { + return options.position; } - const skip = - typeof options?.skipRemoveGroup === 'boolean' && - options.skipRemoveGroup; - - if (!skip) { - this.doRemoveGroup(item, { skipDispose: true }); + if (item instanceof DockviewGroupPanel) { + return item.element.getBoundingClientRect(); } - } - const theme = getTheme(this.gridview.element); + if (item.group) { + return item.group.element.getBoundingClientRect(); + } + return this.element.getBoundingClientRect(); + }; - const popoutWindow = new DockviewPopoutGroupPanel( - `${this.id}-${group.id}`, // globally unique within dockview - group, + const box: Box = getBox(); + + const groupId = + item instanceof DockviewGroupPanel + ? item.id + : this.getNextGroupId(); + + const _window = new PopoutWindow( + `${this.id}-${groupId}`, // globally unique within dockview + theme ?? '', { - className: theme ?? '', - popoutUrl: options?.popoutUrl ?? '/popout.html', - box: { - left: window.screenX + box.left, - top: window.screenY + box.top, - width: box.width, - height: box.height, - }, + url: options?.popoutUrl ?? '/popout.html', + left: window.screenX + box.left, + top: window.screenY + box.top, + width: box.width, + height: box.height, onDidOpen: options?.onDidOpen, onWillClose: options?.onWillClose, } ); - popoutWindow.addDisposables( - { - dispose: () => { - remove(this._popoutGroups, popoutWindow); - this.updateWatermark(); - }, - }, - popoutWindow.window.onDidClose(() => { - this.doAddGroup(group, [0]); + const disposables = new CompositeDisposable( + _window, + _window.onDidClose(() => { + disposables.dispose(); }) ); - this._popoutGroups.push(popoutWindow); - this.updateWatermark(); + const popoutContainer = await _window.open(); + + if (popoutContainer) { + let group: DockviewGroupPanel; + + if (item instanceof DockviewPanel) { + group = this.createGroup({ id: groupId }); + + this.removePanel(item, { + removeEmptyGroup: true, + skipDispose: true, + }); + + group.model.openPanel(item); + } else { + group = item; + + const skip = + typeof options?.skipRemoveGroup === 'boolean' && + options.skipRemoveGroup; + + if (!skip) { + this.doRemoveGroup(item, { skipDispose: true }); + } + } + + popoutContainer.appendChild(group.element); + + group.model.location = { + type: 'popout', + getWindow: () => _window.window!, + }; + + const value = { window: _window, group, disposable: disposables }; + + disposables.addDisposables( + { + dispose: () => { + group.model.location = { type: 'grid' }; + + remove(this._popoutGroups, value); + this.updateWatermark(); + }, + }, + _window.onDidClose(() => { + this.doAddGroup(group, [0]); + }) + ); + + this._popoutGroups.push(value); + this.updateWatermark(); + return true; + } else { + disposables.dispose(); + return false; + } } addFloatingGroup( @@ -1428,7 +1468,7 @@ export class DockviewComponent this._onDidRemoveGroup.fire(group); } - selectedGroup.dispose(); + selectedGroup.disposable.dispose(); if (!options?.skipActive && this._activeGroup === group) { const groups = Array.from(this._groups.values()); @@ -1595,7 +1635,7 @@ export class DockviewComponent if (!selectedPopoutGroup) { throw new Error('failed to find popout group'); } - selectedPopoutGroup.dispose(); + selectedPopoutGroup.disposable.dispose(); } } @@ -1629,6 +1669,15 @@ export class DockviewComponent } } + private getNextGroupId(): string { + let id = this.nextGroupId.next(); + while (this._groups.has(id)) { + id = this.nextGroupId.next(); + } + + return id; + } + createGroup(options?: GroupOptions): DockviewGroupPanel { if (!options) { options = {}; diff --git a/packages/dockview-core/src/dockview/dockviewPopoutGroupPanel.ts b/packages/dockview-core/src/dockview/dockviewPopoutGroupPanel.ts index 858178280..3116b56d9 100644 --- a/packages/dockview-core/src/dockview/dockviewPopoutGroupPanel.ts +++ b/packages/dockview-core/src/dockview/dockviewPopoutGroupPanel.ts @@ -1,14 +1,12 @@ import { CompositeDisposable } from '../lifecycle'; import { PopoutWindow } from '../popoutWindow'; import { Box } from '../types'; -import { DockviewGroupPanel } from './dockviewGroupPanel'; export class DockviewPopoutGroupPanel extends CompositeDisposable { readonly window: PopoutWindow; constructor( readonly id: string, - readonly group: DockviewGroupPanel, private readonly options: { className: string; popoutUrl: string; @@ -29,23 +27,17 @@ export class DockviewPopoutGroupPanel extends CompositeDisposable { onWillClose: this.options.onWillClose, }); - group.model.location = { - type: 'popout', - getWindow: () => this.window.window!, - }; - this.addDisposables( this.window, - { - dispose: () => { - group.model.location = { type: 'grid' }; - }, - }, this.window.onDidClose(() => { this.dispose(); }) ); + } - this.window.open(group.element); + open(): Promise { + const didOpen = this.window.open(); + + return didOpen; } } diff --git a/packages/dockview-core/src/popoutWindow.ts b/packages/dockview-core/src/popoutWindow.ts index 3f1531cac..33d3a1434 100644 --- a/packages/dockview-core/src/popoutWindow.ts +++ b/packages/dockview-core/src/popoutWindow.ts @@ -66,7 +66,7 @@ export class PopoutWindow extends CompositeDisposable { } } - open(content: HTMLElement): void { + async open(): Promise { if (this._window) { throw new Error('instance of popout window is already open'); } @@ -88,9 +88,13 @@ export class PopoutWindow extends CompositeDisposable { const externalWindow = window.open(url, this.target, features); if (!externalWindow) { - return; + /** + * Popup blocked + */ + return null; } + const disposable = new CompositeDisposable(); this._window = { value: externalWindow, disposable }; @@ -104,36 +108,41 @@ export class PopoutWindow extends CompositeDisposable { * @see https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event */ this.close(); - }) - ); - - externalWindow.addEventListener('load', () => { - /** - * @see https://developer.mozilla.org/en-US/docs/Web/API/Window/load_event - */ - - const externalDocument = externalWindow.document; - externalDocument.title = document.title; - - const container = this.createPopoutWindowContainer(); - container.classList.add(this.className); - container.appendChild(content); - - // externalDocument.body.replaceChildren(container); - externalDocument.body.appendChild(container); - externalDocument.body.classList.add(this.className); - - addStyles(externalDocument, window.document.styleSheets); - - externalWindow.addEventListener('beforeunload', () => { + }), + addDisposableWindowListener(externalWindow, 'beforeunload', () => { /** * @see https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event */ this.close(); - }); + }) + ); + + const container = this.createPopoutWindowContainer(); + container.classList.add(this.className); + + this.options.onDidOpen?.({ + id: this.target, + window: externalWindow, }); - this.options.onDidOpen?.({ id: this.target, window: externalWindow }); + return new Promise((resolve) => { + externalWindow.addEventListener('load', () => { + /** + * @see https://developer.mozilla.org/en-US/docs/Web/API/Window/load_event + */ + + const externalDocument = externalWindow.document; + externalDocument.title = document.title; + + // externalDocument.body.replaceChildren(container); + externalDocument.body.appendChild(container); + externalDocument.body.classList.add(this.className); + + addStyles(externalDocument, window.document.styleSheets); + + resolve(container); + }); + }); } private createPopoutWindowContainer(): HTMLElement { diff --git a/packages/docs/package.json b/packages/docs/package.json index efc9d4128..6f40f715e 100644 --- a/packages/docs/package.json +++ b/packages/docs/package.json @@ -34,6 +34,7 @@ "dockview": "^1.9.2", "prism-react-renderer": "^2.3.1", "react-dnd": "^16.0.1", + "react-laag": "^2.0.5", "recoil": "^0.7.7", "source-map-loader": "^4.0.2", "uuid": "^9.0.1" diff --git a/packages/docs/sandboxes/popoutgroup-dockview/src/app.tsx b/packages/docs/sandboxes/popoutgroup-dockview/src/app.tsx index 57fe7e8be..fbca7560d 100644 --- a/packages/docs/sandboxes/popoutgroup-dockview/src/app.tsx +++ b/packages/docs/sandboxes/popoutgroup-dockview/src/app.tsx @@ -6,49 +6,30 @@ import { IDockviewPanelProps, SerializedDockview, DockviewPanelApi, - DockviewGroupLocation, } from 'dockview'; import * as React from 'react'; -import * as ReactDOM from 'react-dom'; import { Icon } from './utils'; import { PopoverMenu } from './popover'; -function usePopoutWindowContext(api: DockviewPanelApi): Window { - const [location, setLocation] = React.useState( - api.location - ); +function usePanelWindowObject(api: DockviewPanelApi): Window { + const [document, setDocument] = React.useState(api.getWindow()); React.useEffect(() => { const disposable = api.onDidLocationChange((event) => { - setLocation(event.location); + setDocument(api.getWindow()); }); return () => { disposable.dispose(); }; - }); + }, [api]); - const windowContext = React.useMemo(() => { - if (location.type === 'popout') { - return location.getWindow(); - } - return window; - }, [location]); - - return windowContext; + return document; } const components = { default: (props: IDockviewPanelProps<{ title: string }>) => { - const windowContext = usePopoutWindowContext(props.api); - - React.useEffect(() => { - setTimeout(() => { - const a = windowContext.document.createElement('div'); - a.className = 'aaa'; - windowContext.document.body.appendChild(a); - }, 5000); - }, [windowContext]); + const _window = usePanelWindowObject(props.api); const [reset, setReset] = React.useState(false); @@ -62,7 +43,7 @@ const components = { > - {!reset && } + {!reset && } {props.api.title} ); @@ -258,12 +239,12 @@ const LeftComponent = (props: IDockviewHeaderActionsProps) => { const RightComponent = (props: IDockviewHeaderActionsProps) => { const [popout, setPopout] = React.useState( - props.api.location === 'popout' + props.api.location.type === 'popout' ); React.useEffect(() => { const disposable = props.group.api.onDidLocationChange((event) => [ - setPopout(event.location === 'popout'), + setPopout(event.location.type === 'popout'), ]); return () => { diff --git a/packages/docs/sandboxes/popoutgroup-dockview/src/popover.tsx b/packages/docs/sandboxes/popoutgroup-dockview/src/popover.tsx index 3b0b8d1f4..9d9663443 100644 --- a/packages/docs/sandboxes/popoutgroup-dockview/src/popover.tsx +++ b/packages/docs/sandboxes/popoutgroup-dockview/src/popover.tsx @@ -3,7 +3,7 @@ import { motion, AnimatePresence } from 'framer-motion'; import * as React from 'react'; import { DockviewPanelApi } from 'dockview'; -export function PopoverMenu(props: { api: DockviewPanelApi }) { +export function PopoverMenu(props: { window: Window }) { const [isOpen, setOpen] = React.useState(false); // helper function to close the menu @@ -11,11 +11,6 @@ export function PopoverMenu(props: { api: DockviewPanelApi }) { setOpen(false); } - const _window = - props.api.location.type === 'popout' - ? props.api.location.getWindow() - : undefined; - const { renderLayer, triggerProps, layerProps, arrowProps } = useLayer({ isOpen, onOutsideClick: close, // close the menu when the user clicks outside @@ -26,15 +21,14 @@ export function PopoverMenu(props: { api: DockviewPanelApi }) { triggerOffset: 12, // keep some distance to the trigger containerOffset: 16, // give the menu some room to breath relative to the container arrowOffset: 16, // let the arrow have some room to breath also, - environment: _window, - container: _window + environment: props.window, + container: props.window ? () => { - const el = _window.document.body; + const el = props.window.document.body; Object.setPrototypeOf(el, HTMLElement.prototype); return el; } : undefined, - // container: props.window.document.body }); // Again, we're using framer-motion for the transition effect From 20c1a66d205855e0b85d2fd457a1047e6d6a77b6 Mon Sep 17 00:00:00 2001 From: mathuo <6710312+mathuo@users.noreply.github.com> Date: Mon, 29 Jan 2024 20:57:15 +0000 Subject: [PATCH 10/19] feat: popout group enhancements --- .../dockview/dockviewComponent.spec.ts | 214 ++++++++++-------- .../gridview/gridviewComponent.spec.ts | 2 +- .../dockview-core/src/api/component.api.ts | 2 +- packages/dockview-core/src/api/panelApi.ts | 59 +++-- .../components/titlebar/tabsContainer.ts | 3 +- .../src/dockview/dockviewComponent.ts | 186 +++++++++------ .../src/dockview/dockviewGroupPanelModel.ts | 1 + .../src/dockview/dockviewPopoutGroupPanel.ts | 43 ---- .../dockview-core/src/gridview/gridview.ts | 28 ++- .../src/gridview/gridviewPanel.ts | 13 +- packages/dockview-core/src/index.ts | 2 - packages/dockview-core/src/lifecycle.ts | 10 +- packages/dockview-core/src/popoutWindow.ts | 34 ++- .../src/splitview/splitviewPanel.ts | 6 +- packages/dockview/src/gridview/view.ts | 5 +- 15 files changed, 335 insertions(+), 273 deletions(-) delete mode 100644 packages/dockview-core/src/dockview/dockviewPopoutGroupPanel.ts diff --git a/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts b/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts index af7b18617..bdf032b03 100644 --- a/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts +++ b/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts @@ -110,109 +110,109 @@ describe('dockviewComponent', () => { window.open = jest.fn(); // not implemented by jest }); - describe('memory leakage', () => { - beforeEach(() => { - window.open = () => fromPartial({ - addEventListener: jest.fn(), - close: jest.fn(), - }); - }); + // describe('memory leakage', () => { + // beforeEach(() => { + // window.open = () => fromPartial({ + // addEventListener: jest.fn(), + // close: jest.fn(), + // }); + // }); - test('event leakage', () => { - Emitter.setLeakageMonitorEnabled(true); + // test('event leakage', () => { + // Emitter.setLeakageMonitorEnabled(true); - dockview = new DockviewComponent({ - parentElement: container, - components: { - default: PanelContentPartTest, - }, - }); + // dockview = new DockviewComponent({ + // parentElement: container, + // components: { + // default: PanelContentPartTest, + // }, + // }); - dockview.layout(500, 1000); + // dockview.layout(500, 1000); - const panel1 = dockview.addPanel({ - id: 'panel1', - component: 'default', - }); + // const panel1 = dockview.addPanel({ + // id: 'panel1', + // component: 'default', + // }); - const panel2 = dockview.addPanel({ - id: 'panel2', - component: 'default', - }); + // const panel2 = dockview.addPanel({ + // id: 'panel2', + // component: 'default', + // }); - dockview.removePanel(panel2); + // dockview.removePanel(panel2); - const panel3 = dockview.addPanel({ - id: 'panel3', - component: 'default', - position: { - direction: 'right', - referencePanel: 'panel1', - }, - }); + // const panel3 = dockview.addPanel({ + // id: 'panel3', + // component: 'default', + // position: { + // direction: 'right', + // referencePanel: 'panel1', + // }, + // }); - const panel4 = dockview.addPanel({ - id: 'panel4', - component: 'default', - position: { - direction: 'above', - }, - }); + // const panel4 = dockview.addPanel({ + // id: 'panel4', + // component: 'default', + // position: { + // direction: 'above', + // }, + // }); - dockview.moveGroupOrPanel( - panel4.group, - panel3.group.id, - panel3.id, - 'center' - ); + // dockview.moveGroupOrPanel( + // panel4.group, + // panel3.group.id, + // panel3.id, + // 'center' + // ); - dockview.addPanel({ - id: 'panel5', - component: 'default', - floating: true, - }); + // dockview.addPanel({ + // id: 'panel5', + // component: 'default', + // floating: true, + // }); - const panel6 = dockview.addPanel({ - id: 'panel6', - component: 'default', - position: { - referencePanel: 'panel5', - direction: 'within', - }, - }); + // const panel6 = dockview.addPanel({ + // id: 'panel6', + // component: 'default', + // position: { + // referencePanel: 'panel5', + // direction: 'within', + // }, + // }); - dockview.addFloatingGroup(panel4.api.group); + // dockview.addFloatingGroup(panel4.api.group); - dockview.addPopoutGroup(panel6); + // dockview.addPopoutGroup(panel6); - dockview.moveGroupOrPanel( - panel1.group, - panel6.group.id, - panel6.id, - 'center' - ); + // dockview.moveGroupOrPanel( + // panel1.group, + // panel6.group.id, + // panel6.id, + // 'center' + // ); - dockview.moveGroupOrPanel( - panel4.group, - panel6.group.id, - panel6.id, - 'center' - ); + // dockview.moveGroupOrPanel( + // panel4.group, + // panel6.group.id, + // panel6.id, + // 'center' + // ); - dockview.dispose(); + // dockview.dispose(); - if (Emitter.MEMORY_LEAK_WATCHER.size > 0) { - for (const entry of Array.from( - Emitter.MEMORY_LEAK_WATCHER.events - )) { - console.log('disposal', entry[1]); - } - throw new Error('not all listeners disposed'); - } + // if (Emitter.MEMORY_LEAK_WATCHER.size > 0) { + // for (const entry of Array.from( + // Emitter.MEMORY_LEAK_WATCHER.events + // )) { + // console.log('disposal', entry[1]); + // } + // throw new Error('not all listeners disposed'); + // } - Emitter.setLeakageMonitorEnabled(false); - }); - }); + // Emitter.setLeakageMonitorEnabled(false); + // }); + // }); test('duplicate panel', () => { dockview.layout(500, 1000); @@ -4425,13 +4425,22 @@ describe('dockviewComponent', () => { beforeEach(() => { jest.spyOn(window, 'open').mockReturnValue( fromPartial({ - addEventListener: jest.fn(), + document: fromPartial({ + body: document.createElement('body'), + }), + addEventListener: jest + .fn() + .mockImplementation((name, cb) => { + if (name === 'load') { + cb(); + } + }), close: jest.fn(), }) ); }); - test('that can remove a popout group', () => { + test('that can remove a popout group', async () => { const container = document.createElement('div'); const dockview = new DockviewComponent({ @@ -4452,10 +4461,10 @@ describe('dockviewComponent', () => { component: 'default', }); - dockview.addPopoutGroup(panel1); + await dockview.addPopoutGroup(panel1); expect(dockview.panels.length).toBe(1); - expect(dockview.groups.length).toBe(1); + expect(dockview.groups.length).toBe(2); expect(panel1.api.group.api.location.type).toBe('popout'); dockview.removePanel(panel1); @@ -4464,7 +4473,7 @@ describe('dockviewComponent', () => { expect(dockview.groups.length).toBe(0); }); - test('add a popout group', () => { + test('add a popout group', async () => { const container = document.createElement('div'); const dockview = new DockviewComponent({ @@ -4495,15 +4504,15 @@ describe('dockviewComponent', () => { expect(dockview.groups.length).toBe(1); expect(dockview.panels.length).toBe(2); - dockview.addPopoutGroup(panel2.group); + await dockview.addPopoutGroup(panel2.group); expect(panel1.group.api.location.type).toBe('popout'); expect(panel2.group.api.location.type).toBe('popout'); - expect(dockview.groups.length).toBe(1); + expect(dockview.groups.length).toBe(2); expect(dockview.panels.length).toBe(2); }); - test('move from fixed to popout group and back', () => { + test('move from fixed to popout group and back', async () => { const container = document.createElement('div'); const dockview = new DockviewComponent({ @@ -4543,12 +4552,12 @@ describe('dockviewComponent', () => { expect(dockview.groups.length).toBe(2); expect(dockview.panels.length).toBe(3); - dockview.addPopoutGroup(panel2.group); + await dockview.addPopoutGroup(panel2.group); expect(panel1.group.api.location.type).toBe('popout'); expect(panel2.group.api.location.type).toBe('popout'); expect(panel3.group.api.location.type).toBe('grid'); - expect(dockview.groups.length).toBe(2); + expect(dockview.groups.length).toBe(3); expect(dockview.panels.length).toBe(3); dockview.moveGroupOrPanel( @@ -4561,7 +4570,20 @@ describe('dockviewComponent', () => { expect(panel1.group.api.location.type).toBe('popout'); expect(panel2.group.api.location.type).toBe('grid'); expect(panel3.group.api.location.type).toBe('grid'); - expect(dockview.groups.length).toBe(3); + expect(dockview.groups.length).toBe(4); + expect(dockview.panels.length).toBe(3); + + dockview.moveGroupOrPanel( + panel3.api.group, + panel1.api.group.id, + panel1.api.id, + 'center' + ); + + expect(panel1.group.api.location.type).toBe('grid'); + expect(panel2.group.api.location.type).toBe('grid'); + expect(panel3.group.api.location.type).toBe('grid'); + expect(dockview.groups.length).toBe(2); expect(dockview.panels.length).toBe(3); }); }); diff --git a/packages/dockview-core/src/__tests__/gridview/gridviewComponent.spec.ts b/packages/dockview-core/src/__tests__/gridview/gridviewComponent.spec.ts index c54341cc0..d0873eba0 100644 --- a/packages/dockview-core/src/__tests__/gridview/gridviewComponent.spec.ts +++ b/packages/dockview-core/src/__tests__/gridview/gridviewComponent.spec.ts @@ -268,7 +268,7 @@ describe('gridview', () => { ], }, }, - activePanel: 'panel_1', + activePanel: 'panel_2', }); }); diff --git a/packages/dockview-core/src/api/component.api.ts b/packages/dockview-core/src/api/component.api.ts index 49d82f98f..e5faa8cd5 100644 --- a/packages/dockview-core/src/api/component.api.ts +++ b/packages/dockview-core/src/api/component.api.ts @@ -833,7 +833,7 @@ export class DockviewApi implements CommonApi { onDidOpen?: (event: { id: string; window: Window }) => void; onWillClose?: (event: { id: string; window: Window }) => void; } - ): Promise { + ): Promise { return this.component.addPopoutGroup(item, options); } } diff --git a/packages/dockview-core/src/api/panelApi.ts b/packages/dockview-core/src/api/panelApi.ts index 95d35a3b8..57e80f214 100644 --- a/packages/dockview-core/src/api/panelApi.ts +++ b/packages/dockview-core/src/api/panelApi.ts @@ -14,6 +14,10 @@ export interface VisibilityEvent { readonly isVisible: boolean; } +export interface HiddenEvent { + readonly isHidden: boolean; +} + export interface ActiveEvent { readonly isActive: boolean; } @@ -24,7 +28,7 @@ export interface PanelApi { readonly onDidFocusChange: Event; readonly onDidVisibilityChange: Event; readonly onDidActiveChange: Event; - setVisible(isVisible: boolean): void; + readonly onDidHiddenChange: Event; setActive(): void; updateParameters(parameters: Parameters): void; /** @@ -43,6 +47,10 @@ export interface PanelApi { * Whether the panel is visible */ readonly isVisible: boolean; + /** + * Whether the panel is hidden + */ + readonly isHidden: boolean; /** * The panel width in pixels */ @@ -60,6 +68,7 @@ export class PanelApiImpl extends CompositeDisposable implements PanelApi { private _isFocused = false; private _isActive = false; private _isVisible = true; + private _isHidden = false; private _width = 0; private _height = 0; @@ -69,56 +78,59 @@ export class PanelApiImpl extends CompositeDisposable implements PanelApi { replay: true, }); readonly onDidDimensionsChange = this._onDidDimensionChange.event; - // + readonly _onDidChangeFocus = new Emitter({ replay: true, }); readonly onDidFocusChange: Event = this._onDidChangeFocus.event; - // + readonly _onFocusEvent = new Emitter(); readonly onFocusEvent: Event = this._onFocusEvent.event; - // + readonly _onDidVisibilityChange = new Emitter({ replay: true, }); readonly onDidVisibilityChange: Event = this._onDidVisibilityChange.event; - // - readonly _onVisibilityChange = new Emitter(); - readonly onVisibilityChange: Event = - this._onVisibilityChange.event; - // + readonly _onDidHiddenChange = new Emitter(); + readonly onDidHiddenChange: Event = + this._onDidHiddenChange.event; + readonly _onDidActiveChange = new Emitter({ replay: true, }); readonly onDidActiveChange: Event = this._onDidActiveChange.event; - // + readonly _onActiveChange = new Emitter(); readonly onActiveChange: Event = this._onActiveChange.event; - // + readonly _onUpdateParameters = new Emitter(); readonly onUpdateParameters: Event = this._onUpdateParameters.event; - // - get isFocused() { + get isFocused(): boolean { return this._isFocused; } - get isActive() { + get isActive(): boolean { return this._isActive; } - get isVisible() { + + get isVisible(): boolean { return this._isVisible; } - get width() { + get isHidden(): boolean { + return this._isHidden; + } + + get width(): number { return this._width; } - get height() { + get height(): number { return this._height; } @@ -135,6 +147,9 @@ export class PanelApiImpl extends CompositeDisposable implements PanelApi { this.onDidVisibilityChange((event) => { this._isVisible = event.isVisible; }), + this.onDidHiddenChange((event) => { + this._isHidden = event.isHidden; + }), this.onDidDimensionsChange((event) => { this._width = event.width; this._height = event.height; @@ -146,7 +161,7 @@ export class PanelApiImpl extends CompositeDisposable implements PanelApi { this._onDidActiveChange, this._onFocusEvent, this._onActiveChange, - this._onVisibilityChange, + this._onDidHiddenChange, this._onUpdateParameters ); } @@ -161,8 +176,8 @@ export class PanelApiImpl extends CompositeDisposable implements PanelApi { ); } - setVisible(isVisible: boolean) { - this._onVisibilityChange.fire({ isVisible }); + setHidden(isHidden: boolean): void { + this._onDidHiddenChange.fire({ isHidden }); } setActive(): void { @@ -172,8 +187,4 @@ export class PanelApiImpl extends CompositeDisposable implements PanelApi { updateParameters(parameters: Parameters): void { this._onUpdateParameters.fire(parameters); } - - dispose() { - super.dispose(); - } } diff --git a/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts b/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts index 6a7caac29..60ae6ed23 100644 --- a/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts +++ b/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts @@ -350,7 +350,8 @@ export class TabsContainer !this.accessor.options.disableFloatingGroups; const isFloatingWithOnePanel = - this.group.api.location.type === 'floating' && this.size === 1; + this.group.api.location.type === 'floating' && + this.size === 1; if ( isFloatingGroupsEnabled && diff --git a/packages/dockview-core/src/dockview/dockviewComponent.ts b/packages/dockview-core/src/dockview/dockviewComponent.ts index 0f6ef22c0..3366da217 100644 --- a/packages/dockview-core/src/dockview/dockviewComponent.ts +++ b/packages/dockview-core/src/dockview/dockviewComponent.ts @@ -58,7 +58,6 @@ import { TabDragEvent, } from './components/titlebar/tabsContainer'; import { Box } from '../types'; -import { DockviewPopoutGroupPanel } from './dockviewPopoutGroupPanel'; import { DEFAULT_FLOATING_GROUP_OVERFLOW_SIZE, DEFAULT_FLOATING_GROUP_POSITION, @@ -290,7 +289,7 @@ export interface IDockviewComponent extends IBaseGrid { onDidOpen?: (event: { id: string; window: Window }) => void; onWillClose?: (event: { id: string; window: Window }) => void; } - ): Promise; + ): Promise; } export class DockviewComponent @@ -334,7 +333,8 @@ export class DockviewComponent private readonly _floatingGroups: DockviewFloatingGroupPanel[] = []; private readonly _popoutGroups: { window: PopoutWindow; - group: DockviewGroupPanel; + popoutGroup: DockviewGroupPanel; + referenceGroup: DockviewGroupPanel; disposable: IDisposable; }[] = []; private readonly _rootDropTarget: Droptarget; @@ -514,7 +514,7 @@ export class DockviewComponent this.updateWatermark(); } - async addPopoutGroup( + addPopoutGroup( item: DockviewPanel | DockviewGroupPanel, options?: { skipRemoveGroup?: boolean; @@ -523,10 +523,28 @@ export class DockviewComponent onDidOpen?: (event: { id: string; window: Window }) => void; onWillClose?: (event: { id: string; window: Window }) => void; } - ): Promise { - const theme = getDockviewTheme(this.gridview.element); + ): Promise { + if (item instanceof DockviewPanel && item.group.size === 1) { + return this.addPopoutGroup(item.group); + } - const getBox: () => Box = () => { + const theme = getDockviewTheme(this.gridview.element); + const element = this.element; + + function moveGroupWithoutDestroying(options: { + from: DockviewGroupPanel; + to: DockviewGroupPanel; + }) { + const panels = [...options.from.panels].map((panel) => + options.from.model.removePanel(panel) + ); + + panels.forEach((panel) => { + options.to.model.openPanel(panel); + }); + } + + function getBox(): Box { if (options?.position) { return options.position; } @@ -538,18 +556,17 @@ export class DockviewComponent if (item.group) { return item.group.element.getBoundingClientRect(); } - return this.element.getBoundingClientRect(); - }; + return element.getBoundingClientRect(); + } const box: Box = getBox(); - const groupId = - item instanceof DockviewGroupPanel - ? item.id - : this.getNextGroupId(); + const groupId = this.getNextGroupId(); //item.id; + + item.api.setHidden(true); const _window = new PopoutWindow( - `${this.id}-${groupId}`, // globally unique within dockview + `${this.id}-${groupId}`, // unique id theme ?? '', { url: options?.popoutUrl ?? '/popout.html', @@ -562,69 +579,85 @@ export class DockviewComponent } ); - const disposables = new CompositeDisposable( + const popoutWindowDisposable = new CompositeDisposable( _window, _window.onDidClose(() => { - disposables.dispose(); + popoutWindowDisposable.dispose(); }) ); - const popoutContainer = await _window.open(); - - if (popoutContainer) { - let group: DockviewGroupPanel; - - if (item instanceof DockviewPanel) { - group = this.createGroup({ id: groupId }); - - this.removePanel(item, { - removeEmptyGroup: true, - skipDispose: true, - }); - - group.model.openPanel(item); - } else { - group = item; - - const skip = - typeof options?.skipRemoveGroup === 'boolean' && - options.skipRemoveGroup; - - if (!skip) { - this.doRemoveGroup(item, { skipDispose: true }); + return _window + .open() + .then((popoutContainer) => { + if (_window.isDisposed) { + return; } - } - popoutContainer.appendChild(group.element); + if (popoutContainer === null) { + popoutWindowDisposable.dispose(); + return; + } - group.model.location = { - type: 'popout', - getWindow: () => _window.window!, - }; + const referenceGroup = + item instanceof DockviewPanel ? item.group : item; - const value = { window: _window, group, disposable: disposables }; + const group = this.createGroup({ id: groupId }); - disposables.addDisposables( - { - dispose: () => { - group.model.location = { type: 'grid' }; + if (item instanceof DockviewPanel) { + const panel = referenceGroup.model.removePanel(item); + group.model.openPanel(panel); + } else { + moveGroupWithoutDestroying({ + from: referenceGroup, + to: group, + }); + referenceGroup.api.setHidden(false); + } - remove(this._popoutGroups, value); - this.updateWatermark(); - }, - }, - _window.onDidClose(() => { - this.doAddGroup(group, [0]); - }) - ); + popoutContainer.appendChild(group.element); - this._popoutGroups.push(value); - this.updateWatermark(); - return true; - } else { - disposables.dispose(); - return false; - } + group.model.location = { + type: 'popout', + getWindow: () => _window.window!, + }; + + const value = { + window: _window, + popoutGroup: group, + referenceGroup, + disposable: popoutWindowDisposable, + }; + + popoutWindowDisposable.addDisposables( + Disposable.from(() => { + if (this.getPanel(referenceGroup.id)) { + moveGroupWithoutDestroying({ + from: group, + to: referenceGroup, + }); + + if (referenceGroup.api.isHidden) { + referenceGroup.api.setHidden(false); + } + + this.doRemoveGroup(group); + } else { + const removedGroup = this.doRemoveGroup(group, { + skipDispose: true, + skipActive: true, + }); + removedGroup.model.location = { type: 'grid' }; + this.doAddGroup(removedGroup, [0]); + } + }) + ); + + this._popoutGroups.push(value); + this.updateWatermark(); + }) + .catch((err) => { + console.error(err); + }); } addFloatingGroup( @@ -923,7 +956,7 @@ export class DockviewComponent const popoutGroups: SerializedPopoutGroup[] = this._popoutGroups.map( (group) => { return { - data: group.group.toJSON() as GroupPanelViewState, + data: group.popoutGroup.toJSON() as GroupPanelViewState, position: group.window.dimensions(), }; } @@ -1307,8 +1340,9 @@ export class DockviewComponent private updateWatermark(): void { if ( - this.groups.filter((x) => x.api.location.type === 'grid').length === - 0 + this.groups.filter( + (x) => x.api.location.type === 'grid' && !x.api.isHidden + ).length === 0 ) { if (!this.watermark) { this.watermark = this.createWatermarkComponent(); @@ -1458,12 +1492,14 @@ export class DockviewComponent if (group.api.location.type === 'popout') { const selectedGroup = this._popoutGroups.find( - (_) => _.group === group + (_) => _.popoutGroup === group ); if (selectedGroup) { if (!options?.skipDispose) { - selectedGroup.group.dispose(); + this.doRemoveGroup(selectedGroup.referenceGroup); + + selectedGroup.popoutGroup.dispose(); this._groups.delete(group.id); this._onDidRemoveGroup.fire(group); } @@ -1478,7 +1514,8 @@ export class DockviewComponent ); } - return selectedGroup.group; + this.updateWatermark(); + return selectedGroup.popoutGroup; } throw new Error('failed to find popout group'); @@ -1630,7 +1667,7 @@ export class DockviewComponent } case 'popout': { const selectedPopoutGroup = this._popoutGroups.find( - (x) => x.group === sourceGroup + (x) => x.popoutGroup === sourceGroup ); if (!selectedPopoutGroup) { throw new Error('failed to find popout group'); @@ -1700,7 +1737,7 @@ export class DockviewComponent } const view = new DockviewGroupPanel(this, id, options); - view.init({ params: {}, accessor: null }); // required to initialized .part and allow for correct disposal of group + view.init({ params: {}, accessor: this }); if (!this._groups.has(view.id)) { const disposable = new CompositeDisposable( @@ -1735,8 +1772,7 @@ export class DockviewComponent this._groups.set(view.id, { value: view, disposable }); } - // TODO: must be called after the above listeners have been setup, - // not an ideal pattern + // TODO: must be called after the above listeners have been setup, not an ideal pattern view.initialize(); return view; diff --git a/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts b/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts index a80a7baa0..120bc1b87 100644 --- a/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts +++ b/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts @@ -838,6 +838,7 @@ export class DockviewGroupPanelModel this.watermark?.element.remove(); this.watermark?.dispose?.(); + this.watermark = undefined; for (const panel of this.panels) { panel.dispose(); diff --git a/packages/dockview-core/src/dockview/dockviewPopoutGroupPanel.ts b/packages/dockview-core/src/dockview/dockviewPopoutGroupPanel.ts deleted file mode 100644 index 3116b56d9..000000000 --- a/packages/dockview-core/src/dockview/dockviewPopoutGroupPanel.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { CompositeDisposable } from '../lifecycle'; -import { PopoutWindow } from '../popoutWindow'; -import { Box } from '../types'; - -export class DockviewPopoutGroupPanel extends CompositeDisposable { - readonly window: PopoutWindow; - - constructor( - readonly id: string, - private readonly options: { - className: string; - popoutUrl: string; - box: Box; - onDidOpen?: (event: { id: string; window: Window }) => void; - onWillClose?: (event: { id: string; window: Window }) => void; - } - ) { - super(); - - this.window = new PopoutWindow(id, options.className ?? '', { - url: this.options.popoutUrl, - left: this.options.box.left, - top: this.options.box.top, - width: this.options.box.width, - height: this.options.box.height, - onDidOpen: this.options.onDidOpen, - onWillClose: this.options.onWillClose, - }); - - this.addDisposables( - this.window, - this.window.onDidClose(() => { - this.dispose(); - }) - ); - } - - open(): Promise { - const didOpen = this.window.open(); - - return didOpen; - } -} diff --git a/packages/dockview-core/src/gridview/gridview.ts b/packages/dockview-core/src/gridview/gridview.ts index f34fe2672..48138d1e8 100644 --- a/packages/dockview-core/src/gridview/gridview.ts +++ b/packages/dockview-core/src/gridview/gridview.ts @@ -273,7 +273,9 @@ export class Gridview implements IDisposable { readonly element: HTMLElement; private _root: BranchNode | undefined; - private _maximizedNode: LeafNode | undefined = undefined; + private _maximizedNode: + | { leaf: LeafNode; hiddenOnMaximize: LeafNode[] } + | undefined = undefined; private readonly disposable: MutableDisposable = new MutableDisposable(); private readonly _onDidChange = new Emitter<{ @@ -329,7 +331,7 @@ export class Gridview implements IDisposable { } maximizedView(): IGridView | undefined { - return this._maximizedNode?.view; + return this._maximizedNode?.leaf.view; } hasMaximizedView(): boolean { @@ -344,7 +346,7 @@ export class Gridview implements IDisposable { return; } - if (this._maximizedNode === node) { + if (this._maximizedNode?.leaf === node) { return; } @@ -352,12 +354,18 @@ export class Gridview implements IDisposable { this.exitMaximizedView(); } + const hiddenOnMaximize: LeafNode[] = []; + function hideAllViewsBut(parent: BranchNode, exclude: LeafNode): void { for (let i = 0; i < parent.children.length; i++) { const child = parent.children[i]; if (child instanceof LeafNode) { if (child !== exclude) { - parent.setChildVisible(i, false); + if (parent.isChildVisible(i)) { + parent.setChildVisible(i, false); + } else { + hiddenOnMaximize.push(child); + } } } else { hideAllViewsBut(child, exclude); @@ -366,7 +374,7 @@ export class Gridview implements IDisposable { } hideAllViewsBut(this.root, node); - this._maximizedNode = node; + this._maximizedNode = { leaf: node, hiddenOnMaximize }; this._onDidMaxmizedNodeChange.fire(); } @@ -375,11 +383,15 @@ export class Gridview implements IDisposable { return; } + const hiddenOnMaximize = this._maximizedNode.hiddenOnMaximize; + function showViewsInReverseOrder(parent: BranchNode): void { for (let index = parent.children.length - 1; index >= 0; index--) { const child = parent.children[index]; if (child instanceof LeafNode) { - parent.setChildVisible(index, true); + if (!hiddenOnMaximize.includes(child)) { + parent.setChildVisible(index, true); + } } else { showViewsInReverseOrder(child); } @@ -395,8 +407,8 @@ export class Gridview implements IDisposable { public serialize(): SerializedGridview { if (this.hasMaximizedView()) { /** - * do not persist maximized view state but we must first exit any maximized views - * before serialization to ensure the correct dimensions are persisted + * do not persist maximized view state + * firstly exit any maximized views to ensure the correct dimensions are persisted */ this.exitMaximizedView(); } diff --git a/packages/dockview-core/src/gridview/gridviewPanel.ts b/packages/dockview-core/src/gridview/gridviewPanel.ts index b35758287..c2573bde8 100644 --- a/packages/dockview-core/src/gridview/gridviewPanel.ts +++ b/packages/dockview-core/src/gridview/gridviewPanel.ts @@ -16,6 +16,7 @@ import { import { LayoutPriority } from '../splitview/splitview'; import { Emitter, Event } from '../events'; import { IViewSize } from './gridview'; +import { BaseGrid, IGridPanelView } from './baseComponentGridview'; export interface GridviewInitParameters extends PanelInitParameters { minimumWidth?: number; @@ -24,7 +25,7 @@ export interface GridviewInitParameters extends PanelInitParameters { maximumHeight?: number; priority?: LayoutPriority; snap?: boolean; - accessor: GridviewComponent; + accessor: BaseGrid; isVisible?: boolean; } @@ -157,14 +158,16 @@ export abstract class GridviewPanel< this.api.initialize(this); // TODO: required to by-pass 'super before this' requirement this.addDisposables( - this.api.onVisibilityChange((event) => { - const { isVisible } = event; + this.api.onDidHiddenChange((event) => { + const { isHidden } = event; const { accessor } = this._params as GridviewInitParameters; - accessor.setVisible(this, isVisible); + + accessor.setVisible(this, !isHidden); }), this.api.onActiveChange(() => { const { accessor } = this._params as GridviewInitParameters; - accessor.setActive(this); + + accessor.doSetGroupActive(this); }), this.api.onDidConstraintsChangeInternal((event) => { if ( diff --git a/packages/dockview-core/src/index.ts b/packages/dockview-core/src/index.ts index 3f5c8bf70..62d415174 100644 --- a/packages/dockview-core/src/index.ts +++ b/packages/dockview-core/src/index.ts @@ -11,8 +11,6 @@ export { CompositeDisposable as DockviewCompositeDisposable, } from './lifecycle'; -export { PopoutWindow } from './popoutWindow'; - export * from './panel/types'; export * from './panel/componentFactory'; diff --git a/packages/dockview-core/src/lifecycle.ts b/packages/dockview-core/src/lifecycle.ts index 69936fff2..439b181c1 100644 --- a/packages/dockview-core/src/lifecycle.ts +++ b/packages/dockview-core/src/lifecycle.ts @@ -24,10 +24,10 @@ export namespace Disposable { } export class CompositeDisposable { - private readonly _disposables: IDisposable[]; + private _disposables: IDisposable[]; private _isDisposed = false; - protected get isDisposed(): boolean { + get isDisposed(): boolean { return this._isDisposed; } @@ -40,9 +40,13 @@ export class CompositeDisposable { } public dispose(): void { - this._disposables.forEach((arg) => arg.dispose()); + if (this._isDisposed) { + return; + } this._isDisposed = true; + this._disposables.forEach((arg) => arg.dispose()); + this._disposables = []; } } diff --git a/packages/dockview-core/src/popoutWindow.ts b/packages/dockview-core/src/popoutWindow.ts index 33d3a1434..b289ba645 100644 --- a/packages/dockview-core/src/popoutWindow.ts +++ b/packages/dockview-core/src/popoutWindow.ts @@ -94,7 +94,6 @@ export class PopoutWindow extends CompositeDisposable { return null; } - const disposable = new CompositeDisposable(); this._window = { value: externalWindow, disposable }; @@ -108,17 +107,14 @@ export class PopoutWindow extends CompositeDisposable { * @see https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event */ this.close(); - }), - addDisposableWindowListener(externalWindow, 'beforeunload', () => { - /** - * @see https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event - */ - this.close(); }) ); const container = this.createPopoutWindowContainer(); - container.classList.add(this.className); + + if (this.className) { + container.classList.add(this.className); + } this.options.onDidOpen?.({ id: this.target, @@ -126,6 +122,11 @@ export class PopoutWindow extends CompositeDisposable { }); return new Promise((resolve) => { + externalWindow.addEventListener('unload', (e) => { + // if page fails to load before unloading + // this.close(); + }); + externalWindow.addEventListener('load', () => { /** * @see https://developer.mozilla.org/en-US/docs/Web/API/Window/load_event @@ -134,12 +135,25 @@ export class PopoutWindow extends CompositeDisposable { const externalDocument = externalWindow.document; externalDocument.title = document.title; - // externalDocument.body.replaceChildren(container); externalDocument.body.appendChild(container); - externalDocument.body.classList.add(this.className); addStyles(externalDocument, window.document.styleSheets); + /** + * 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( + externalWindow, + 'beforeunload', + () => { + /** + * @see https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event + */ + this.close(); + } + ); + resolve(container); }); }); diff --git a/packages/dockview-core/src/splitview/splitviewPanel.ts b/packages/dockview-core/src/splitview/splitviewPanel.ts index 4782e8c30..9cafdb801 100644 --- a/packages/dockview-core/src/splitview/splitviewPanel.ts +++ b/packages/dockview-core/src/splitview/splitviewPanel.ts @@ -89,10 +89,10 @@ export abstract class SplitviewPanel this.addDisposables( this._onDidChange, - this.api.onVisibilityChange((event) => { - const { isVisible } = event; + this.api.onDidHiddenChange((event) => { + const { isHidden } = event; const { accessor } = this._params as PanelViewInitParameters; - accessor.setVisible(this, isVisible); + accessor.setVisible(this, !isHidden); }), this.api.onActiveChange(() => { const { accessor } = this._params as PanelViewInitParameters; diff --git a/packages/dockview/src/gridview/view.ts b/packages/dockview/src/gridview/view.ts index fce6f0690..35ed132df 100644 --- a/packages/dockview/src/gridview/view.ts +++ b/packages/dockview/src/gridview/view.ts @@ -3,6 +3,7 @@ import { GridviewPanel, GridviewInitParameters, IFrameworkPart, + GridviewComponent, } from 'dockview-core'; import { ReactPart, ReactPortalStore } from '../react'; import { IGridviewPanelProps } from './gridview'; @@ -25,8 +26,10 @@ export class ReactGridPanelView extends GridviewPanel { { params: this._params?.params ?? {}, api: this.api, + // TODO: fix casting hack containerApi: new GridviewApi( - (this._params as GridviewInitParameters).accessor + (this._params as GridviewInitParameters) + .accessor as GridviewComponent ), } ); From e1849f72a54446f23a96812be38d33d4af59ee7e Mon Sep 17 00:00:00 2001 From: mathuo <6710312+mathuo@users.noreply.github.com> Date: Mon, 29 Jan 2024 22:40:43 +0000 Subject: [PATCH 11/19] chore: internals renaming --- packages/dockview-core/src/gridview/branchNode.ts | 10 +++++----- packages/dockview-core/src/gridview/gridview.ts | 4 ++-- packages/dockview-core/src/splitview/splitview.scss | 2 +- packages/dockview-core/src/splitview/splitview.ts | 6 +++--- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/dockview-core/src/gridview/branchNode.ts b/packages/dockview-core/src/gridview/branchNode.ts index d0e449e01..511f9398e 100644 --- a/packages/dockview-core/src/gridview/branchNode.ts +++ b/packages/dockview-core/src/gridview/branchNode.ts @@ -131,12 +131,12 @@ export class BranchNode extends CompositeDisposable implements IView { return LayoutPriority.Normal; } - get locked(): boolean { - return this.splitview.locked; + get disabled(): boolean { + return this.splitview.disabled; } - set locked(value: boolean) { - this.splitview.locked = value; + set disabled(value: boolean) { + this.splitview.disabled = value; } constructor( @@ -187,7 +187,7 @@ export class BranchNode extends CompositeDisposable implements IView { }); } - this.locked = disabled; + this.disabled = disabled; this.addDisposables( this._onDidChange, diff --git a/packages/dockview-core/src/gridview/gridview.ts b/packages/dockview-core/src/gridview/gridview.ts index 8af0cc0e5..f73b75bd8 100644 --- a/packages/dockview-core/src/gridview/gridview.ts +++ b/packages/dockview-core/src/gridview/gridview.ts @@ -42,7 +42,7 @@ function flipNode( node.styles, size, orthogonalSize, - node.locked + node.disabled ); let totalSize = 0; @@ -350,7 +350,7 @@ export class Gridview implements IDisposable { const node = branch.pop(); if (node instanceof BranchNode) { - node.locked = value; + node.disabled = value; branch.push(...node.children); } } diff --git a/packages/dockview-core/src/splitview/splitview.scss b/packages/dockview-core/src/splitview/splitview.scss index 82c8a7ced..a9f9644e6 100644 --- a/packages/dockview-core/src/splitview/splitview.scss +++ b/packages/dockview-core/src/splitview/splitview.scss @@ -25,7 +25,7 @@ height: 100%; width: 100%; - &.dv-splitview-locked { + &.dv-splitview-disabled { & > .sash-container > .sash { pointer-events: none; } diff --git a/packages/dockview-core/src/splitview/splitview.ts b/packages/dockview-core/src/splitview/splitview.ts index 4c5ef3707..465958fe6 100644 --- a/packages/dockview-core/src/splitview/splitview.ts +++ b/packages/dockview-core/src/splitview/splitview.ts @@ -201,14 +201,14 @@ export class Splitview { this.updateSashEnablement(); } - get locked(): boolean { + get disabled(): boolean { return this._disabled; } - set locked(value: boolean) { + set disabled(value: boolean) { this._disabled = value; - toggleClass(this.element, 'dv-splitview-locked', value); + toggleClass(this.element, 'dv-splitview-disabled', value); } constructor( From 0bca63b550dd66fd7922afaabb4b39d5d300752f Mon Sep 17 00:00:00 2001 From: mathuo <6710312+mathuo@users.noreply.github.com> Date: Tue, 30 Jan 2024 17:41:04 +0000 Subject: [PATCH 12/19] feat: popout group enhancements --- .../dockview/components/panel/content.spec.ts | 39 ++++++----- .../dockview/dockviewComponent.spec.ts | 67 ++++++++++--------- .../src/dockview/components/panel/content.ts | 4 +- .../src/dockview/dockviewComponent.ts | 45 +++++++++++-- .../src/dockview/dockviewGroupPanelModel.ts | 22 ++++++ 5 files changed, 121 insertions(+), 56 deletions(-) diff --git a/packages/dockview-core/src/__tests__/dockview/components/panel/content.spec.ts b/packages/dockview-core/src/__tests__/dockview/components/panel/content.spec.ts index eacad7b9a..670c981d8 100644 --- a/packages/dockview-core/src/__tests__/dockview/components/panel/content.spec.ts +++ b/packages/dockview-core/src/__tests__/dockview/components/panel/content.spec.ts @@ -62,16 +62,19 @@ describe('contentContainer', () => { const disposable = new CompositeDisposable(); - const dockviewComponent = jest.fn(() => { - return { - renderer: 'onlyWhenVisibile', - overlayRenderContainer: new OverlayRenderContainer( - document.createElement('div') - ), - } as DockviewComponent; - }); + const overlayRenderContainer = new OverlayRenderContainer( + document.createElement('div') + ); - const cut = new ContentContainer(dockviewComponent(), jest.fn() as any); + const cut = new ContentContainer( + fromPartial({ + renderer: 'onlyWhenVisibile', + overlayRenderContainer, + }), + fromPartial({ + renderContainer: overlayRenderContainer, + }) + ); disposable.addDisposables( cut.onDidFocus(() => { @@ -84,12 +87,12 @@ describe('contentContainer', () => { const contentRenderer = new TestContentRenderer('id-1'); - const panel = { + const panel = fromPartial({ view: { content: contentRenderer, - } as Partial, + }, api: { renderer: 'onlyWhenVisibile' }, - } as Partial; + }); cut.openPanel(panel as IDockviewPanel); @@ -151,13 +154,17 @@ describe('contentContainer', () => { }); test("that panels renderered as 'onlyWhenVisibile' are removed when closed", () => { + const overlayRenderContainer = fromPartial({ + detatch: jest.fn(), + }); + const cut = new ContentContainer( fromPartial({ - overlayRenderContainer: { - detatch: jest.fn(), - }, + overlayRenderContainer, }), - fromPartial({}) + fromPartial({ + renderContainer: overlayRenderContainer, + }) ); const panel1 = fromPartial({ diff --git a/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts b/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts index 49c5ff221..51482f0ed 100644 --- a/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts +++ b/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts @@ -4434,44 +4434,12 @@ describe('dockviewComponent', () => { cb(); } }), + removeEventListener: jest.fn(), close: jest.fn(), }) ); }); - test('that can remove a popout group', async () => { - const container = document.createElement('div'); - - const dockview = new DockviewComponent({ - parentElement: container, - components: { - default: PanelContentPartTest, - }, - tabComponents: { - test_tab_id: PanelTabPartTest, - }, - orientation: Orientation.HORIZONTAL, - }); - - dockview.layout(1000, 500); - - const panel1 = dockview.addPanel({ - id: 'panel_1', - component: 'default', - }); - - await dockview.addPopoutGroup(panel1); - - expect(dockview.panels.length).toBe(1); - expect(dockview.groups.length).toBe(2); - expect(panel1.api.group.api.location.type).toBe('popout'); - - dockview.removePanel(panel1); - - expect(dockview.panels.length).toBe(0); - expect(dockview.groups.length).toBe(0); - }); - test('add a popout group', async () => { const container = document.createElement('div'); @@ -4511,6 +4479,39 @@ describe('dockviewComponent', () => { expect(dockview.panels.length).toBe(2); }); + test('that can remove a popout group', async () => { + const container = document.createElement('div'); + + const dockview = new DockviewComponent({ + parentElement: container, + components: { + default: PanelContentPartTest, + }, + tabComponents: { + test_tab_id: PanelTabPartTest, + }, + orientation: Orientation.HORIZONTAL, + }); + + dockview.layout(1000, 500); + + const panel1 = dockview.addPanel({ + id: 'panel_1', + component: 'default', + }); + + await dockview.addPopoutGroup(panel1); + + expect(dockview.panels.length).toBe(1); + expect(dockview.groups.length).toBe(2); + expect(panel1.api.group.api.location.type).toBe('popout'); + + dockview.removePanel(panel1); + + expect(dockview.panels.length).toBe(0); + expect(dockview.groups.length).toBe(0); + }); + test('move from fixed to popout group and back', async () => { const container = document.createElement('div'); diff --git a/packages/dockview-core/src/dockview/components/panel/content.ts b/packages/dockview-core/src/dockview/components/panel/content.ts index 9d9e3f2ca..933415283 100644 --- a/packages/dockview-core/src/dockview/components/panel/content.ts +++ b/packages/dockview-core/src/dockview/components/panel/content.ts @@ -133,7 +133,7 @@ export class ContentContainer switch (panel.api.renderer) { case 'onlyWhenVisibile': - this.accessor.overlayRenderContainer.detatch(panel); + this.group.renderContainer.detatch(panel); if (this.panel) { if (doRender) { this._element.appendChild( @@ -149,7 +149,7 @@ export class ContentContainer ) { this._element.removeChild(panel.view.content.element); } - container = this.accessor.overlayRenderContainer.attach({ + container = this.group.renderContainer.attach({ panel, referenceContainer: this, }); diff --git a/packages/dockview-core/src/dockview/dockviewComponent.ts b/packages/dockview-core/src/dockview/dockviewComponent.ts index 400205cc6..926abd40c 100644 --- a/packages/dockview-core/src/dockview/dockviewComponent.ts +++ b/packages/dockview-core/src/dockview/dockviewComponent.ts @@ -13,7 +13,7 @@ import { import { tail, sequenceEquals, remove } from '../array'; import { DockviewPanel, IDockviewPanel } from './dockviewPanel'; import { CompositeDisposable, Disposable, IDisposable } from '../lifecycle'; -import { Event, Emitter } from '../events'; +import { Event, Emitter, addDisposableWindowListener } from '../events'; import { Watermark } from './components/watermark/watermark'; import { IWatermarkRenderer, GroupviewPanelState } from './types'; import { sequentialNumberGenerator } from '../math'; @@ -561,12 +561,15 @@ export class DockviewComponent from: DockviewGroupPanel; to: DockviewGroupPanel; }) { + const activePanel = options.from.activePanel; const panels = [...options.from.panels].map((panel) => options.from.model.removePanel(panel) ); panels.forEach((panel) => { - options.to.model.openPanel(panel); + options.to.model.openPanel(panel, { + skipSetPanelActive: activePanel !== panel, + }); }); } @@ -624,10 +627,18 @@ export class DockviewComponent return; } + const gready = document.createElement('div'); + gready.className = 'dv-overlay-render-container'; + + const overlayRenderContainer = new OverlayRenderContainer( + gready + ); + const referenceGroup = item instanceof DockviewPanel ? item.group : item; const group = this.createGroup({ id: groupId }); + group.model.renderContainer = overlayRenderContainer; if (item instanceof DockviewPanel) { const panel = referenceGroup.model.removePanel(item); @@ -637,9 +648,13 @@ export class DockviewComponent from: referenceGroup, to: group, }); - referenceGroup.api.setHidden(false); + referenceGroup.api.setHidden(true); } + popoutContainer.classList.add('dv-dockview'); + popoutContainer.style.overflow = 'hidden'; + popoutContainer.appendChild(gready); + popoutContainer.appendChild(group.element); group.model.location = { @@ -655,6 +670,19 @@ export class DockviewComponent }; popoutWindowDisposable.addDisposables( + /** + * 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.innerWidth, window.innerHeight); + } + ), + overlayRenderContainer, Disposable.from(() => { if (this.getPanel(referenceGroup.id)) { moveGroupWithoutDestroying({ @@ -666,12 +694,16 @@ export class DockviewComponent referenceGroup.api.setHidden(false); } - this.doRemoveGroup(group); + this.doRemoveGroup(group, { + skipPopoutAssociated: true, + }); } else { const removedGroup = this.doRemoveGroup(group, { skipDispose: true, skipActive: true, }); + removedGroup.model.renderContainer = + this.overlayRenderContainer; removedGroup.model.location = { type: 'grid' }; this.doAddGroup(removedGroup, [0]); } @@ -1484,6 +1516,7 @@ export class DockviewComponent | { skipActive?: boolean; skipDispose?: boolean; + skipPopoutAssociated?: boolean; } | undefined ): DockviewGroupPanel { @@ -1523,7 +1556,9 @@ export class DockviewComponent if (selectedGroup) { if (!options?.skipDispose) { - this.doRemoveGroup(selectedGroup.referenceGroup); + if (!options?.skipPopoutAssociated) { + this.removeGroup(selectedGroup.referenceGroup); + } selectedGroup.popoutGroup.dispose(); this._groups.delete(group.id); diff --git a/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts b/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts index 44a912c55..12d548681 100644 --- a/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts +++ b/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts @@ -26,6 +26,7 @@ import { DockviewDropTargets, IWatermarkRenderer } from './types'; import { DockviewGroupPanel } from './dockviewGroupPanel'; import { IDockviewPanel } from './dockviewPanel'; import { IHeaderActionsRenderer } from './options'; +import { OverlayRenderContainer } from '../overlayRenderContainer'; interface GroupMoveEvent { groupId: string; @@ -421,6 +422,27 @@ export class DockviewGroupPanelModel ); } + private _overwriteRenderContainer: OverlayRenderContainer | null = null; + + set renderContainer(value: OverlayRenderContainer | null) { + this.panels.forEach((panel) => { + this.renderContainer.detatch(panel); + }); + + this._overwriteRenderContainer = value; + + this.panels.forEach((panel) => { + this.rerender(panel); + }); + } + + get renderContainer(): OverlayRenderContainer { + return ( + this._overwriteRenderContainer ?? + this.accessor.overlayRenderContainer + ); + } + initialize(): void { if (this.options.panels) { this.options.panels.forEach((panel) => { From 926b68826788b6f381897e86e02e07659f7e86c2 Mon Sep 17 00:00:00 2001 From: mathuo <6710312+mathuo@users.noreply.github.com> Date: Tue, 30 Jan 2024 19:41:05 +0000 Subject: [PATCH 13/19] chore: stop nightly docs job --- .github/workflows/deploy-docs.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index 595869a12..14d6189cc 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -2,8 +2,6 @@ name: Deploy Docs on: workflow_dispatch: - schedule: - - cron: '0 3 * * *' # every day at 3 am UTC jobs: deploy-nightly-demo-app: From 66d96cf6ae5a7e349fc3cf8c9efaa097e5f0ecec Mon Sep 17 00:00:00 2001 From: mathuo <6710312+mathuo@users.noreply.github.com> Date: Tue, 30 Jan 2024 19:43:27 +0000 Subject: [PATCH 14/19] chore: upgrade builds to node v20 --- .github/workflows/deploy-docs.yml | 2 +- .github/workflows/main.yml | 2 +- .github/workflows/publish.yml | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index 14d6189cc..4086652d6 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -12,7 +12,7 @@ jobs: - name: Use Node.js uses: actions/setup-node@v3 with: - node-version: '18.x' + node-version: '20.x' - uses: actions/cache@v3 with: diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ce489b923..e651f5912 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -14,7 +14,7 @@ jobs: - name: Use Node.js uses: actions/setup-node@v3 with: - node-version: '18.x' + node-version: '20.x' - uses: actions/cache@v3 with: diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 2eb2ffa1d..bb21bec62 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -18,7 +18,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v3 with: - node-version: '18.x' + node-version: '20.x' registry-url: 'https://registry.npmjs.org' - uses: actions/cache@v3 @@ -46,7 +46,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v3 with: - node-version: '18.x' + node-version: '20.x' registry-url: 'https://registry.npmjs.org' - uses: actions/cache@v3 From 0b1a09d910ad5a6c357e2ab49475c4ded9e3ba05 Mon Sep 17 00:00:00 2001 From: mathuo <6710312+mathuo@users.noreply.github.com> Date: Mon, 29 Jan 2024 23:13:44 +0000 Subject: [PATCH 15/19] feat: replace transform with top,left,width,height --- .../src/__tests__/dnd/droptarget.spec.ts | 88 +++++++++++++------ packages/dockview-core/src/dnd/droptarget.ts | 41 ++++++--- 2 files changed, 89 insertions(+), 40 deletions(-) diff --git a/packages/dockview-core/src/__tests__/dnd/droptarget.spec.ts b/packages/dockview-core/src/__tests__/dnd/droptarget.spec.ts index 517fa40aa..3f04a800e 100644 --- a/packages/dockview-core/src/__tests__/dnd/droptarget.spec.ts +++ b/packages/dockview-core/src/__tests__/dnd/droptarget.spec.ts @@ -171,18 +171,37 @@ describe('droptarget', () => { createOffsetDragOverEvent({ clientX: 19, clientY: 0 }) ); + function check( + element: HTMLElement, + box: { + left: string; + top: string; + width: string; + height: string; + } + ) { + expect(element.style.top).toBe(box.top); + expect(element.style.left).toBe(box.left); + expect(element.style.width).toBe(box.width); + expect(element.style.height).toBe(box.height); + } + viewQuery = element.querySelectorAll( '.drop-target > .drop-target-dropzone > .drop-target-selection' ); expect(viewQuery.length).toBe(1); expect(droptarget.state).toBe('left'); - expect( - ( - element - .getElementsByClassName('drop-target-selection') - .item(0) as HTMLDivElement - ).style.transform - ).toBe('translateX(-25%) scaleX(0.5)'); + check( + element + .getElementsByClassName('drop-target-selection') + .item(0) as HTMLDivElement, + { + top: '0px', + left: '0px', + width: '50%', + height: '100%', + } + ); fireEvent( target, @@ -194,13 +213,17 @@ describe('droptarget', () => { ); expect(viewQuery.length).toBe(1); expect(droptarget.state).toBe('top'); - expect( - ( - element - .getElementsByClassName('drop-target-selection') - .item(0) as HTMLDivElement - ).style.transform - ).toBe('translateY(-25%) scaleY(0.5)'); + check( + element + .getElementsByClassName('drop-target-selection') + .item(0) as HTMLDivElement, + { + top: '0px', + left: '0px', + width: '100%', + height: '50%', + } + ); fireEvent( target, @@ -212,13 +235,17 @@ describe('droptarget', () => { ); expect(viewQuery.length).toBe(1); expect(droptarget.state).toBe('bottom'); - expect( - ( - element - .getElementsByClassName('drop-target-selection') - .item(0) as HTMLDivElement - ).style.transform - ).toBe('translateY(25%) scaleY(0.5)'); + check( + element + .getElementsByClassName('drop-target-selection') + .item(0) as HTMLDivElement, + { + top: '50%', + left: '0px', + width: '100%', + height: '50%', + } + ); fireEvent( target, @@ -230,14 +257,17 @@ describe('droptarget', () => { ); expect(viewQuery.length).toBe(1); expect(droptarget.state).toBe('right'); - expect( - ( - element - .getElementsByClassName('drop-target-selection') - .item(0) as HTMLDivElement - ).style.transform - ).toBe('translateX(25%) scaleX(0.5)'); - + check( + element + .getElementsByClassName('drop-target-selection') + .item(0) as HTMLDivElement, + { + top: '0px', + left: '50%', + width: '50%', + height: '100%', + } + ); fireEvent( target, createOffsetDragOverEvent({ clientX: 100, clientY: 50 }) diff --git a/packages/dockview-core/src/dnd/droptarget.ts b/packages/dockview-core/src/dnd/droptarget.ts index d5c7e290c..2f2868fbc 100644 --- a/packages/dockview-core/src/dnd/droptarget.ts +++ b/packages/dockview-core/src/dnd/droptarget.ts @@ -304,24 +304,43 @@ export class Droptarget extends CompositeDisposable { } } - const translate = (1 - size) / 2; - const scale = size; - - let transform: string; + const box = { top: '0px', left: '0px', width: '100%', height: '100%' }; + /** + * You can also achieve the overlay placement using the transform CSS property + * to translate and scale the element however this has the undesired effect of + * 'skewing' the element. Comment left here for anybody that ever revisits this. + * + * @see https://developer.mozilla.org/en-US/docs/Web/CSS/transform + * + * right + * translateX(${100 * (1 - size) / 2}%) scaleX(${scale}) + * + * left + * translateX(-${100 * (1 - size) / 2}%) scaleX(${scale}) + * + * top + * translateY(-${100 * (1 - size) / 2}%) scaleY(${scale}) + * + * bottom + * translateY(${100 * (1 - size) / 2}%) scaleY(${scale}) + */ if (rightClass) { - transform = `translateX(${100 * translate}%) scaleX(${scale})`; + box.left = `${100 * (1 - size)}%`; + box.width = `${100 * size}%`; } else if (leftClass) { - transform = `translateX(-${100 * translate}%) scaleX(${scale})`; + box.width = `${100 * size}%`; } else if (topClass) { - transform = `translateY(-${100 * translate}%) scaleY(${scale})`; + box.height = `${100 * size}%`; } else if (bottomClass) { - transform = `translateY(${100 * translate}%) scaleY(${scale})`; - } else { - transform = ''; + box.top = `${100 * size}%`; + box.height = `${100 * size}%`; } - this.overlayElement.style.transform = transform; + this.overlayElement.style.top = box.top; + this.overlayElement.style.left = box.left; + this.overlayElement.style.width = box.width; + this.overlayElement.style.height = box.height; toggleClass( this.overlayElement, From aeccce639ce17726388e3deaa7795b20c855c0d0 Mon Sep 17 00:00:00 2001 From: mathuo <6710312+mathuo@users.noreply.github.com> Date: Tue, 30 Jan 2024 19:56:40 +0000 Subject: [PATCH 16/19] chore: migrate to v4 build actions --- .github/workflows/codeql-analysis.yml | 2 +- .github/workflows/deploy-docs.yml | 6 +++--- .github/workflows/main.yml | 6 +++--- .github/workflows/publish.yml | 8 ++++---- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 035c9224f..7bc290aa8 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -30,7 +30,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index 4086652d6..9b43e8fdc 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -8,13 +8,13 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout 🛎ī¸ - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Use Node.js - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: '20.x' - - uses: actions/cache@v3 + - uses: actions/cache@v4 with: path: ~/.npm key: ${{ runner.os }}-node-${{ hashFiles('**/yarn.lock') }} diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e651f5912..b01f4356c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -7,16 +7,16 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 # might be required for sonar to work correctly with: fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis - name: Use Node.js - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: '20.x' - - uses: actions/cache@v3 + - uses: actions/cache@v4 with: path: ~/.npm key: ${{ runner.os }}-node-${{ hashFiles('**/yarn.lock') }} diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index bb21bec62..23b0df832 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -16,12 +16,12 @@ jobs: id-token: write steps: - uses: actions/checkout@v4 - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: node-version: '20.x' registry-url: 'https://registry.npmjs.org' - - uses: actions/cache@v3 + - uses: actions/cache@v4 with: path: ~/.npm key: ${{ runner.os }}-node-${{ hashFiles('**/yarn.lock') }} @@ -44,12 +44,12 @@ jobs: id-token: write steps: - uses: actions/checkout@v4 - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: node-version: '20.x' registry-url: 'https://registry.npmjs.org' - - uses: actions/cache@v3 + - uses: actions/cache@v4 with: path: ~/.npm key: ${{ runner.os }}-node-${{ hashFiles('**/yarn.lock') }} From 9b641f64be8327bb0f8983fac469a40578dfb97a Mon Sep 17 00:00:00 2001 From: mathuo <6710312+mathuo@users.noreply.github.com> Date: Tue, 30 Jan 2024 19:57:46 +0000 Subject: [PATCH 17/19] chore: migrate to codeql v3 --- .github/workflows/codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 7bc290aa8..3f21a3b3b 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -34,7 +34,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v1 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -45,7 +45,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v1 + uses: github/codeql-action/autobuild@v3 # ℹī¸ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -59,4 +59,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 + uses: github/codeql-action/analyze@v3 From c4f46a190ae288ab6f1545f19ffa6c74e6ce3be2 Mon Sep 17 00:00:00 2001 From: mathuo <6710312+mathuo@users.noreply.github.com> Date: Wed, 31 Jan 2024 21:32:01 +0000 Subject: [PATCH 18/19] feat: fix popout group persistance --- .../src/dockview/dockviewComponent.ts | 64 +++++++++++++------ 1 file changed, 45 insertions(+), 19 deletions(-) diff --git a/packages/dockview-core/src/dockview/dockviewComponent.ts b/packages/dockview-core/src/dockview/dockviewComponent.ts index 926abd40c..7ecbd55f8 100644 --- a/packages/dockview-core/src/dockview/dockviewComponent.ts +++ b/packages/dockview-core/src/dockview/dockviewComponent.ts @@ -110,6 +110,7 @@ export interface SerializedFloatingGroup { export interface SerializedPopoutGroup { data: GroupPanelViewState; + gridReferenceGroup: string; position: Box | null; } @@ -541,17 +542,21 @@ export class DockviewComponent } addPopoutGroup( - item: DockviewPanel | DockviewGroupPanel, + itemToPopout: DockviewPanel | DockviewGroupPanel, options?: { skipRemoveGroup?: boolean; position?: Box; popoutUrl?: string; onDidOpen?: (event: { id: string; window: Window }) => void; onWillClose?: (event: { id: string; window: Window }) => void; + overridePopoutGroup?: DockviewGroupPanel; } ): Promise { - if (item instanceof DockviewPanel && item.group.size === 1) { - return this.addPopoutGroup(item.group); + if ( + itemToPopout instanceof DockviewPanel && + itemToPopout.group.size === 1 + ) { + return this.addPopoutGroup(itemToPopout.group); } const theme = getDockviewTheme(this.gridview.element); @@ -578,21 +583,22 @@ export class DockviewComponent return options.position; } - if (item instanceof DockviewGroupPanel) { - return item.element.getBoundingClientRect(); + if (itemToPopout instanceof DockviewGroupPanel) { + return itemToPopout.element.getBoundingClientRect(); } - if (item.group) { - return item.group.element.getBoundingClientRect(); + if (itemToPopout.group) { + return itemToPopout.group.element.getBoundingClientRect(); } return element.getBoundingClientRect(); } const box: Box = getBox(); - const groupId = this.getNextGroupId(); //item.id; + const groupId = + options?.overridePopoutGroup?.id ?? this.getNextGroupId(); //item.id; - item.api.setHidden(true); + itemToPopout.api.setHidden(true); const _window = new PopoutWindow( `${this.id}-${groupId}`, // unique id @@ -635,13 +641,18 @@ export class DockviewComponent ); const referenceGroup = - item instanceof DockviewPanel ? item.group : item; + itemToPopout instanceof DockviewPanel + ? itemToPopout.group + : itemToPopout; - const group = this.createGroup({ id: groupId }); + const group = + options?.overridePopoutGroup ?? + this.createGroup({ id: groupId }); group.model.renderContainer = overlayRenderContainer; - if (item instanceof DockviewPanel) { - const panel = referenceGroup.model.removePanel(item); + if (itemToPopout instanceof DockviewPanel) { + const panel = + referenceGroup.model.removePanel(itemToPopout); group.model.openPanel(panel); } else { moveGroupWithoutDestroying({ @@ -1015,6 +1026,7 @@ export class DockviewComponent (group) => { return { data: group.popoutGroup.toJSON() as GroupPanelViewState, + gridReferenceGroup: group.referenceGroup.id, position: group.window.dimensions(), }; } @@ -1142,14 +1154,26 @@ export class DockviewComponent const serializedPopoutGroups = data.popoutGroups ?? []; for (const serializedPopoutGroup of serializedPopoutGroups) { - const { data, position } = serializedPopoutGroup; + const { data, position, gridReferenceGroup } = + serializedPopoutGroup; const group = createGroupFromSerializedState(data); - this.addPopoutGroup(group, { - skipRemoveGroup: true, - position: position ?? undefined, - }); + if (!gridReferenceGroup) { + /** + * workaround to handle <= v1.9.2 + */ + this.doAddGroup(group, [0]); + } + + this.addPopoutGroup( + this.getPanel(gridReferenceGroup) ?? group, + { + skipRemoveGroup: true, + position: position ?? undefined, + overridePopoutGroup: group, + } + ); } for (const floatingGroup of this._floatingGroups) { @@ -1557,7 +1581,9 @@ export class DockviewComponent if (selectedGroup) { if (!options?.skipDispose) { if (!options?.skipPopoutAssociated) { - this.removeGroup(selectedGroup.referenceGroup); + if (this._groups.has(selectedGroup.referenceGroup.id)) { + this.removeGroup(selectedGroup.referenceGroup); + } } selectedGroup.popoutGroup.dispose(); From 9e57a8691a508f74005341be9bcb66376e4967db Mon Sep 17 00:00:00 2001 From: mathuo <6710312+mathuo@users.noreply.github.com> Date: Thu, 1 Feb 2024 19:51:07 +0000 Subject: [PATCH 19/19] feat: popout persistance logic --- .../src/dockview/dockviewComponent.ts | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/dockview-core/src/dockview/dockviewComponent.ts b/packages/dockview-core/src/dockview/dockviewComponent.ts index 7ecbd55f8..192eefc7e 100644 --- a/packages/dockview-core/src/dockview/dockviewComponent.ts +++ b/packages/dockview-core/src/dockview/dockviewComponent.ts @@ -110,7 +110,7 @@ export interface SerializedFloatingGroup { export interface SerializedPopoutGroup { data: GroupPanelViewState; - gridReferenceGroup: string; + gridReferenceGroup?: string; position: Box | null; } @@ -339,7 +339,7 @@ export class DockviewComponent private readonly _popoutGroups: { window: PopoutWindow; popoutGroup: DockviewGroupPanel; - referenceGroup: DockviewGroupPanel; + referenceGroup: string; disposable: IDisposable; }[] = []; private readonly _rootDropTarget: Droptarget; @@ -676,7 +676,7 @@ export class DockviewComponent const value = { window: _window, popoutGroup: group, - referenceGroup, + referenceGroup: referenceGroup.id, disposable: popoutWindowDisposable, }; @@ -1026,7 +1026,7 @@ export class DockviewComponent (group) => { return { data: group.popoutGroup.toJSON() as GroupPanelViewState, - gridReferenceGroup: group.referenceGroup.id, + gridReferenceGroup: group.referenceGroup, position: group.window.dimensions(), }; } @@ -1159,19 +1159,16 @@ export class DockviewComponent const group = createGroupFromSerializedState(data); - if (!gridReferenceGroup) { - /** - * workaround to handle <= v1.9.2 - */ - this.doAddGroup(group, [0]); - } - this.addPopoutGroup( - this.getPanel(gridReferenceGroup) ?? group, + (gridReferenceGroup + ? this.getPanel(gridReferenceGroup) + : undefined) ?? group, { skipRemoveGroup: true, position: position ?? undefined, - overridePopoutGroup: group, + overridePopoutGroup: gridReferenceGroup + ? group + : undefined, } ); } @@ -1581,8 +1578,11 @@ export class DockviewComponent if (selectedGroup) { if (!options?.skipDispose) { if (!options?.skipPopoutAssociated) { - if (this._groups.has(selectedGroup.referenceGroup.id)) { - this.removeGroup(selectedGroup.referenceGroup); + const refGroup = this.getPanel( + selectedGroup.referenceGroup + ); + if (refGroup) { + this.removeGroup(refGroup); } }