From 6b969dfa18de4fc00396eb2b330e2de4ca1ac443 Mon Sep 17 00:00:00 2001 From: mathuo <6710312+mathuo@users.noreply.github.com> Date: Thu, 23 Nov 2023 22:27:00 +0000 Subject: [PATCH] feat: gready render mode --- .../__tests__/api/dockviewPanelApi.spec.ts | 8 +- .../dockview/components/panel/content.spec.ts | 12 +- .../dockview/dockviewGroupPanelModel.spec.ts | 12 +- .../__tests__/dockview/dockviewPanel.spec.ts | 24 ++- .../dockview-core/src/api/dockviewPanelApi.ts | 24 ++- packages/dockview-core/src/dnd/dnd.ts | 50 +++-- packages/dockview-core/src/dnd/droptarget.ts | 175 +++++++++--------- .../components/greadyReadyContainer.scss | 15 ++ .../components/greadyRenderContainer.ts | 133 +++++++++++++ .../src/dockview/components/panel/content.ts | 154 ++++++++++++--- .../src/dockview/deserializer.ts | 3 +- .../src/dockview/dockviewComponent.ts | 23 ++- .../src/dockview/dockviewGroupPanelModel.ts | 62 ++----- .../src/dockview/dockviewPanel.ts | 25 ++- .../dockview-core/src/dockview/options.ts | 2 + packages/dockview-core/src/dockview/types.ts | 1 + packages/dockview-core/src/dom.ts | 15 ++ packages/dockview-core/src/index.ts | 8 +- packages/dockview/src/dockview/dockview.tsx | 5 + .../docs/sandboxes/demo-dockview/src/app.tsx | 83 +++++++-- 20 files changed, 623 insertions(+), 211 deletions(-) create mode 100644 packages/dockview-core/src/dockview/components/greadyReadyContainer.scss create mode 100644 packages/dockview-core/src/dockview/components/greadyRenderContainer.ts diff --git a/packages/dockview-core/src/__tests__/api/dockviewPanelApi.spec.ts b/packages/dockview-core/src/__tests__/api/dockviewPanelApi.spec.ts index 475146b5f..1d610ba99 100644 --- a/packages/dockview-core/src/__tests__/api/dockviewPanelApi.spec.ts +++ b/packages/dockview-core/src/__tests__/api/dockviewPanelApi.spec.ts @@ -36,7 +36,7 @@ describe('groupPanelApi', () => { }); test('updateParameters', () => { - const groupPanel: Partial = { + const groupPanel: Partial = { id: 'test_id', update: jest.fn(), }; @@ -53,7 +53,7 @@ describe('groupPanelApi', () => { ); const cut = new DockviewPanelApiImpl( - groupPanel, + groupPanel, groupViewPanel, accessor ); @@ -67,7 +67,7 @@ describe('groupPanelApi', () => { }); test('onDidGroupChange', () => { - const groupPanel: Partial = { + const groupPanel: Partial = { id: 'test_id', }; @@ -83,7 +83,7 @@ describe('groupPanelApi', () => { ); const cut = new DockviewPanelApiImpl( - groupPanel, + groupPanel, groupViewPanel, accessor ); 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 34ba707a3..1b6d7b161 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 @@ -9,6 +9,7 @@ import { CompositeDisposable } from '../../../../lifecycle'; import { PanelUpdateEvent } from '../../../../panel/types'; import { IDockviewPanel } from '../../../../dockview/dockviewPanel'; import { IDockviewPanelModel } from '../../../../dockview/dockviewPanelModel'; +import { DockviewComponent } from '../../../../dockview/dockviewComponent'; class TestContentRenderer extends CompositeDisposable @@ -56,7 +57,14 @@ describe('contentContainer', () => { let blur = 0; const disposable = new CompositeDisposable(); - const cut = new ContentContainer(); + + const dockviewComponent = jest.fn(() => { + return { + renderingType: 'destructive', + } as DockviewComponent; + }); + + const cut = new ContentContainer(dockviewComponent(), jest.fn() as any); disposable.addDisposables( cut.onDidFocus(() => { @@ -73,6 +81,7 @@ describe('contentContainer', () => { view: { content: contentRenderer, } as Partial, + api: { renderingType: 'destructive' }, } as Partial; cut.openPanel(panel as IDockviewPanel); @@ -107,6 +116,7 @@ describe('contentContainer', () => { view: { content: contentRenderer2, } as Partial, + api: { renderingType: 'destructive' }, } as Partial; cut.openPanel(panel2 as IDockviewPanel); diff --git a/packages/dockview-core/src/__tests__/dockview/dockviewGroupPanelModel.spec.ts b/packages/dockview-core/src/__tests__/dockview/dockviewGroupPanelModel.spec.ts index 2052ef1be..b73c49c5d 100644 --- a/packages/dockview-core/src/__tests__/dockview/dockviewGroupPanelModel.spec.ts +++ b/packages/dockview-core/src/__tests__/dockview/dockviewGroupPanelModel.spec.ts @@ -576,19 +576,25 @@ describe('groupview', () => { .getElementsByClassName('content-container') .item(0)!.childNodes; - const panel1 = new TestPanel('id_1', null as any); + const panel1 = new TestPanel('id_1', { + renderingType: 'destructive', + } as any); cut.openPanel(panel1); expect(contentContainer.length).toBe(1); expect(contentContainer.item(0)).toBe(panel1.view.content.element); - const panel2 = new TestPanel('id_2', null as any); + const panel2 = new TestPanel('id_2', { + renderingType: 'destructive', + } as any); cut.openPanel(panel2); expect(contentContainer.length).toBe(1); expect(contentContainer.item(0)).toBe(panel2.view.content.element); - const panel3 = new TestPanel('id_2', null as any); + const panel3 = new TestPanel('id_2', { + renderingType: 'destructive', + } as any); cut.openPanel(panel3, { skipSetPanelActive: true }); expect(contentContainer.length).toBe(1); diff --git a/packages/dockview-core/src/__tests__/dockview/dockviewPanel.spec.ts b/packages/dockview-core/src/__tests__/dockview/dockviewPanel.spec.ts index c67546db8..b3ab5d7ad 100644 --- a/packages/dockview-core/src/__tests__/dockview/dockviewPanel.spec.ts +++ b/packages/dockview-core/src/__tests__/dockview/dockviewPanel.spec.ts @@ -29,7 +29,9 @@ describe('dockviewPanel', () => { const group = new groupMock(); const model = new panelModelMock(); - const cut = new DockviewPanel('fake-id', accessor, api, group, model); + const cut = new DockviewPanel('fake-id', accessor, api, group, model, { + renderingType: 'destructive', + }); let latestTitle: string | undefined = undefined; @@ -74,7 +76,9 @@ describe('dockviewPanel', () => { const group = new groupMock(); const model = new panelModelMock(); - const cut = new DockviewPanel('fake-id', accessor, api, group, model); + const cut = new DockviewPanel('fake-id', accessor, api, group, model, { + renderingType: 'destructive', + }); cut.init({ title: 'myTitle', params: {} }); expect(cut.title).toBe('myTitle'); @@ -109,7 +113,9 @@ describe('dockviewPanel', () => { const group = new groupMock(); const model = new panelModelMock(); - const cut = new DockviewPanel('fake-id', accessor, api, group, model); + const cut = new DockviewPanel('fake-id', accessor, api, group, model, { + renderingType: 'destructive', + }); cut.init({ params: {}, title: 'title' }); @@ -141,7 +147,9 @@ describe('dockviewPanel', () => { const group = new groupMock(); const model = new panelModelMock(); - const cut = new DockviewPanel('fake-id', accessor, api, group, model); + const cut = new DockviewPanel('fake-id', accessor, api, group, model, { + renderingType: 'destructive', + }); expect(cut.params).toEqual(undefined); @@ -177,7 +185,9 @@ describe('dockviewPanel', () => { const group = new groupMock(); const model = new panelModelMock(); - const cut = new DockviewPanel('fake-id', accessor, api, group, model); + const cut = new DockviewPanel('fake-id', accessor, api, group, model, { + renderingType: 'destructive', + }); cut.api.setSize({ height: 123, width: 456 }); @@ -208,7 +218,9 @@ describe('dockviewPanel', () => { const group = new groupMock(); const model = new panelModelMock(); - const cut = new DockviewPanel('fake-id', accessor, api, group, model); + const cut = new DockviewPanel('fake-id', accessor, api, group, model, { + renderingType: 'destructive', + }); cut.init({ params: { a: '1', b: '2' }, title: 'A title' }); expect(cut.params).toEqual({ a: '1', b: '2' }); diff --git a/packages/dockview-core/src/api/dockviewPanelApi.ts b/packages/dockview-core/src/api/dockviewPanelApi.ts index e724c251d..bc1101711 100644 --- a/packages/dockview-core/src/api/dockviewPanelApi.ts +++ b/packages/dockview-core/src/api/dockviewPanelApi.ts @@ -2,14 +2,19 @@ import { Emitter, Event } from '../events'; import { GridviewPanelApiImpl, GridviewPanelApi } from './gridviewPanelApi'; import { DockviewGroupPanel } from '../dockview/dockviewGroupPanel'; import { MutableDisposable } from '../lifecycle'; -import { IDockviewPanel } from '../dockview/dockviewPanel'; +import { DockviewPanel, IDockviewPanel } from '../dockview/dockviewPanel'; import { DockviewComponent } from '../dockview/dockviewComponent'; import { Position } from '../dnd/droptarget'; +import { RenderingType } from '../dockview/components/greadyRenderContainer'; export interface TitleEvent { readonly title: string; } +export interface RenderingTypeEvent { + renderingType: RenderingType; +} + /* * omit visibility modifiers since the visibility of a single group doesn't make sense * because it belongs to a groupview @@ -21,11 +26,14 @@ export interface DockviewPanelApi > { readonly group: DockviewGroupPanel; readonly isGroupActive: boolean; + readonly renderingType: RenderingType; readonly title: string | undefined; readonly onDidActiveGroupChange: Event; readonly onDidGroupChange: Event; + readonly onDidRendeingTypeChange: Event; close(): void; setTitle(title: string): void; + setRenderingType(renderingType: RenderingType): void; moveTo(options: { group: DockviewGroupPanel; position?: Position; @@ -48,6 +56,9 @@ export class DockviewPanelApiImpl private readonly _onDidGroupChange = new Emitter(); readonly onDidGroupChange = this._onDidGroupChange.event; + readonly _onDidRenderingTypeChange = new Emitter(); + readonly onDidRendeingTypeChange = this._onDidRenderingTypeChange.event; + private readonly disposable = new MutableDisposable(); get title(): string | undefined { @@ -58,6 +69,10 @@ export class DockviewPanelApiImpl return !!this.group?.isActive; } + get renderingType(): RenderingType { + return this.panel.renderingType; + } + set group(value: DockviewGroupPanel) { const isOldGroupActive = this.isGroupActive; @@ -81,7 +96,7 @@ export class DockviewPanelApiImpl } constructor( - private panel: IDockviewPanel, + private panel: DockviewPanel, group: DockviewGroupPanel, private readonly accessor: DockviewComponent ) { @@ -93,6 +108,7 @@ export class DockviewPanelApiImpl this.addDisposables( this.disposable, + this._onDidRenderingTypeChange, this._onDidTitleChange, this._onDidGroupChange, this._onDidActiveGroupChange @@ -117,6 +133,10 @@ export class DockviewPanelApiImpl this.panel.setTitle(title); } + setRenderingType(renderingType: RenderingType): void { + this.panel.setRenderingType(renderingType); + } + close(): void { this.group.model.closePanel(this.panel); } diff --git a/packages/dockview-core/src/dnd/dnd.ts b/packages/dockview-core/src/dnd/dnd.ts index 533be7e11..2120f2994 100644 --- a/packages/dockview-core/src/dnd/dnd.ts +++ b/packages/dockview-core/src/dnd/dnd.ts @@ -21,14 +21,43 @@ export class DragAndDropObserver extends CompositeDisposable { this.registerListeners(); } + onDragEnter(e: DragEvent): void { + this.target = e.target; + this.callbacks.onDragEnter(e); + } + + onDragOver(e: DragEvent): void { + e.preventDefault(); // needed so that the drop event fires (https://stackoverflow.com/questions/21339924/drop-event-not-firing-in-chrome) + + if (this.callbacks.onDragOver) { + this.callbacks.onDragOver(e); + } + } + + onDragLeave(e: DragEvent): void { + if (this.target === e.target) { + this.target = null; + + this.callbacks.onDragLeave(e); + } + } + + onDragEnd(e: DragEvent): void { + this.target = null; + this.callbacks.onDragEnd(e); + } + + onDrop(e: DragEvent): void { + this.callbacks.onDrop(e); + } + private registerListeners(): void { this.addDisposables( addDisposableListener( this.element, 'dragenter', (e: DragEvent) => { - this.target = e.target; - this.callbacks.onDragEnter(e); + this.onDragEnter(e); }, true ) @@ -39,11 +68,7 @@ export class DragAndDropObserver extends CompositeDisposable { this.element, 'dragover', (e: DragEvent) => { - e.preventDefault(); // needed so that the drop event fires (https://stackoverflow.com/questions/21339924/drop-event-not-firing-in-chrome) - - if (this.callbacks.onDragOver) { - this.callbacks.onDragOver(e); - } + this.onDragOver(e); }, true ) @@ -51,24 +76,19 @@ export class DragAndDropObserver extends CompositeDisposable { this.addDisposables( addDisposableListener(this.element, 'dragleave', (e: DragEvent) => { - if (this.target === e.target) { - this.target = null; - - this.callbacks.onDragLeave(e); - } + this.onDragLeave(e); }) ); this.addDisposables( addDisposableListener(this.element, 'dragend', (e: DragEvent) => { - this.target = null; - this.callbacks.onDragEnd(e); + this.onDragEnd(e); }) ); this.addDisposables( addDisposableListener(this.element, 'drop', (e: DragEvent) => { - this.callbacks.onDrop(e); + this.onDrop(e); }) ); } diff --git a/packages/dockview-core/src/dnd/droptarget.ts b/packages/dockview-core/src/dnd/droptarget.ts index 003f8b045..357d4c32f 100644 --- a/packages/dockview-core/src/dnd/droptarget.ts +++ b/packages/dockview-core/src/dnd/droptarget.ts @@ -63,6 +63,8 @@ export class Droptarget extends CompositeDisposable { private readonly _onDrop = new Emitter(); readonly onDrop: Event = this._onDrop.event; + readonly dnd: DragAndDropObserver; + private static USED_EVENT_ID = '__dockview_droptarget_event_is_used__'; get state(): Position | undefined { @@ -90,98 +92,97 @@ export class Droptarget extends CompositeDisposable { this.options.acceptedTargetZones ); - this.addDisposables( - this._onDrop, - new DragAndDropObserver(this.element, { - onDragEnter: () => undefined, - onDragOver: (e) => { - if (this._acceptedTargetZonesSet.size === 0) { - this.removeDropTarget(); - return; - } - - const width = this.element.clientWidth; - const height = this.element.clientHeight; - - if (width === 0 || height === 0) { - return; // avoid div!0 - } - - const rect = ( - e.currentTarget as HTMLElement - ).getBoundingClientRect(); - const x = e.clientX - rect.left; - const y = e.clientY - rect.top; - - const quadrant = this.calculateQuadrant( - this._acceptedTargetZonesSet, - x, - y, - width, - height - ); - - /** - * If the event has already been used by another DropTarget instance - * then don't show a second drop target, only one target should be - * active at any one time - */ - if (this.isAlreadyUsed(e) || quadrant === null) { - // no drop target should be displayed - this.removeDropTarget(); - return; - } + this.dnd = new DragAndDropObserver(this.element, { + onDragEnter: () => undefined, + onDragOver: (e) => { + if (this._acceptedTargetZonesSet.size === 0) { + this.removeDropTarget(); + return; + } + + const width = this.element.clientWidth; + const height = this.element.clientHeight; + + if (width === 0 || height === 0) { + return; // avoid div!0 + } + + const rect = ( + e.currentTarget as HTMLElement + ).getBoundingClientRect(); + const x = e.clientX - rect.left; + const y = e.clientY - rect.top; + + const quadrant = this.calculateQuadrant( + this._acceptedTargetZonesSet, + x, + y, + width, + height + ); + + /** + * If the event has already been used by another DropTarget instance + * then don't show a second drop target, only one target should be + * active at any one time + */ + if (this.isAlreadyUsed(e) || quadrant === null) { + // no drop target should be displayed + this.removeDropTarget(); + return; + } - if (typeof this.options.canDisplayOverlay === 'boolean') { - if (!this.options.canDisplayOverlay) { - this.removeDropTarget(); - return; - } - } else if (!this.options.canDisplayOverlay(e, quadrant)) { + if (typeof this.options.canDisplayOverlay === 'boolean') { + if (!this.options.canDisplayOverlay) { this.removeDropTarget(); return; } - - this.markAsUsed(e); - - if (!this.targetElement) { - this.targetElement = document.createElement('div'); - this.targetElement.className = 'drop-target-dropzone'; - this.overlayElement = document.createElement('div'); - this.overlayElement.className = 'drop-target-selection'; - this._state = 'center'; - this.targetElement.appendChild(this.overlayElement); - - this.element.classList.add('drop-target'); - this.element.append(this.targetElement); - } - - this.toggleClasses(quadrant, width, height); - - this.setState(quadrant); - }, - onDragLeave: () => { - this.removeDropTarget(); - }, - onDragEnd: () => { + } else if (!this.options.canDisplayOverlay(e, quadrant)) { this.removeDropTarget(); - }, - onDrop: (e) => { - e.preventDefault(); - - const state = this._state; - - this.removeDropTarget(); - - if (state) { - // only stop the propagation of the event if we are dealing with it - // which is only when the target has state - e.stopPropagation(); - this._onDrop.fire({ position: state, nativeEvent: e }); - } - }, - }) - ); + return; + } + + this.markAsUsed(e); + + if (!this.targetElement) { + this.targetElement = document.createElement('div'); + this.targetElement.className = 'drop-target-dropzone'; + this.overlayElement = document.createElement('div'); + this.overlayElement.className = 'drop-target-selection'; + this._state = 'center'; + this.targetElement.appendChild(this.overlayElement); + + this.element.classList.add('drop-target'); + this.element.append(this.targetElement); + } + + this.toggleClasses(quadrant, width, height); + + this.setState(quadrant); + }, + onDragLeave: () => { + this.removeDropTarget(); + }, + onDragEnd: () => { + this.removeDropTarget(); + }, + onDrop: (e) => { + e.preventDefault(); + + const state = this._state; + + this.removeDropTarget(); + + if (state) { + // only stop the propagation of the event if we are dealing with it + // which is only when the target has state + e.stopPropagation(); + this._onDrop.fire({ position: state, nativeEvent: e }); + } + }, + }); + + this.addDisposables(this._onDrop, this.dnd); } setTargetZones(acceptedTargetZones: Position[]): void { diff --git a/packages/dockview-core/src/dockview/components/greadyReadyContainer.scss b/packages/dockview-core/src/dockview/components/greadyReadyContainer.scss new file mode 100644 index 000000000..cf67afd19 --- /dev/null +++ b/packages/dockview-core/src/dockview/components/greadyReadyContainer.scss @@ -0,0 +1,15 @@ +.dv-render-overlay { + position: absolute; + z-index: 1; + + &.dv-render-overlay-float { + z-index: 999; + } +} + +.dv-debug { + .dv-render-overlay { + outline: 1px solid red; + outline-offset: -1; + } +} diff --git a/packages/dockview-core/src/dockview/components/greadyRenderContainer.ts b/packages/dockview-core/src/dockview/components/greadyRenderContainer.ts new file mode 100644 index 000000000..419a7c91c --- /dev/null +++ b/packages/dockview-core/src/dockview/components/greadyRenderContainer.ts @@ -0,0 +1,133 @@ +import { DragAndDropObserver } from '../../dnd/dnd'; +import { Droptarget } from '../../dnd/droptarget'; +import { getDomNodePagePosition, toggleClass } from '../../dom'; +import { CompositeDisposable, Disposable, IDisposable } from '../../lifecycle'; +import { IDockviewPanel } from '../dockviewPanel'; + +export type RenderingType = 'destructive' | 'gready'; + +export interface IRenderable { + readonly element: HTMLElement; + readonly dropTarget: Droptarget; +} + +export class GreadyRenderContainer extends CompositeDisposable { + private readonly map: Record = {}; + + get allIds(): string[] { + return Object.keys(this.map); + } + + constructor(private readonly element: HTMLElement) { + super(); + + this.addDisposables({ + dispose: () => { + for (const value of Object.values(this.map)) { + value.dispose(); + } + }, + }); + } + + remove(panel: IDockviewPanel): boolean { + if (this.map[panel.api.id]) { + this.map[panel.api.id].dispose(); + delete this.map[panel.api.id]; + return true; + } + return false; + } + + setReferenceContentContainer( + panel: IDockviewPanel, + referenceContainer: IRenderable + ) { + if (!this.map[panel.api.id]) { + this.map[panel.api.id] = Disposable.NONE; + } + + this.map[panel.api.id]?.dispose(); + + if (panel.view.content.element.parentElement !== this.element) { + this.element.appendChild(panel.view.content.element); + } + + const resize = () => { + const box = getDomNodePagePosition(referenceContainer.element); + const box2 = getDomNodePagePosition(this.element); + panel.view.content.element.style.left = `${box.left - box2.left}px`; + panel.view.content.element.style.top = `${box.top - box2.top}px`; + panel.view.content.element.style.width = `${box.width}px`; + panel.view.content.element.style.height = `${box.height}px`; + + toggleClass( + panel.view.content.element, + 'dv-render-overlay-float', + panel.group.api.isFloating + ); + }; + + const disposable = new CompositeDisposable( + /** + * dnd will not work as expected unless we explictly forward those events from the + * view to the underlying container + */ + new DragAndDropObserver(panel.view.content.element, { + onDragEnd: (e) => { + referenceContainer.dropTarget.dnd.onDragEnd(e); + }, + onDragEnter: (e) => { + referenceContainer.dropTarget.dnd.onDragEnter(e); + }, + onDragLeave: (e) => { + referenceContainer.dropTarget.dnd.onDragLeave(e); + }, + onDrop: (e) => { + referenceContainer.dropTarget.dnd.onDrop(e); + }, + onDragOver: (e) => { + referenceContainer.dropTarget.dnd.onDragOver(e); + }, + }), + panel.api.onDidVisibilityChange((event) => { + panel.view.content.element.style.display = event.isVisible + ? '' + : 'none'; + }), + panel.api.onDidDimensionsChange((event) => { + resize(); + }), + { + dispose: () => { + panel.view.content.element.style.display = ''; + panel.view.content.element.style.left = ''; + panel.view.content.element.style.top = ''; + panel.view.content.element.style.width = ''; + panel.view.content.element.style.height = ''; + + this.element.removeChild(panel.view.content.element); + + toggleClass( + panel.view.content.element, + 'dv-render-overlay', + false + ); + toggleClass( + panel.view.content.element, + 'dv-render-overlay-float', + false + ); + }, + } + ); + + toggleClass(panel.view.content.element, 'dv-render-overlay', true); + + queueMicrotask(() => { + resize(); + }); + + this.map[panel.api.id] = disposable; + } +} diff --git a/packages/dockview-core/src/dockview/components/panel/content.ts b/packages/dockview-core/src/dockview/components/panel/content.ts index 9934c5ec4..663c152b7 100644 --- a/packages/dockview-core/src/dockview/components/panel/content.ts +++ b/packages/dockview-core/src/dockview/components/panel/content.ts @@ -1,13 +1,21 @@ import { CompositeDisposable, + Disposable, IDisposable, MutableDisposable, } from '../../../lifecycle'; import { Emitter, Event } from '../../../events'; import { trackFocus } from '../../../dom'; import { IDockviewPanel } from '../../dockviewPanel'; +import { DockviewComponent } from '../../dockviewComponent'; +import { DragAndDropObserver } from '../../../dnd/dnd'; +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; onDidFocus: Event; onDidBlur: Event; element: HTMLElement; @@ -16,6 +24,7 @@ export interface IContentContainer extends IDisposable { closePanel: () => void; show(): void; hide(): void; + renderPanel(panel: IDockviewPanel): void; } export class ContentContainer @@ -36,7 +45,12 @@ export class ContentContainer return this._element; } - constructor() { + readonly dropTarget: Droptarget; + + constructor( + private readonly accessor: DockviewComponent, + private readonly group: DockviewGroupPanelModel + ) { super(); this._element = document.createElement('div'); this._element.className = 'content-container'; @@ -49,6 +63,51 @@ export class ContentContainer // 2) register window dragStart events to disable pointer events // 3) register dragEnd events // 4) register mouseMove events (if no buttons are present we take this as a dragEnd event) + + this.dropTarget = new Droptarget(this.element, { + acceptedTargetZones: ['top', 'bottom', 'left', 'right', 'center'], + canDisplayOverlay: (event, position) => { + if ( + this.group.locked === 'no-drop-target' || + (this.group.locked && position === 'center') + ) { + return false; + } + + const data = getPanelData(); + + if (!data && event.shiftKey && !this.group.isFloating) { + return false; + } + + if (data && data.viewId === this.accessor.id) { + if (data.groupId === this.group.id) { + if (position === 'center') { + // don't allow to drop on self for center position + return false; + } + if (data.panelId === null) { + // don't allow group move to drop anywhere on self + return false; + } + } + + const groupHasOnePanelAndIsActiveDragElement = + this.group.panels.length === 1 && + data.groupId === this.group.id; + + return !groupHasOnePanelAndIsActiveDragElement; + } + + return this.group.canDisplayOverlay( + event, + position, + DockviewDropTargets.Panel + ); + }, + }); + + this.addDisposables(this.dropTarget); } show(): void { @@ -59,44 +118,85 @@ export class ContentContainer this.element.style.display = 'none'; } + renderPanel(panel: IDockviewPanel): void { + const isActive = panel === this.group.activePanel; + + switch (panel.api.renderingType) { + case 'destructive': + this.accessor.greadyRenderContainer.remove(panel); + if (isActive) { + if (this.panel) { + this._element.appendChild( + this.panel.view.content.element + ); + } + } + break; + case 'gready': + if ( + panel.view.content.element.parentElement === this._element + ) { + this._element.removeChild(panel.view.content.element); + } + this.accessor.greadyRenderContainer.setReferenceContentContainer( + panel, + this + ); + break; + } + } + public openPanel(panel: IDockviewPanel): void { if (this.panel === panel) { return; } - if (this.panel) { - if (this.panel.view?.content) { - this._element.removeChild(this.panel.view.content.element); - } - this.panel = undefined; + + const panelRenderingType = panel.api.renderingType; + + if ( + this.panel && + this.panel.view.content.element.parentElement === this._element + ) { + /** + * If the currently attached panel is mounted directly to the content then remove it + */ + this._element.removeChild(this.panel.view.content.element); } + this.panel = panel; const disposable = new CompositeDisposable(); - if (this.panel.view) { - const _onDidFocus = this.panel.view.content.onDidFocus; - const _onDidBlur = this.panel.view.content.onDidBlur; + const _onDidFocus = this.panel.view.content.onDidFocus; + const _onDidBlur = this.panel.view.content.onDidBlur; - const focusTracker = trackFocus(this._element); + const focusTracker = trackFocus(this._element); + disposable.addDisposables( + focusTracker, + focusTracker.onDidFocus(() => this._onDidFocus.fire()), + focusTracker.onDidBlur(() => this._onDidBlur.fire()) + ); + + if (_onDidFocus) { disposable.addDisposables( - focusTracker, - focusTracker.onDidFocus(() => this._onDidFocus.fire()), - focusTracker.onDidBlur(() => this._onDidBlur.fire()) + _onDidFocus(() => this._onDidFocus.fire()) ); + } + if (_onDidBlur) { + disposable.addDisposables(_onDidBlur(() => this._onDidBlur.fire())); + } - if (_onDidFocus) { - disposable.addDisposables( - _onDidFocus(() => this._onDidFocus.fire()) - ); - } - if (_onDidBlur) { - disposable.addDisposables( - _onDidBlur(() => this._onDidBlur.fire()) + switch (panelRenderingType) { + case 'gready': + this.accessor.greadyRenderContainer.setReferenceContentContainer( + panel, + this ); - } - - this._element.appendChild(this.panel.view.content.element); + break; + case 'destructive': + this._element.appendChild(this.panel.view.content.element); + break; } this.disposable.value = disposable; @@ -107,8 +207,10 @@ export class ContentContainer } public closePanel(): void { - if (this.panel?.view?.content?.element) { - this._element.removeChild(this.panel.view.content.element); + if (this.panel) { + if (this.accessor.options.renderingType === 'destructive') { + this._element.removeChild(this.panel.view.content.element); + } this.panel = undefined; } } diff --git a/packages/dockview-core/src/dockview/deserializer.ts b/packages/dockview-core/src/dockview/deserializer.ts index b8f308038..4a616fc6c 100644 --- a/packages/dockview-core/src/dockview/deserializer.ts +++ b/packages/dockview-core/src/dockview/deserializer.ts @@ -52,7 +52,8 @@ export class DefaultDockviewDeserialzier implements IPanelDeserializer { this.layout, new DockviewApi(this.layout), group, - view + view, + { renderingType: this.layout.renderingType } ); panel.init({ diff --git a/packages/dockview-core/src/dockview/dockviewComponent.ts b/packages/dockview-core/src/dockview/dockviewComponent.ts index e7ab959c2..2a806705b 100644 --- a/packages/dockview-core/src/dockview/dockviewComponent.ts +++ b/packages/dockview-core/src/dockview/dockviewComponent.ts @@ -55,6 +55,10 @@ import { GroupDragEvent, TabDragEvent, } from './components/titlebar/tabsContainer'; +import { + GreadyRenderContainer, + RenderingType, +} from './components/greadyRenderContainer'; const DEFAULT_FLOATING_GROUP_OVERFLOW_SIZE = 100; @@ -245,6 +249,8 @@ export class DockviewComponent private _options: Exclude; private watermark: IWatermarkRenderer | null = null; + readonly greadyRenderContainer: GreadyRenderContainer; + private readonly _onWillDragPanel = new Emitter(); readonly onWillDragPanel: Event = this._onWillDragPanel.event; @@ -299,6 +305,10 @@ export class DockviewComponent return activeGroup.activePanel; } + get renderingType(): RenderingType { + return this.options.renderingType ?? 'destructive'; + } + constructor(options: DockviewComponentOptions) { super({ proportionalLayout: true, @@ -308,9 +318,17 @@ export class DockviewComponent disableAutoResizing: options.disableAutoResizing, }); + const gready = document.createElement('div'); + gready.style.position = 'relative'; + this.gridview.element.appendChild(gready); + + this.greadyRenderContainer = new GreadyRenderContainer(gready); + toggleClass(this.gridview.element, 'dv-dockview', true); + toggleClass(this.element, 'dv-debug', !!options.debug); this.addDisposables( + this.greadyRenderContainer, this._onWillDragPanel, this._onWillDragGroup, this._onDidActivePanelChange, @@ -1041,6 +1059,7 @@ export class DockviewComponent group.model.removePanel(panel); if (!options.skipDispose) { + this.greadyRenderContainer.remove(panel); panel.dispose(); } @@ -1463,8 +1482,10 @@ export class DockviewComponent this, this._api, group, - view + view, + { renderingType: this.renderingType } ); + panel.init({ title: options.title ?? options.id, params: options?.params ?? {}, diff --git a/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts b/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts index 43ee03b01..cfe10ac6c 100644 --- a/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts +++ b/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts @@ -136,7 +136,7 @@ export class DockviewGroupPanelModel { private readonly tabsContainer: ITabsContainer; private readonly contentContainer: IContentContainer; - private readonly dropTarget: Droptarget; + // private readonly dropTarget: Droptarget; private _activePanel: IDockviewPanel | undefined; private watermark?: IWatermarkRenderer; private _isGroupActive = false; @@ -248,7 +248,7 @@ export class DockviewGroupPanelModel set isFloating(value: boolean) { this._isFloating = value; - this.dropTarget.setTargetZones( + this.contentContainer.dropTarget.setTargetZones( value ? ['center'] : ['top', 'bottom', 'left', 'right', 'center'] ); @@ -272,49 +272,7 @@ export class DockviewGroupPanelModel this.tabsContainer = new TabsContainer(this.accessor, this.groupPanel); - this.contentContainer = new ContentContainer(); - - this.dropTarget = new Droptarget(this.contentContainer.element, { - acceptedTargetZones: ['top', 'bottom', 'left', 'right', 'center'], - canDisplayOverlay: (event, position) => { - if ( - this.locked === 'no-drop-target' || - (this.locked && position === 'center') - ) { - return false; - } - - const data = getPanelData(); - - if (!data && event.shiftKey && !this.isFloating) { - return false; - } - - if (data && data.viewId === this.accessor.id) { - if (data.groupId === this.id) { - if (position === 'center') { - // don't allow to drop on self for center position - return false; - } - if (data.panelId === null) { - // don't allow group move to drop anywhere on self - return false; - } - } - - const groupHasOnePanelAndIsActiveDragElement = - this._panels.length === 1 && data.groupId === this.id; - - return !groupHasOnePanelAndIsActiveDragElement; - } - - return this.canDisplayOverlay( - event, - position, - DockviewDropTargets.Panel - ); - }, - }); + this.contentContainer = new ContentContainer(this.accessor, this); container.append( this.tabsContainer.element, @@ -342,7 +300,7 @@ export class DockviewGroupPanelModel this.contentContainer.onDidBlur(() => { // noop }), - this.dropTarget.onDrop((event) => { + this.contentContainer.dropTarget.onDrop((event) => { this.handleDropEvent(event.nativeEvent, event.position); }), this._onMove, @@ -416,6 +374,10 @@ export class DockviewGroupPanelModel } } + rerender(panel: IDockviewPanel): void { + this.contentContainer.renderPanel(panel); + } + public indexOf(panel: IDockviewPanel): number { return this.tabsContainer.indexOf(panel.id); } @@ -687,15 +649,15 @@ export class DockviewGroupPanelModel const existingPanel = this._panels.indexOf(panel); const hasExistingPanel = existingPanel > -1; + this.tabsContainer.show(); + this.contentContainer.show(); + this.tabsContainer.openPanel(panel, index); if (!skipSetActive) { this.contentContainer.openPanel(panel); } - this.tabsContainer.show(); - this.contentContainer.show(); - if (hasExistingPanel) { // TODO - need to ensure ordering hasn't changed and if it has need to re-order this.panels return; @@ -849,7 +811,7 @@ export class DockviewGroupPanelModel panel.dispose(); } - this.dropTarget.dispose(); + // this.dropTarget.dispose(); this.tabsContainer.dispose(); this.contentContainer.dispose(); } diff --git a/packages/dockview-core/src/dockview/dockviewPanel.ts b/packages/dockview-core/src/dockview/dockviewPanel.ts index 080abf1cf..555956535 100644 --- a/packages/dockview-core/src/dockview/dockviewPanel.ts +++ b/packages/dockview-core/src/dockview/dockviewPanel.ts @@ -9,6 +9,7 @@ import { CompositeDisposable, IDisposable } from '../lifecycle'; import { IPanel, PanelUpdateEvent, Parameters } from '../panel/types'; import { IDockviewPanelModel } from './dockviewPanelModel'; import { DockviewComponent } from './dockviewComponent'; +import { RenderingType } from './components/greadyRenderContainer'; export interface IDockviewPanel extends IDisposable, IPanel { readonly view: IDockviewPanelModel; @@ -33,6 +34,8 @@ export class DockviewPanel private _title: string | undefined; + private _renderingType: RenderingType; + get params(): Parameters | undefined { return this._params; } @@ -45,14 +48,20 @@ export class DockviewPanel return this._group; } + get renderingType(): RenderingType { + return this._renderingType; + } + constructor( public readonly id: string, accessor: DockviewComponent, private readonly containerApi: DockviewApi, group: DockviewGroupPanel, - readonly view: IDockviewPanelModel + readonly view: IDockviewPanelModel, + options: { renderingType: RenderingType } ) { super(); + this._renderingType = options.renderingType; this._group = group; this.api = new DockviewPanelApiImpl(this, this._group, accessor); @@ -65,6 +74,9 @@ export class DockviewPanel // forward the resize event to the group since if you want to resize a panel // you are actually just resizing the panels parent which is the group this.group.api.setSize(event); + }), + this.api.onDidRendeingTypeChange((event) => { + this.group.model.rerender(this); }) ); } @@ -114,6 +126,17 @@ export class DockviewPanel } } + setRenderingType(renderingType: RenderingType): void { + const didChange = renderingType !== this.renderingType; + + if (didChange) { + this._renderingType = renderingType; + this.api._onDidRenderingTypeChange.fire({ + renderingType, + }); + } + } + public update(event: PanelUpdateEvent): void { // merge the new parameters with the existing parameters this._params = { diff --git a/packages/dockview-core/src/dockview/options.ts b/packages/dockview-core/src/dockview/options.ts index 8bd23025a..864e03bd4 100644 --- a/packages/dockview-core/src/dockview/options.ts +++ b/packages/dockview-core/src/dockview/options.ts @@ -96,6 +96,8 @@ export interface DockviewComponentOptions extends DockviewRenderFunctions { minimumHeightWithinViewport?: number; minimumWidthWithinViewport?: number; }; + renderingType?: 'gready' | 'destructive'; + debug?: boolean; } export interface PanelOptions

{ diff --git a/packages/dockview-core/src/dockview/types.ts b/packages/dockview-core/src/dockview/types.ts index 7d870746c..86741882b 100644 --- a/packages/dockview-core/src/dockview/types.ts +++ b/packages/dockview-core/src/dockview/types.ts @@ -91,5 +91,6 @@ export interface GroupviewPanelState { contentComponent?: string; tabComponent?: string; title?: string; + renderType?: string; params?: { [key: string]: any }; } diff --git a/packages/dockview-core/src/dom.ts b/packages/dockview-core/src/dom.ts index a12b50742..12cac06e8 100644 --- a/packages/dockview-core/src/dom.ts +++ b/packages/dockview-core/src/dom.ts @@ -185,3 +185,18 @@ export function quasiPreventDefault(event: Event): void { export function quasiDefaultPrevented(event: Event): boolean { return (event as any)[QUASI_PREVENT_DEFAULT_KEY]; } + +export function getDomNodePagePosition(domNode: Element): { + left: number; + top: number; + width: number; + height: number; +} { + const { left, top, width, height } = domNode.getBoundingClientRect(); + return { + left: left + window.scrollX, + top: top + window.scrollY, + width: width, + height: height, + }; +} diff --git a/packages/dockview-core/src/index.ts b/packages/dockview-core/src/index.ts index e85e335c0..c3f326272 100644 --- a/packages/dockview-core/src/index.ts +++ b/packages/dockview-core/src/index.ts @@ -49,6 +49,8 @@ export * from './splitview/splitviewPanel'; export * from './paneview/paneviewPanel'; export * from './dockview/types'; +export { RenderingType } from './dockview/components/greadyRenderContainer'; + export { Position, positionToDirection, @@ -66,7 +68,11 @@ export { GridviewPanelApi, GridConstraintChangeEvent, } from './api/gridviewPanelApi'; -export { TitleEvent, DockviewPanelApi } from './api/dockviewPanelApi'; +export { + TitleEvent, + RenderingTypeEvent, + DockviewPanelApi, +} from './api/dockviewPanelApi'; export { PanelSizeEvent, PanelConstraintChangeEvent, diff --git a/packages/dockview/src/dockview/dockview.tsx b/packages/dockview/src/dockview/dockview.tsx index c9fd48b09..277306a35 100644 --- a/packages/dockview/src/dockview/dockview.tsx +++ b/packages/dockview/src/dockview/dockview.tsx @@ -10,6 +10,7 @@ import { ITabRenderer, DockviewGroupPanel, IHeaderActionsRenderer, + RenderingType, } from 'dockview-core'; import { ReactPanelContentPart } from './reactContentPart'; import { ReactPanelHeaderPart } from './reactHeaderPart'; @@ -76,6 +77,8 @@ export interface IDockviewReactProps { minimumHeightWithinViewport?: number; minimumWidthWithinViewport?: number; }; + debug?: boolean; + renderingType?: RenderingType; } const DEFAULT_REACT_TAB = 'props.defaultTabComponent'; @@ -175,6 +178,8 @@ export const DockviewReact = React.forwardRef( singleTabMode: props.singleTabMode, disableFloatingGroups: props.disableFloatingGroups, floatingGroupBounds: props.floatingGroupBounds, + renderingType: props.renderingType, + debug: props.debug, }); const { clientWidth, clientHeight } = domRef.current; diff --git a/packages/docs/sandboxes/demo-dockview/src/app.tsx b/packages/docs/sandboxes/demo-dockview/src/app.tsx index 43a1c779f..b1fdc9b0c 100644 --- a/packages/docs/sandboxes/demo-dockview/src/app.tsx +++ b/packages/docs/sandboxes/demo-dockview/src/app.tsx @@ -5,15 +5,71 @@ import { IDockviewPanelHeaderProps, IDockviewPanelProps, IDockviewHeaderActionsProps, + DockviewPanelApi, + RenderingType, } from 'dockview'; import * as React from 'react'; import * as ReactDOM from 'react-dom'; import { v4 } from 'uuid'; import './app.scss'; +const useRenderingType = ( + api: DockviewPanelApi +): [RenderingType, (value: RenderingType) => void] => { + const [mode, setMode] = React.useState(api.renderingType); + + React.useEffect(() => { + const disposable = api.onDidRendeingTypeChange((event) => { + setMode(event.renderingType); + }); + + return () => { + disposable.dispose(); + }; + }, []); + + const _setMode = React.useCallback( + (mode: RenderingType) => { + api.setRenderingType(mode); + }, + [api] + ); + + return [mode, _setMode]; +}; + const components = { default: (props: IDockviewPanelProps<{ title: string }>) => { - return

{props.params.title}
; + const [mode, setMode] = useRenderingType(props.api); + + return ( +
+
+
{props.api.title}
+ +
+ {mode} + +
+
+
+ ); }, }; @@ -233,18 +289,18 @@ const DockviewDemo = (props: { theme?: string }) => { title: 'Panel 4', position: { referencePanel: 'panel_3', direction: 'right' }, }); - event.api.addPanel({ - id: 'panel_5', - component: 'default', - title: 'Panel 5', - position: { referencePanel: 'panel_3', direction: 'below' }, - }); - event.api.addPanel({ - id: 'panel_6', - component: 'default', - title: 'Panel 6', - position: { referencePanel: 'panel_3', direction: 'right' }, - }); + // event.api.addPanel({ + // id: 'panel_5', + // component: 'default', + // title: 'Panel 5', + // position: { referencePanel: 'panel_3', direction: 'below' }, + // }); + // event.api.addPanel({ + // id: 'panel_6', + // component: 'default', + // title: 'Panel 6', + // position: { referencePanel: 'panel_3', direction: 'right' }, + // }); event.api.getPanel('panel_1')!.api.setActive(); @@ -260,6 +316,7 @@ const DockviewDemo = (props: { theme?: string }) => { prefixHeaderActionsComponent={PrefixHeaderControls} onReady={onReady} className={props.theme || 'dockview-theme-abyss'} + // debug={true} /> ); };