diff --git a/.codesandbox/ci.json b/.codesandbox/ci.json index e5e5db007..53e2d4776 100644 --- a/.codesandbox/ci.json +++ b/.codesandbox/ci.json @@ -23,6 +23,7 @@ "/packages/docs/sandboxes/nativeapp-dockview", "/packages/docs/sandboxes/nested-dockview", "/packages/docs/sandboxes/rendering-dockview", + "/packages/docs/sandboxes/rendermode-dockview", "/packages/docs/sandboxes/resize-dockview", "/packages/docs/sandboxes/resizecontainer-dockview", "/packages/docs/sandboxes/simple-dockview", @@ -37,4 +38,4 @@ "/packages/docs/sandboxes/javascript/vanilla-dockview" ], "node": "18" -} \ No newline at end of file +} 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..ecb7cb948 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 { + renderer: '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: { renderer: 'destructive' }, } as Partial; cut.openPanel(panel as IDockviewPanel); @@ -107,6 +116,7 @@ describe('contentContainer', () => { view: { content: contentRenderer2, } as Partial, + api: { renderer: '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..8b574ce6d 100644 --- a/packages/dockview-core/src/__tests__/dockview/dockviewGroupPanelModel.spec.ts +++ b/packages/dockview-core/src/__tests__/dockview/dockviewGroupPanelModel.spec.ts @@ -257,9 +257,15 @@ describe('groupview', () => { }); test('panel events are captured during de-serialization', () => { - const panel1 = new TestPanel('panel1', jest.fn() as any); - const panel2 = new TestPanel('panel2', jest.fn() as any); - const panel3 = new TestPanel('panel3', jest.fn() as any); + const panel1 = new TestPanel('panel1', { + renderer: 'destructive', + } as any); + const panel2 = new TestPanel('panel2', { + renderer: 'destructive', + } as any); + const panel3 = new TestPanel('panel3', { + renderer: 'destructive', + } as any); const groupview2 = new DockviewGroupPanel(dockview, 'groupview-2', { panels: [panel1, panel2, panel3], @@ -343,9 +349,15 @@ describe('groupview', () => { }) ); - const panel1 = new TestPanel('panel1', jest.fn() as any); - const panel2 = new TestPanel('panel2', jest.fn() as any); - const panel3 = new TestPanel('panel3', jest.fn() as any); + const panel1 = new TestPanel('panel1', { + renderer: 'destructive', + } as any); + const panel2 = new TestPanel('panel2', { + renderer: 'destructive', + } as any); + const panel3 = new TestPanel('panel3', { + renderer: 'destructive', + } as any); expect(events.length).toBe(0); @@ -423,9 +435,15 @@ describe('groupview', () => { }); test('moveToPrevious and moveToNext', () => { - const panel1 = new TestPanel('panel1', jest.fn() as any); - const panel2 = new TestPanel('panel2', jest.fn() as any); - const panel3 = new TestPanel('panel3', jest.fn() as any); + const panel1 = new TestPanel('panel1', { + renderer: 'destructive', + } as any); + const panel2 = new TestPanel('panel2', { + renderer: 'destructive', + } as any); + const panel3 = new TestPanel('panel3', { + renderer: 'destructive', + } as any); groupview.model.openPanel(panel1); groupview.model.openPanel(panel2); @@ -469,9 +487,15 @@ describe('groupview', () => { }); test('closeAllPanels with panels', () => { - const panel1 = new TestPanel('panel1', jest.fn() as any); - const panel2 = new TestPanel('panel2', jest.fn() as any); - const panel3 = new TestPanel('panel3', jest.fn() as any); + const panel1 = new TestPanel('panel1', { + renderer: 'destructive', + } as any); + const panel2 = new TestPanel('panel2', { + renderer: 'destructive', + } as any); + const panel3 = new TestPanel('panel3', { + renderer: 'destructive', + } as any); groupview.model.openPanel(panel1); groupview.model.openPanel(panel2); @@ -576,19 +600,25 @@ describe('groupview', () => { .getElementsByClassName('content-container') .item(0)!.childNodes; - const panel1 = new TestPanel('id_1', null as any); + const panel1 = new TestPanel('id_1', { + renderer: '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', { + renderer: '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', { + renderer: 'destructive', + } as any); cut.openPanel(panel3, { skipSetPanelActive: true }); expect(contentContainer.length).toBe(1); @@ -790,7 +820,11 @@ describe('groupview', () => { new groupPanelMock() as DockviewGroupPanel ); - cut.openPanel(new TestPanel('panel1', jest.fn() as any)); + cut.openPanel( + new TestPanel('panel1', { + renderer: 'destructive', + } as any) + ); const element = container .getElementsByClassName('content-container') @@ -856,8 +890,16 @@ describe('groupview', () => { new groupPanelMock() as DockviewGroupPanel ); - cut.openPanel(new TestPanel('panel1', jest.fn() as any)); - cut.openPanel(new TestPanel('panel2', jest.fn() as any)); + cut.openPanel( + new TestPanel('panel1', { + renderer: 'destructive', + } as any) + ); + cut.openPanel( + new TestPanel('panel2', { + renderer: 'destructive', + } as any) + ); const element = container .getElementsByClassName('content-container') @@ -923,8 +965,16 @@ describe('groupview', () => { new groupPanelMock() as DockviewGroupPanel ); - cut.openPanel(new TestPanel('panel1', jest.fn() as any)); - cut.openPanel(new TestPanel('panel2', jest.fn() as any)); + cut.openPanel( + new TestPanel('panel1', { + renderer: 'destructive', + } as any) + ); + cut.openPanel( + new TestPanel('panel2', { + renderer: 'destructive', + } as any) + ); const element = container .getElementsByClassName('content-container') @@ -1025,7 +1075,11 @@ describe('groupview', () => { container.getElementsByClassName('watermark-test-container').length ).toBe(1); - cut.openPanel(new TestPanel('panel1', jest.fn() as any)); + cut.openPanel( + new TestPanel('panel1', { + renderer: 'destructive', + } as any) + ); expect( container.getElementsByClassName('watermark-test-container').length @@ -1035,7 +1089,11 @@ describe('groupview', () => { .length ).toBe(1); - cut.openPanel(new TestPanel('panel2', jest.fn() as any)); + cut.openPanel( + new TestPanel('panel2', { + renderer: 'destructive', + } as any) + ); expect( container.getElementsByClassName('watermark-test-container').length @@ -1053,7 +1111,11 @@ describe('groupview', () => { container.getElementsByClassName('watermark-test-container').length ).toBe(1); - cut.openPanel(new TestPanel('panel1', jest.fn() as any)); + cut.openPanel( + new TestPanel('panel1', { + renderer: 'destructive', + } as any) + ); expect( container.getElementsByClassName('watermark-test-container').length diff --git a/packages/dockview-core/src/__tests__/dockview/dockviewPanel.spec.ts b/packages/dockview-core/src/__tests__/dockview/dockviewPanel.spec.ts index c67546db8..352804f56 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, { + renderer: '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, { + renderer: '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, { + renderer: '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, { + renderer: '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, { + renderer: '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, { + renderer: '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..b6eb47706 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 { DockviewPanelRenderer } from '../dockview/components/greadyRenderContainer'; export interface TitleEvent { readonly title: string; } +export interface RendererChangedEvent { + renderer: DockviewPanelRenderer; +} + /* * 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 renderer: DockviewPanelRenderer; readonly title: string | undefined; readonly onDidActiveGroupChange: Event; readonly onDidGroupChange: Event; + readonly onDidRendererChange: Event; close(): void; setTitle(title: string): void; + setRenderer(renderer: DockviewPanelRenderer): 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 _onDidRendererChange = new Emitter(); + readonly onDidRendererChange = this._onDidRendererChange.event; + private readonly disposable = new MutableDisposable(); get title(): string | undefined { @@ -58,6 +69,10 @@ export class DockviewPanelApiImpl return !!this.group?.isActive; } + get renderer(): DockviewPanelRenderer { + return this.panel.renderer; + } + 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._onDidRendererChange, this._onDidTitleChange, this._onDidGroupChange, this._onDidActiveGroupChange @@ -117,6 +133,10 @@ export class DockviewPanelApiImpl this.panel.setTitle(title); } + setRenderer(renderer: DockviewPanelRenderer): void { + this.panel.setRenderer(renderer); + } + 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..7e08072e4 --- /dev/null +++ b/packages/dockview-core/src/dockview/components/greadyReadyContainer.scss @@ -0,0 +1,16 @@ +.dv-render-overlay { + position: absolute; + z-index: 1; + height: 100%; + + &.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..b3f75f36c --- /dev/null +++ b/packages/dockview-core/src/dockview/components/greadyRenderContainer.ts @@ -0,0 +1,141 @@ +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 DockviewPanelRenderer = 'destructive' | 'gready'; + +export interface IRenderable { + readonly element: HTMLElement; + readonly dropTarget: Droptarget; +} + +function createFocusableElement(): HTMLDivElement { + const element = document.createElement('div'); + element.tabIndex = -1; + return element; +} + +export class GreadyRenderContainer extends CompositeDisposable { + private readonly map: Record< + string, + { disposable: IDisposable; element: HTMLElement } + > = {}; + + 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.disposable.dispose(); + } + }, + }); + } + + remove(panel: IDockviewPanel): boolean { + if (this.map[panel.api.id]) { + this.map[panel.api.id].disposable.dispose(); + delete this.map[panel.api.id]; + return true; + } + return false; + } + + setReferenceContentContainer( + panel: IDockviewPanel, + referenceContainer: IRenderable + ): HTMLElement { + if (!this.map[panel.api.id]) { + const element = createFocusableElement(); + element.className = 'dv-render-overlay'; + + this.map[panel.api.id] = { + disposable: Disposable.NONE, + element, + }; + } + + this.map[panel.api.id]?.disposable.dispose(); + const focusContainer = this.map[panel.api.id].element; + + if (panel.view.content.element.parentElement !== focusContainer) { + focusContainer.appendChild(panel.view.content.element); + } + + if (focusContainer.parentElement !== this.element) { + this.element.appendChild(focusContainer); + } + + const resize = () => { + // TODO propagate position to avoid getDomNodePagePosition calls + const box = getDomNodePagePosition(referenceContainer.element); + const box2 = getDomNodePagePosition(this.element); + focusContainer.style.left = `${box.left - box2.left}px`; + focusContainer.style.top = `${box.top - box2.top}px`; + focusContainer.style.width = `${box.width}px`; + focusContainer.style.height = `${box.height}px`; + + toggleClass( + focusContainer, + 'dv-render-overlay-float', + panel.group.api.isFloating + ); + }; + + const disposable = new CompositeDisposable( + /** + * since container is positioned absoutely we must explicitly forward + * the dnd events for the expect behaviours to continue to occur in terms of dnd + */ + new DragAndDropObserver(focusContainer, { + 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) => { + focusContainer.style.display = event.isVisible ? '' : 'none'; + }), + panel.api.onDidDimensionsChange((event) => { + resize(); + }), + { + dispose: () => { + focusContainer.removeChild(panel.view.content.element); + this.element.removeChild(focusContainer); + }, + } + ); + + queueMicrotask(() => { + /** + * wait until everything has finished in the current stack-frame call before + * calling the first resize as other size-altering events may still occur before + * the end of the stack-frame. + */ + resize(); + }); + + this.map[panel.api.id].disposable = disposable; + + return focusContainer; + } +} diff --git a/packages/dockview-core/src/dockview/components/panel/content.ts b/packages/dockview-core/src/dockview/components/panel/content.ts index 9934c5ec4..ec33ce675 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,25 +118,43 @@ export class ContentContainer this.element.style.display = 'none'; } - 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; - } - this.panel = panel; + renderPanel(panel: IDockviewPanel): void { + const isActive = panel === this.group.activePanel; - const disposable = new CompositeDisposable(); + let container: HTMLElement; + + switch (panel.api.renderer) { + case 'destructive': + this.accessor.greadyRenderContainer.remove(panel); + if (isActive) { + if (this.panel) { + this._element.appendChild( + this.panel.view.content.element + ); + } + } + container = this._element; + break; + case 'gready': + if ( + panel.view.content.element.parentElement === this._element + ) { + this._element.removeChild(panel.view.content.element); + } + container = + this.accessor.greadyRenderContainer.setReferenceContentContainer( + panel, + this + ); + break; + } - if (this.panel.view) { - const _onDidFocus = this.panel.view.content.onDidFocus; - const _onDidBlur = this.panel.view.content.onDidBlur; + if (isActive) { + const _onDidFocus = panel.view.content.onDidFocus; + const _onDidBlur = panel.view.content.onDidBlur; - const focusTracker = trackFocus(this._element); + const focusTracker = trackFocus(container); + const disposable = new CompositeDisposable(); disposable.addDisposables( focusTracker, @@ -96,7 +173,64 @@ export class ContentContainer ); } - this._element.appendChild(this.panel.view.content.element); + this.disposable.value = disposable; + } + } + + public openPanel(panel: IDockviewPanel): void { + if (this.panel === panel) { + return; + } + + const renderer = panel.api.renderer; + + 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; + + let container: HTMLElement; + + switch (renderer) { + case 'gready': + container = + this.accessor.greadyRenderContainer.setReferenceContentContainer( + panel, + this + ); + break; + case 'destructive': + this._element.appendChild(this.panel.view.content.element); + container = this._element; + break; + } + + const _onDidFocus = this.panel.view.content.onDidFocus; + const _onDidBlur = this.panel.view.content.onDidBlur; + + const disposable = new CompositeDisposable(); + const focusTracker = trackFocus(container); + + disposable.addDisposables( + focusTracker, + focusTracker.onDidFocus(() => this._onDidFocus.fire()), + focusTracker.onDidBlur(() => this._onDidBlur.fire()) + ); + + if (_onDidFocus) { + disposable.addDisposables( + _onDidFocus(() => this._onDidFocus.fire()) + ); + } + if (_onDidBlur) { + disposable.addDisposables(_onDidBlur(() => this._onDidBlur.fire())); } this.disposable.value = disposable; @@ -107,8 +241,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.defaultRenderer === '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..3b9ab8e96 100644 --- a/packages/dockview-core/src/dockview/deserializer.ts +++ b/packages/dockview-core/src/dockview/deserializer.ts @@ -21,7 +21,7 @@ interface LegacyState extends GroupviewPanelState { } export class DefaultDockviewDeserialzier implements IPanelDeserializer { - constructor(private readonly layout: DockviewComponent) {} + constructor(private readonly accessor: DockviewComponent) {} public fromJSON( panelData: GroupviewPanelState, @@ -41,7 +41,7 @@ export class DefaultDockviewDeserialzier implements IPanelDeserializer { : panelData.tabComponent; const view = new DockviewPanelModel( - this.layout, + this.accessor, panelId, contentComponent, tabComponent @@ -49,10 +49,13 @@ export class DefaultDockviewDeserialzier implements IPanelDeserializer { const panel = new DockviewPanel( panelId, - this.layout, - new DockviewApi(this.layout), + this.accessor, + new DockviewApi(this.accessor), group, - view + view, + { + renderer: panelData.renderer, + } ); panel.init({ diff --git a/packages/dockview-core/src/dockview/dockviewComponent.scss b/packages/dockview-core/src/dockview/dockviewComponent.scss index 1aa878539..3bcfb7a57 100644 --- a/packages/dockview-core/src/dockview/dockviewComponent.scss +++ b/packages/dockview-core/src/dockview/dockviewComponent.scss @@ -10,6 +10,10 @@ width: 100%; z-index: 1; } + + .dv-gready-render-container { + position: relative; + } } .groupview { diff --git a/packages/dockview-core/src/dockview/dockviewComponent.ts b/packages/dockview-core/src/dockview/dockviewComponent.ts index e7ab959c2..b4617da9e 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, + DockviewPanelRenderer, +} 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 renderer(): DockviewPanelRenderer { + return this.options.defaultRenderer ?? 'destructive'; + } + constructor(options: DockviewComponentOptions) { super({ proportionalLayout: true, @@ -308,9 +318,17 @@ export class DockviewComponent disableAutoResizing: options.disableAutoResizing, }); + const gready = document.createElement('div'); + gready.className = 'dv-gready-render-container'; + 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, + { renderer: options.renderer } ); + 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..da990c58e 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 { DockviewPanelRenderer } from './components/greadyRenderContainer'; export interface IDockviewPanel extends IDisposable, IPanel { readonly view: IDockviewPanelModel; @@ -28,10 +29,11 @@ export class DockviewPanel implements IDockviewPanel { readonly api: DockviewPanelApiImpl; + private _group: DockviewGroupPanel; private _params?: Parameters; - private _title: string | undefined; + private _renderer: DockviewPanelRenderer | undefined; get params(): Parameters | undefined { return this._params; @@ -45,14 +47,20 @@ export class DockviewPanel return this._group; } + get renderer(): DockviewPanelRenderer { + return this._renderer ?? this.accessor.renderer; + } + constructor( public readonly id: string, - accessor: DockviewComponent, + private readonly accessor: DockviewComponent, private readonly containerApi: DockviewApi, group: DockviewGroupPanel, - readonly view: IDockviewPanelModel + readonly view: IDockviewPanelModel, + options: { renderer?: DockviewPanelRenderer } ) { super(); + this._renderer = options.renderer; this._group = group; this.api = new DockviewPanelApiImpl(this, this._group, accessor); @@ -65,6 +73,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.onDidRendererChange((event) => { + this.group.model.rerender(this); }) ); } @@ -95,6 +106,7 @@ export class DockviewPanel ? this._params : undefined, title: this.title, + renderer: this._renderer, }; } @@ -114,6 +126,17 @@ export class DockviewPanel } } + setRenderer(renderer: DockviewPanelRenderer): void { + const didChange = renderer !== this.renderer; + + if (didChange) { + this._renderer = renderer; + this.api._onDidRendererChange.fire({ + renderer: renderer, + }); + } + } + 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..97587f1a6 100644 --- a/packages/dockview-core/src/dockview/options.ts +++ b/packages/dockview-core/src/dockview/options.ts @@ -20,6 +20,7 @@ import { FrameworkFactory, } from '../panel/componentFactory'; import { DockviewGroupPanelApi } from '../api/dockviewGroupPanelApi'; +import { DockviewPanelRenderer } from './components/greadyRenderContainer'; export interface IHeaderActionsRenderer extends IDisposable { readonly element: HTMLElement; @@ -96,6 +97,8 @@ export interface DockviewComponentOptions extends DockviewRenderFunctions { minimumHeightWithinViewport?: number; minimumWidthWithinViewport?: number; }; + defaultRenderer?: DockviewPanelRenderer; + debug?: boolean; } export interface PanelOptions

{ @@ -168,6 +171,7 @@ export type AddPanelOptions

= Omit< > & { component: string; tabComponent?: string; + renderer?: DockviewPanelRenderer; } & Partial; type AddGroupOptionsWithPanel = { diff --git a/packages/dockview-core/src/dockview/types.ts b/packages/dockview-core/src/dockview/types.ts index 7d870746c..8afab7040 100644 --- a/packages/dockview-core/src/dockview/types.ts +++ b/packages/dockview-core/src/dockview/types.ts @@ -5,6 +5,7 @@ import { DockviewApi } from '../api/component.api'; import { Event } from '../events'; import { Optional } from '../types'; import { DockviewGroupPanel, IDockviewGroupPanel } from './dockviewGroupPanel'; +import { DockviewPanelRenderer } from './components/greadyRenderContainer'; export enum DockviewDropTargets { Tab, @@ -91,5 +92,6 @@ export interface GroupviewPanelState { contentComponent?: string; tabComponent?: string; title?: string; + renderer?: DockviewPanelRenderer; 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..7e9b1205c 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 { DockviewPanelRenderer } 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, + RendererChangedEvent, + DockviewPanelApi, +} from './api/dockviewPanelApi'; export { PanelSizeEvent, PanelConstraintChangeEvent, diff --git a/packages/dockview/src/dockview/defaultTab.tsx b/packages/dockview/src/dockview/defaultTab.tsx index 5032cfb03..850d35e7c 100644 --- a/packages/dockview/src/dockview/defaultTab.tsx +++ b/packages/dockview/src/dockview/defaultTab.tsx @@ -57,7 +57,7 @@ export const DockviewDefaultTab: React.FunctionComponent< onClick={onClick} className="dockview-react-tab" > - {api.title} + {api.renderer} {!hideClose && (

+## Render Mode + +Dockview has two rendering modes `destructive` (default) and `gready`. A rendering mode can be defined through the `renderer` prop to `DockviewReact` or at an individual panel level when added where +the panel declaration takes precedence if both are defined. Rendering modes defined at the panel level are persisted, those defined at the `DockviewReact` level are not persisted. + +destructive +- Destructive mode is the default mode. In this mode when a panel is no longer visible through either it's visiblity being hidden or it not being the active panel within a group the panels HTMLElement is removed +from the DOM and any DOM state such as scrollbar positions will be lost. If you are using any ResizeObservers to measure size this will result both zero height and width as the HTMLElement no longer belongs to the DOM. +This design allows for maximum performance at some cost. +- Gready mode. In this mode when panels become hidden the HTMLElement is not destroyed so all DOM state such as scrollbar positions will be maintained. This is implemented by rendering each panel as an absolutely positioned +HTMLElement and hidden the HTMLElement with `display: none` when it should be hidden. + + + ## iFrames iFrames required special attention because of a particular behaviour in how iFrames render: diff --git a/packages/docs/sandboxes/demo-dockview/src/app.tsx b/packages/docs/sandboxes/demo-dockview/src/app.tsx index 43a1c779f..bccf309f4 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, + DockviewPanelRenderer, } from 'dockview'; import * as React from 'react'; import * as ReactDOM from 'react-dom'; import { v4 } from 'uuid'; import './app.scss'; +const useRenderer = ( + api: DockviewPanelApi +): [DockviewPanelRenderer, (value: DockviewPanelRenderer) => void] => { + const [mode, setMode] = React.useState(api.renderer); + + React.useEffect(() => { + const disposable = api.onDidRendererChange((event) => { + setMode(event.renderer); + }); + + return () => { + disposable.dispose(); + }; + }, []); + + const _setMode = React.useCallback( + (mode: DockviewPanelRenderer) => { + api.setRenderer(mode); + }, + [api] + ); + + return [mode, _setMode]; +}; + const components = { default: (props: IDockviewPanelProps<{ title: string }>) => { - return
{props.params.title}
; + const [mode, setMode] = useRenderer(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} /> ); }; diff --git a/packages/docs/sandboxes/rendermode-dockview/package.json b/packages/docs/sandboxes/rendermode-dockview/package.json new file mode 100644 index 000000000..d1fcfa6ce --- /dev/null +++ b/packages/docs/sandboxes/rendermode-dockview/package.json @@ -0,0 +1,33 @@ +{ + "name": "rendermode-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", + "@types/uuid": "^9.0.0", + "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/rendermode-dockview/public/index.html b/packages/docs/sandboxes/rendermode-dockview/public/index.html new file mode 100644 index 000000000..5a4850c1d --- /dev/null +++ b/packages/docs/sandboxes/rendermode-dockview/public/index.html @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + React App + + + + +
+ + + + diff --git a/packages/docs/sandboxes/rendermode-dockview/src/app.scss b/packages/docs/sandboxes/rendermode-dockview/src/app.scss new file mode 100644 index 000000000..e69de29bb diff --git a/packages/docs/sandboxes/rendermode-dockview/src/app.tsx b/packages/docs/sandboxes/rendermode-dockview/src/app.tsx new file mode 100644 index 000000000..941a135ea --- /dev/null +++ b/packages/docs/sandboxes/rendermode-dockview/src/app.tsx @@ -0,0 +1,160 @@ +import { + DockviewReact, + DockviewReadyEvent, + IDockviewPanelProps, + DockviewPanelApi, + DockviewPanelRenderer, + DockviewApi, + SerializedDockview, +} from 'dockview'; +import * as React from 'react'; +import './app.scss'; + +const useRenderer = ( + api: DockviewPanelApi +): [DockviewPanelRenderer, (value: DockviewPanelRenderer) => void] => { + const [mode, setMode] = React.useState(api.renderer); + + React.useEffect(() => { + const disposable = api.onDidRendererChange((event) => { + setMode(event.renderer); + }); + + return () => { + disposable.dispose(); + }; + }, []); + + const _setMode = React.useCallback( + (mode: DockviewPanelRenderer) => { + api.setRenderer(mode); + }, + [api] + ); + + return [mode, _setMode]; +}; + +const components = { + default: (props: IDockviewPanelProps<{ title: string }>) => { + const [mode, setMode] = useRenderer(props.api); + + return ( +
+
+
{props.api.title}
+ +
+ {mode} + +
+
+
+ ); + }, +}; + +const DockviewDemo = (props: { theme?: string }) => { + const [value, setValue] = React.useState('100'); + const [api, setApi] = React.useState(null); + + const onReady = (event: DockviewReadyEvent) => { + event.api.addPanel({ + id: 'panel_1', + component: 'default', + title: 'Panel 1', + }); + event.api.addPanel({ + id: 'panel_2', + component: 'default', + title: 'Panel 2', + position: { referencePanel: 'panel_1', direction: 'within' }, + }); + event.api.addPanel({ + id: 'panel_3', + component: 'default', + title: 'Panel 3', + }); + + event.api.addPanel({ + id: 'panel_4', + component: 'default', + title: 'Panel 4', + position: { referencePanel: 'panel_3', direction: 'below' }, + }); + + setApi(event.api); + }; + + const onSave = () => { + if (!api) { + return; + } + + localStorage.setItem( + 'dv_rendermode_state', + JSON.stringify({ size: value, state: api.toJSON() }) + ); + }; + + const onLoad = () => { + if (!api) { + return; + } + + const state = localStorage.getItem('dv_rendermode_state'); + if (typeof state !== 'string') { + return; + } + + const json = JSON.parse(state) as { + size: string; + state: SerializedDockview; + }; + setValue(json.size); + api.fromJSON(json.state); + }; + + return ( +
+
+ + + setValue(event.target.value)} + type="range" + min="1" + max="100" + value={value} + /> +
+
+ +
+
+ ); +}; + +export default DockviewDemo; diff --git a/packages/docs/sandboxes/rendermode-dockview/src/index.tsx b/packages/docs/sandboxes/rendermode-dockview/src/index.tsx new file mode 100644 index 000000000..2fe1be232 --- /dev/null +++ b/packages/docs/sandboxes/rendermode-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/rendermode-dockview/src/styles.css b/packages/docs/sandboxes/rendermode-dockview/src/styles.css new file mode 100644 index 000000000..92b6a1b36 --- /dev/null +++ b/packages/docs/sandboxes/rendermode-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/rendermode-dockview/tsconfig.json b/packages/docs/sandboxes/rendermode-dockview/tsconfig.json new file mode 100644 index 000000000..cdc4fb5f5 --- /dev/null +++ b/packages/docs/sandboxes/rendermode-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/resizecontainer-dockview/src/app.tsx b/packages/docs/sandboxes/resizecontainer-dockview/src/app.tsx index 05ef34182..95fb87945 100644 --- a/packages/docs/sandboxes/resizecontainer-dockview/src/app.tsx +++ b/packages/docs/sandboxes/resizecontainer-dockview/src/app.tsx @@ -112,8 +112,6 @@ const Container = () => {
); - - return ; }; export default Container;