diff --git a/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts b/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts index 8091ff768..15461e713 100644 --- a/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts +++ b/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts @@ -8,7 +8,7 @@ import { PanelUpdateEvent } from '../../panel/types'; import { Orientation } from '../../splitview/splitview'; import { CompositeDisposable } from '../../lifecycle'; import { Emitter } from '../../events'; -import { IDockviewPanel } from '../../dockview/dockviewPanel'; +import { DockviewPanel, IDockviewPanel } from '../../dockview/dockviewPanel'; import { DockviewGroupPanel } from '../../dockview/dockviewGroupPanel'; class PanelContentPartTest implements IContentRenderer { @@ -2619,4 +2619,32 @@ describe('dockviewComponent', () => { }, }); }); + + test('floating: group is removed', 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); + + expect(dockview.groups.length).toBe(0); + const panel = dockview.addPanel({ + id: 'panel_1', + component: 'default', + floating: true, + }); + expect(dockview.groups.length).toBe(1); + + dockview.removePanel(panel); + expect(dockview.groups.length).toBe(0); + }); }); diff --git a/packages/dockview-core/src/dnd/abstractDragHandler.ts b/packages/dockview-core/src/dnd/abstractDragHandler.ts index 2d9ce2cfa..0e452dcf3 100644 --- a/packages/dockview-core/src/dnd/abstractDragHandler.ts +++ b/packages/dockview-core/src/dnd/abstractDragHandler.ts @@ -35,10 +35,10 @@ export abstract class DragHandler extends CompositeDisposable { this.addDisposables( this._onDragStart, addDisposableListener(this.el, 'dragstart', (event) => { - if (this.isCancelled(event)) { - event.preventDefault(); - return; - } + if (this.isCancelled(event)) { + event.preventDefault(); + return; + } const iframes = [ ...getElementsByTagName('iframe'), diff --git a/packages/dockview-core/src/dnd/droptarget.scss b/packages/dockview-core/src/dnd/droptarget.scss index fbb800615..5a17b75f2 100644 --- a/packages/dockview-core/src/dnd/droptarget.scss +++ b/packages/dockview-core/src/dnd/droptarget.scss @@ -8,6 +8,7 @@ height: 100%; width: 100%; z-index: 10000; + pointer-events: none; > .drop-target-selection { position: relative; @@ -15,7 +16,9 @@ height: 100%; width: 100%; background-color: var(--dv-drag-over-background-color); - transition: top 70ms ease-out,left 70ms ease-out,width 70ms ease-out,height 70ms ease-out,opacity .15s ease-out; + transition: top 70ms ease-out, left 70ms ease-out, + width 70ms ease-out, height 70ms ease-out, + opacity 0.15s ease-out; will-change: transform; pointer-events: none; diff --git a/packages/dockview-core/src/dnd/droptarget.ts b/packages/dockview-core/src/dnd/droptarget.ts index a45cac380..7bccf6481 100644 --- a/packages/dockview-core/src/dnd/droptarget.ts +++ b/packages/dockview-core/src/dnd/droptarget.ts @@ -54,6 +54,17 @@ export type CanDisplayOverlay = | boolean | ((dragEvent: DragEvent, state: Position) => boolean); +const eventMarkTag = 'dv_droptarget_marked'; + +function markEvent(event: DragEvent): void { + (event as any)[eventMarkTag] = true; +} + +function isEventMarked(event: DragEvent) { + const value = (event as any)[eventMarkTag]; + return typeof value === 'boolean' && value; +} + export class Droptarget extends CompositeDisposable { private targetElement: HTMLElement | undefined; private overlayElement: HTMLElement | undefined; @@ -114,7 +125,7 @@ export class Droptarget extends CompositeDisposable { height ); - if (quadrant === null) { + if (isEventMarked(e) || quadrant === null) { // no drop target should be displayed this.removeDropTarget(); return; @@ -128,6 +139,8 @@ export class Droptarget extends CompositeDisposable { return; } + markEvent(e); + if (!this.targetElement) { this.targetElement = document.createElement('div'); this.targetElement.className = 'drop-target-dropzone'; diff --git a/packages/dockview-core/src/dnd/groupDragHandler.ts b/packages/dockview-core/src/dnd/groupDragHandler.ts index b47ea4a03..08be61781 100644 --- a/packages/dockview-core/src/dnd/groupDragHandler.ts +++ b/packages/dockview-core/src/dnd/groupDragHandler.ts @@ -17,7 +17,7 @@ export class GroupDragHandler extends DragHandler { } override isCancelled(_event: DragEvent): boolean { - if (this.group.model.isFloating) { + if (this.group.model.isFloating && !_event.shiftKey) { return true; } return false; diff --git a/packages/dockview-core/src/dnd/overlay.ts b/packages/dockview-core/src/dnd/overlay.ts index b10f5fa64..c4d58b454 100644 --- a/packages/dockview-core/src/dnd/overlay.ts +++ b/packages/dockview-core/src/dnd/overlay.ts @@ -1,5 +1,10 @@ import { toggleClass } from '../dom'; -import { addDisposableListener, addDisposableWindowListener } from '../events'; +import { + Emitter, + Event, + addDisposableListener, + addDisposableWindowListener, +} from '../events'; import { CompositeDisposable, MutableDisposable } from '../lifecycle'; import { clamp } from '../math'; @@ -21,6 +26,9 @@ const bringElementToFront = (() => { export class Overlay extends CompositeDisposable { private _element: HTMLElement = document.createElement('div'); + private readonly _onDidChange = new Emitter(); + readonly onDidChange: Event = this._onDidChange.event; + constructor( private readonly options: { height: number; @@ -35,6 +43,8 @@ export class Overlay extends CompositeDisposable { ) { super(); + this.addDisposables(this._onDidChange); + this.setupOverlay(); // this.setupDrag(true,this._element); this.setupResize('top'); @@ -52,6 +62,18 @@ export class Overlay extends CompositeDisposable { // this.renderWithinBoundaryConditions(); } + toJSON(): { top: number; left: number; height: number; width: number } { + const container = this.options.container.getBoundingClientRect(); + const element = this._element.getBoundingClientRect(); + + return { + top: element.top - container.top, + left: element.left - container.left, + width: element.width, + height: element.height, + }; + } + private setupResize( direction: | 'top' @@ -216,6 +238,7 @@ export class Overlay extends CompositeDisposable { }), addDisposableWindowListener(window, 'mouseup', () => { move.dispose(); + this._onDidChange.fire(); }) ); }) @@ -296,24 +319,37 @@ export class Overlay extends CompositeDisposable { ); move.dispose(); + this._onDidChange.fire(); }) ); }; this.addDisposables( move, - addDisposableListener(dragTarget, 'mousedown', (_) => { - if (_.defaultPrevented) { + addDisposableListener(dragTarget, 'mousedown', (event) => { + if ( + // event.shiftKey || + event.defaultPrevented + ) { + event.preventDefault(); return; } track(); }), - addDisposableListener(this.options.content, 'mousedown', (_) => { - if (_.shiftKey) { - track(); + addDisposableListener( + this.options.content, + 'mousedown', + (event) => { + if (event.defaultPrevented) { + return; + } + + if (event.shiftKey) { + track(); + } } - }), + ), addDisposableListener( this.options.content, 'mousedown', @@ -324,29 +360,32 @@ export class Overlay extends CompositeDisposable { ) ); + bringElementToFront(this._element); + if (connect) { track(); } } renderWithinBoundaryConditions(): void { - const rect = this.options.container.getBoundingClientRect(); - const rect2 = this._element.getBoundingClientRect(); + const containerRect = this.options.container.getBoundingClientRect(); + const overlayRect = this._element.getBoundingClientRect(); + + const xOffset = Math.max(0, overlayRect.width - this.options.minX); + const yOffset = Math.max(0, overlayRect.height - this.options.minY); const left = clamp( - Math.max(this.options.left, 0), - 0, - Math.max(0, rect.width - rect2.width) + this.options.left, + -xOffset, + Math.max(0, containerRect.width - overlayRect.width + xOffset) ); const top = clamp( - Math.max(this.options.top, 0), - 0, - Math.max(0, rect.height - rect2.height) + this.options.top, + -yOffset, + Math.max(0, containerRect.height - overlayRect.height + yOffset) ); - console.log(new Error().stack); - this._element.style.left = `${left}px`; this._element.style.top = `${top}px`; } diff --git a/packages/dockview-core/src/dockview/components/tab/tab.ts b/packages/dockview-core/src/dockview/components/tab/tab.ts index c33455172..b57d08b26 100644 --- a/packages/dockview-core/src/dockview/components/tab/tab.ts +++ b/packages/dockview-core/src/dockview/components/tab/tab.ts @@ -6,12 +6,11 @@ import { PanelTransfer, } from '../../../dnd/dataTransfer'; import { toggleClass } from '../../../dom'; -import { IDockviewComponent } from '../../dockviewComponent'; +import { DockviewComponent } from '../../dockviewComponent'; import { DockviewDropTargets, ITabRenderer } from '../../types'; import { DockviewGroupPanel } from '../../dockviewGroupPanel'; import { DroptargetEvent, Droptarget } from '../../../dnd/droptarget'; import { DragHandler } from '../../../dnd/abstractDragHandler'; -import { DockviewPanel } from '../../dockviewPanel'; export interface ITab extends IDisposable { readonly panelId: string; @@ -39,7 +38,7 @@ export class Tab extends CompositeDisposable implements ITab { constructor( public readonly panelId: string, - private readonly accessor: IDockviewComponent, + private readonly accessor: DockviewComponent, private readonly group: DockviewGroupPanel ) { super(); @@ -77,22 +76,10 @@ export class Tab extends CompositeDisposable implements ITab { this.addDisposables( addDisposableListener(this._element, 'mousedown', (event) => { - if (event.shiftKey) { - event.preventDefault(); - - const panel = this.accessor.getGroupPanel(this.panelId); - - const { top, left } = this.element.getBoundingClientRect(); - - this.accessor.addFloating(panel as DockviewPanel, { - x: left, - y: top, - }); - } - if (event.defaultPrevented) { return; } + /** * TODO: alternative to stopPropagation * diff --git a/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts b/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts index b0ce14fc8..4baffe1c4 100644 --- a/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts +++ b/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts @@ -9,7 +9,7 @@ import { DockviewComponent } from '../../dockviewComponent'; import { DockviewGroupPanel } from '../../dockviewGroupPanel'; import { VoidContainer } from './voidContainer'; import { toggleClass } from '../../../dom'; -import { IDockviewPanel } from '../../dockviewPanel'; +import { DockviewPanel, IDockviewPanel } from '../../dockviewPanel'; export interface TabDropIndexEvent { readonly event: DragEvent; @@ -187,6 +187,26 @@ export class TabsContainer index: this.tabs.length, }); }), + addDisposableListener( + this.voidContainer.element, + 'mousedown', + (event) => { + if (event.shiftKey && !this.group.model.isFloating) { + event.preventDefault(); + + const { top, left } = + this.element.getBoundingClientRect(); + const { top: rootTop, left: rootLeft } = + this.accessor.element.getBoundingClientRect(); + + this.accessor.addFloatingGroup(this.group, { + x: left - rootLeft + 20, + y: top - rootTop + 20, + }); + event.preventDefault(); + } + } + ), addDisposableListener(this.tabContainer, 'mousedown', (event) => { if (event.defaultPrevented) { return; @@ -263,6 +283,23 @@ export class TabsContainer const disposable = CompositeDisposable.from( tabToAdd.onChanged((event) => { + if (event.shiftKey) { + event.preventDefault(); + + const panel = this.accessor.getGroupPanel(tabToAdd.panelId); + + const { top, left } = + tabToAdd.element.getBoundingClientRect(); + const { top: rootTop, left: rootLeft } = + this.accessor.element.getBoundingClientRect(); + + this.accessor.addFloatingGroup(panel as DockviewPanel, { + x: left - rootLeft, + y: top - rootTop, + }); + return; + } + const alreadyFocused = panel.id === this.group.model.activePanel?.id && this.group.model.isContentFocused; diff --git a/packages/dockview-core/src/dockview/components/titlebar/voidContainer.ts b/packages/dockview-core/src/dockview/components/titlebar/voidContainer.ts index 62b1f5ec2..5aef855c6 100644 --- a/packages/dockview-core/src/dockview/components/titlebar/voidContainer.ts +++ b/packages/dockview-core/src/dockview/components/titlebar/voidContainer.ts @@ -69,16 +69,6 @@ export class VoidContainer extends CompositeDisposable { this.addDisposables( handler, - addDisposableListener(this._element, 'mousedown', (event) => { - if (event.shiftKey && !this.group.model.isFloating) { - event.preventDefault(); - this.accessor.addFloating(this.group, { - x: event.clientX + 20, - y: event.clientY + 20, - }); - } - }), - this.voidDropTarget.onDrop((event) => { this._onDrop.fire(event); }), diff --git a/packages/dockview-core/src/dockview/dockviewComponent.scss b/packages/dockview-core/src/dockview/dockviewComponent.scss index f07025428..cc96b24bb 100644 --- a/packages/dockview-core/src/dockview/dockviewComponent.scss +++ b/packages/dockview-core/src/dockview/dockviewComponent.scss @@ -8,7 +8,7 @@ left: 0px; height: 100%; width: 100%; - z-index: 9999; + z-index: 9997; } } diff --git a/packages/dockview-core/src/dockview/dockviewComponent.ts b/packages/dockview-core/src/dockview/dockviewComponent.ts index 10e58eed1..c40a423e7 100644 --- a/packages/dockview-core/src/dockview/dockviewComponent.ts +++ b/packages/dockview-core/src/dockview/dockviewComponent.ts @@ -52,6 +52,11 @@ export interface PanelReference { remove: () => void; } +export interface SerializedFloatingGroup { + data: GroupPanelViewState; + position: { height: number; width: number; left: number; top: number }; +} + export interface SerializedDockview { grid: { root: SerializedGridObject; @@ -59,8 +64,9 @@ export interface SerializedDockview { width: number; orientation: Orientation; }; - panels: { [key: string]: GroupviewPanelState }; + panels: Record; activeGroup?: string; + floatingGroups?: SerializedFloatingGroup[]; } export type DockviewComponentUpdateOptions = Pick< @@ -118,7 +124,7 @@ export interface IDockviewComponent extends IBaseGrid { readonly onDidAddPanel: Event; readonly onDidLayoutFromJSON: Event; readonly onDidActivePanelChange: Event; - addFloating( + addFloatingGroup( item: DockviewPanel | DockviewGroupPanel, coord?: { x: number; y: number } ): void; @@ -156,7 +162,7 @@ export class DockviewComponent private readonly floatingGroups: { instance: DockviewGroupPanel; disposable: IDisposable; - render: () => void; + overlay: Overlay; }[] = []; get orientation(): Orientation { @@ -290,9 +296,10 @@ export class DockviewComponent this.updateWatermark(); } - addFloating( + addFloatingGroup( item: DockviewPanel | DockviewGroupPanel, - coord?: { x: number; y: number } + coord?: { x?: number; y?: number; height?: number; width?: number }, + options?: { skipRemoveGroup: boolean; connect: boolean } ): void { let group: DockviewGroupPanel; @@ -307,7 +314,14 @@ export class DockviewComponent group.model.openPanel(item); } else { group = item; - this.doRemoveGroup(item, { skipDispose: true }); + + const skip = + typeof options?.skipRemoveGroup === 'boolean' && + options.skipRemoveGroup; + + if (!skip) { + this.doRemoveGroup(item, { skipDispose: true }); + } } group.model.isFloating = true; @@ -315,15 +329,15 @@ export class DockviewComponent const { left, top } = this.element.getBoundingClientRect(); const overlayLeft = - typeof coord?.x === 'number' ? Math.max(coord.x - left, 0) : 100; + typeof coord?.x === 'number' ? Math.max(coord.x, 0) : 100; const overlayTop = - typeof coord?.y === 'number' ? Math.max(0, coord.y - top) : 100; + typeof coord?.y === 'number' ? Math.max(coord.y, 0) : 100; const overlay = new Overlay({ container: this.gridview.element, content: group.element, - height: 300, - width: 300, + height: coord?.height ?? 300, + width: coord?.width ?? 300, left: overlayLeft, top: overlayTop, minX: 100, @@ -333,20 +347,28 @@ export class DockviewComponent const el = group.element.querySelector('#dv-group-float-drag-handle'); if (el) { - overlay.setupDrag(true, el as HTMLElement); + overlay.setupDrag( + typeof options?.connect === 'boolean' ? options.connect : true, + el as HTMLElement + ); } const instance = { instance: group, - render: () => { - overlay.renderWithinBoundaryConditions(); - }, - disposable: new CompositeDisposable(overlay, { - dispose: () => { - group.model.isFloating = false; - remove(this.floatingGroups, instance); - }, - }), + + overlay, + disposable: new CompositeDisposable( + overlay, + overlay.onDidChange(() => { + this._bufferOnDidLayoutChange.fire(); + }), + { + dispose: () => { + group.model.isFloating = false; + remove(this.floatingGroups, instance); + }, + } + ), }; this.floatingGroups.push(instance); @@ -409,7 +431,7 @@ export class DockviewComponent if (this.floatingGroups) { for (const floating of this.floatingGroups) { - floating.render(); + floating.overlay.renderWithinBoundaryConditions(); } } } @@ -485,11 +507,26 @@ export class DockviewComponent return collection; }, {} as { [key: string]: GroupviewPanelState }); - return { + const floats: SerializedFloatingGroup[] = this.floatingGroups.map( + (floatingGroup) => { + return { + data: floatingGroup.instance.toJSON() as GroupPanelViewState, + position: floatingGroup.overlay.toJSON(), + }; + } + ); + + const result: SerializedDockview = { grid: data, panels, activeGroup: this.activeGroup?.id, }; + + if (floats.length > 0) { + result.floatingGroups = floats; + } + + return result; } fromJSON(data: SerializedDockview): void { @@ -505,49 +542,70 @@ export class DockviewComponent const width = this.width; const height = this.height; + const createGroupFromSerializedState = (data: GroupPanelViewState) => { + const { id, locked, hideHeader, views, activeView } = data; + + const group = this.createGroup({ + id, + locked: !!locked, + hideHeader: !!hideHeader, + }); + + this._onDidAddGroup.fire(group); + + for (const child of views) { + const panel = this._deserializer.fromJSON(panels[child], group); + + const isActive = + typeof activeView === 'string' && activeView === panel.id; + + group.model.openPanel(panel, { + skipSetPanelActive: !isActive, + skipSetGroupActive: true, + }); + } + + if (!group.activePanel && group.panels.length > 0) { + group.model.openPanel(group.panels[group.panels.length - 1], { + skipSetGroupActive: true, + }); + } + + return group; + }; + this.gridview.deserialize(grid, { fromJSON: (node: ISerializedLeafNode) => { - const { id, locked, hideHeader, views, activeView } = node.data; - - const group = this.createGroup({ - id, - locked: !!locked, - hideHeader: !!hideHeader, - }); - - this._onDidAddGroup.fire(group); - - for (const child of views) { - const panel = this._deserializer.fromJSON( - panels[child], - group - ); - - const isActive = - typeof activeView === 'string' && - activeView === panel.id; - - group.model.openPanel(panel, { - skipSetPanelActive: !isActive, - skipSetGroupActive: true, - }); - } - - if (!group.activePanel && group.panels.length > 0) { - group.model.openPanel( - group.panels[group.panels.length - 1], - { - skipSetGroupActive: true, - } - ); - } - - return group; + return createGroupFromSerializedState(node.data); }, }); this.layout(width, height); + const serializedFloatingGroups = data.floatingGroups || []; + + for (const serializedFloatingGroup of serializedFloatingGroups) { + const { data, position } = serializedFloatingGroup; + const group = createGroupFromSerializedState(data); + + const { left, top } = this.element.getBoundingClientRect(); + + this.addFloatingGroup( + group, + { + x: position.left, + y: position.top, + height: position.height, + width: position.width, + }, + { skipRemoveGroup: true, connect: false } + ); + } + + for (const floatingGroup of this.floatingGroups) { + floatingGroup.overlay.renderWithinBoundaryConditions(); + } + if (typeof activeGroup === 'string') { const panel = this.getPanel(activeGroup); if (panel) { @@ -595,6 +653,12 @@ export class DockviewComponent let referenceGroup: DockviewGroupPanel | undefined; + if (options.position && options.floating) { + throw new Error( + 'you can only provide one of: position, floating as arguments to .addPanel(...)' + ); + } + if (options.position) { if (isPanelOptionsWithPanel(options.position)) { const referencePanel = @@ -639,7 +703,23 @@ export class DockviewComponent const target = toTarget( options.position?.direction || 'within' ); - if (target === 'center') { + + if (options.floating) { + const group = this.createGroup(); + panel = this.createPanel(options, group); + group.model.openPanel(panel); + + const o = + typeof options.floating === 'object' && + options.floating !== null + ? options.floating + : {}; + + this.addFloatingGroup(group, o, { + connect: false, + skipRemoveGroup: true, + }); + } else if (referenceGroup.model.isFloating || target === 'center') { panel = this.createPanel(options, referenceGroup); referenceGroup.model.openPanel(panel); } else { @@ -653,10 +733,26 @@ export class DockviewComponent panel = this.createPanel(options, group); group.model.openPanel(panel); } + } else if (options.floating) { + const group = this.createGroup(); + panel = this.createPanel(options, group); + group.model.openPanel(panel); + + const o = + typeof options.floating === 'object' && + options.floating !== null + ? options.floating + : {}; + + this.addFloatingGroup(group, o, { + connect: false, + skipRemoveGroup: true, + }); } else { const group = this.createGroupAtLocation(); panel = this.createPanel(options, group); + group.model.openPanel(panel); } @@ -704,7 +800,7 @@ export class DockviewComponent } private updateWatermark(): void { - if (this.groups.length === 0) { + if (this.groups.filter((x) => !x.model.isFloating).length === 0) { if (!this.watermark) { this.watermark = this.createWatermarkComponent(); @@ -823,8 +919,10 @@ export class DockviewComponent if (floatingGroup) { if (!options?.skipDispose) { floatingGroup.instance.dispose(); + this._groups.delete(group.id); } floatingGroup.disposable.dispose(); + return floatingGroup.instance; } diff --git a/packages/dockview-core/src/dockview/dockviewGroupPanel.ts b/packages/dockview-core/src/dockview/dockviewGroupPanel.ts index 6f722021e..a99ab64c3 100644 --- a/packages/dockview-core/src/dockview/dockviewGroupPanel.ts +++ b/packages/dockview-core/src/dockview/dockviewGroupPanel.ts @@ -4,6 +4,7 @@ import { GridviewPanelApi } from '../api/gridviewPanelApi'; import { DockviewGroupPanelModel, GroupOptions, + GroupPanelViewState, IDockviewGroupPanelModel, IHeader, } from './dockviewGroupPanelModel'; @@ -94,7 +95,6 @@ export class DockviewGroupPanel } toJSON(): any { - // TODO fix typing return this.model.toJSON(); } } diff --git a/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts b/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts index e68cc2e53..cc8030a7d 100644 --- a/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts +++ b/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts @@ -261,6 +261,10 @@ export class DockviewGroupPanelModel return false; } + if (event.shiftKey && !this.isFloating) { + return false; + } + const data = getPanelData(); if (data && data.viewId === this.accessor.id) { diff --git a/packages/dockview-core/src/dockview/options.ts b/packages/dockview-core/src/dockview/options.ts index 147cc5e48..e1d9d7861 100644 --- a/packages/dockview-core/src/dockview/options.ts +++ b/packages/dockview-core/src/dockview/options.ts @@ -18,6 +18,7 @@ import { IDisposable } from '../lifecycle'; import { Position } from '../dnd/droptarget'; import { IDockviewPanel } from './dockviewPanel'; import { FrameworkFactory } from '../panel/componentFactory'; +import { Optional } from '../types'; export interface IHeaderActionsRenderer extends IDisposable { readonly element: HTMLElement; @@ -134,12 +135,32 @@ export function isPanelOptionsWithGroup( return false; } -export interface AddPanelOptions - extends Omit { +type AddPanelFloatingGroupUnion = { + floating: + | { + height?: number; + width?: number; + x?: number; + y?: number; + } + | true; + position: never; +}; + +type AddPanelPositionUnion = { + floating: false | never; + position: AddPanelPositionOptions; +}; + +type AddPanelOptionsUnion = AddPanelFloatingGroupUnion | AddPanelPositionUnion; + +export type AddPanelOptions = Omit< + PanelOptions, + 'component' | 'tabComponent' +> & { component: string; tabComponent?: string; - position?: AddPanelPositionOptions; -} +} & Partial; type AddGroupOptionsWithPanel = { referencePanel: string | IDockviewPanel; diff --git a/packages/docs/docs/components/dockview.mdx b/packages/docs/docs/components/dockview.mdx index d2215db7f..8293bf027 100644 --- a/packages/docs/docs/components/dockview.mdx +++ b/packages/docs/docs/components/dockview.mdx @@ -28,6 +28,7 @@ import DockviewExternalDnd from '@site/sandboxes/externaldnd-dockview/src/app'; import DockviewResizeContainer from '@site/sandboxes/resizecontainer-dockview/src/app'; import DockviewTabheight from '@site/sandboxes/tabheight-dockview/src/app'; import DockviewWithIFrames from '@site/sandboxes/iframe-dockview/src/app'; +import DockviewFloating from '@site/sandboxes/floatinggroup-dockview/src/app'; import { attach as attachDockviewVanilla } from '@site/sandboxes/javascript/vanilla-dockview/src/app'; import { attach as attachSimpleDockview } from '@site/sandboxes/javascript/simple-dockview/src/app'; @@ -361,6 +362,15 @@ any drag and drop logic for other controls. +## Floating Groups + +Dockview has built-in support for floating groups. Each floating container can contain a single group with many panels +and you can have as many floating containers as needed. You cannot dock multiple groups together in the same floating container. + + + + + ## Panels ### Add Panel diff --git a/packages/docs/docusaurus.config.js b/packages/docs/docusaurus.config.js index fc5450b1b..4c770d792 100644 --- a/packages/docs/docusaurus.config.js +++ b/packages/docs/docusaurus.config.js @@ -39,13 +39,15 @@ const config = { 'docusaurus-plugin-sass', (context, options) => { return { - name: 'webpack', + name: 'custom-webpack', configureWebpack: (config, isServer, utils) => { return { // externals: ['react', 'react-dom'], devtool: 'source-map', resolve: { + ...config.resolve, alias: { + ...config.resolve.alias, react: path.join( __dirname, '../../node_modules', @@ -57,9 +59,6 @@ const config = { 'react-dom' ), }, - fallback: { - timers: false, - }, }, }; }, diff --git a/packages/docs/sandboxes/floatinggroup-dockview/package.json b/packages/docs/sandboxes/floatinggroup-dockview/package.json new file mode 100644 index 000000000..9ce09597c --- /dev/null +++ b/packages/docs/sandboxes/floatinggroup-dockview/package.json @@ -0,0 +1,32 @@ +{ + "name": "floatinggroup-dockview", + "description": "", + "keywords": [ + "dockview" + ], + "version": "1.0.0", + "main": "src/index.tsx", + "dependencies": { + "dockview": "*", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@types/react": "^18.0.28", + "@types/react-dom": "^18.0.11", + "typescript": "^4.9.5", + "react-scripts": "*" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test --env=jsdom", + "eject": "react-scripts eject" + }, + "browserslist": [ + ">0.2%", + "not dead", + "not ie <= 11", + "not op_mini all" + ] +} diff --git a/packages/docs/sandboxes/floatinggroup-dockview/public/index.html b/packages/docs/sandboxes/floatinggroup-dockview/public/index.html new file mode 100644 index 000000000..1f8a52426 --- /dev/null +++ b/packages/docs/sandboxes/floatinggroup-dockview/public/index.html @@ -0,0 +1,44 @@ + + + + + + + + + + + + + React App + + + + +
+ + + + diff --git a/packages/docs/sandboxes/floatinggroup-dockview/src/app.tsx b/packages/docs/sandboxes/floatinggroup-dockview/src/app.tsx new file mode 100644 index 000000000..5ddc51d4e --- /dev/null +++ b/packages/docs/sandboxes/floatinggroup-dockview/src/app.tsx @@ -0,0 +1,217 @@ +import { + DockviewApi, + DockviewReact, + DockviewReadyEvent, + IDockviewPanelProps, + SerializedDockview, +} from 'dockview'; +import * as React from 'react'; + +const components = { + default: (props: IDockviewPanelProps<{ title: string }>) => { + return ( +
+ {props.params.title} +
+ ); + }, +}; + +const counter = (() => { + let i = 0; + + return { + next: () => ++i, + }; +})(); + +function loadDefaultLayout(api: DockviewApi) { + api.addPanel({ + id: 'panel_1', + component: 'default', + }); + + api.addPanel({ + id: 'panel_2', + component: 'default', + }); + + api.addPanel({ + id: 'panel_3', + component: 'default', + }); + + const panel4 = api.addPanel({ + id: 'panel_4', + component: 'default', + floating: true, + }); + + api.addPanel({ + id: 'panel_5', + component: 'default', + floating: false, + position: { referencePanel: panel4 }, + }); + + api.addPanel({ + id: 'panel_6', + component: 'default', + }); +} + +let panelCount = 0; + +function addFloatingPanel(api: DockviewApi) { + api.addPanel({ + id: (++panelCount).toString(), + title: `Tab ${panelCount}`, + component: 'default', + floating: true, + }); +} + +function addFloatingPanel2(api: DockviewApi) { + api.addPanel({ + id: (++panelCount).toString(), + title: `Tab ${panelCount}`, + component: 'default', + floating: { width: 250, height: 150, x: 50, y: 50 }, + }); +} + +function safeParse(value: any): T | null { + try { + return JSON.parse(value) as T; + } catch (err) { + return null; + } +} + +const useLocalStorage = ( + key: string +): [T | null, (setter: T | null) => void] => { + const [state, setState] = React.useState( + safeParse(localStorage.getItem(key)) + ); + + React.useEffect(() => { + const _state = localStorage.getItem('key'); + try { + if (_state !== null) { + setState(JSON.parse(_state)); + } + } catch (err) { + // + } + }, [key]); + + return [ + state, + (_state: T | null) => { + if (_state === null) { + localStorage.removeItem(key); + } else { + localStorage.setItem(key, JSON.stringify(_state)); + setState(_state); + } + }, + ]; +}; + +export const DockviewPersistance = () => { + const [api, setApi] = React.useState(); + const [layout, setLayout] = + useLocalStorage('floating.layout'); + + const load = (api: DockviewApi) => { + api.clear(); + if (layout) { + try { + api.fromJSON(layout); + } catch (err) { + console.error(err); + api.clear(); + loadDefaultLayout(api); + } + } else { + loadDefaultLayout(api); + } + }; + + const onReady = (event: DockviewReadyEvent) => { + load(event.api); + setApi(event.api); + }; + + return ( +
+
+ + + + +
+
+ +
+
+ ); +}; + +export default DockviewPersistance; + +const Watermark = () => { + return
watermark
; +}; diff --git a/packages/docs/sandboxes/floatinggroup-dockview/src/index.tsx b/packages/docs/sandboxes/floatinggroup-dockview/src/index.tsx new file mode 100644 index 000000000..2fe1be232 --- /dev/null +++ b/packages/docs/sandboxes/floatinggroup-dockview/src/index.tsx @@ -0,0 +1,20 @@ +import { StrictMode } from 'react'; +import * as ReactDOMClient from 'react-dom/client'; +import './styles.css'; +import 'dockview/dist/styles/dockview.css'; + +import App from './app'; + +const rootElement = document.getElementById('root'); + +if (rootElement) { + const root = ReactDOMClient.createRoot(rootElement); + + root.render( + +
+ +
+
+ ); +} diff --git a/packages/docs/sandboxes/floatinggroup-dockview/src/styles.css b/packages/docs/sandboxes/floatinggroup-dockview/src/styles.css new file mode 100644 index 000000000..92b6a1b36 --- /dev/null +++ b/packages/docs/sandboxes/floatinggroup-dockview/src/styles.css @@ -0,0 +1,16 @@ +body { + margin: 0px; + color: white; + font-family: sans-serif; + text-align: center; +} + +#root { + height: 100vh; + width: 100vw; +} + +.app { + height: 100%; + +} diff --git a/packages/docs/sandboxes/floatinggroup-dockview/tsconfig.json b/packages/docs/sandboxes/floatinggroup-dockview/tsconfig.json new file mode 100644 index 000000000..cdc4fb5f5 --- /dev/null +++ b/packages/docs/sandboxes/floatinggroup-dockview/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "outDir": "build/dist", + "module": "esnext", + "target": "es5", + "lib": ["es6", "dom"], + "sourceMap": true, + "allowJs": true, + "jsx": "react-jsx", + "moduleResolution": "node", + "rootDir": "src", + "forceConsistentCasingInFileNames": true, + "noImplicitReturns": true, + "noImplicitThis": true, + "noImplicitAny": true, + "strictNullChecks": true + } +} diff --git a/packages/docs/sandboxes/layout-dockview/src/app.tsx b/packages/docs/sandboxes/layout-dockview/src/app.tsx index 33f1d6368..f0796befa 100644 --- a/packages/docs/sandboxes/layout-dockview/src/app.tsx +++ b/packages/docs/sandboxes/layout-dockview/src/app.tsx @@ -71,7 +71,7 @@ export const DockviewPersistance = () => { event.api.fromJSON(layout); success = true; } catch (err) { - // + console.error(err); } } diff --git a/packages/docs/src/components/gridview/events.tsx b/packages/docs/src/components/gridview/events.tsx index 6917df907..9f2b0546d 100644 --- a/packages/docs/src/components/gridview/events.tsx +++ b/packages/docs/src/components/gridview/events.tsx @@ -251,8 +251,6 @@ export const EventsGridview = () => { }, }); - console.log('sdf'); - api.addPanel({ id: 'panel_4', component: 'default',