feat: gready render mode

This commit is contained in:
mathuo 2023-11-23 22:27:00 +00:00
parent b17aa24637
commit a2a4e68166
No known key found for this signature in database
GPG Key ID: C6EEDEFD6CA07281
32 changed files with 1064 additions and 234 deletions

View File

@ -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",

View File

@ -36,7 +36,7 @@ describe('groupPanelApi', () => {
});
test('updateParameters', () => {
const groupPanel: Partial<IDockviewPanel> = {
const groupPanel: Partial<DockviewPanel> = {
id: 'test_id',
update: jest.fn(),
};
@ -53,7 +53,7 @@ describe('groupPanelApi', () => {
);
const cut = new DockviewPanelApiImpl(
<IDockviewPanel>groupPanel,
<DockviewPanel>groupPanel,
<DockviewGroupPanel>groupViewPanel,
<DockviewComponent>accessor
);
@ -67,7 +67,7 @@ describe('groupPanelApi', () => {
});
test('onDidGroupChange', () => {
const groupPanel: Partial<IDockviewPanel> = {
const groupPanel: Partial<DockviewPanel> = {
id: 'test_id',
};
@ -83,7 +83,7 @@ describe('groupPanelApi', () => {
);
const cut = new DockviewPanelApiImpl(
<IDockviewPanel>groupPanel,
<DockviewPanel>groupPanel,
<DockviewGroupPanel>groupViewPanel,
<DockviewComponent>accessor
);

View File

@ -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<DockviewComponent, []>(() => {
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<IDockviewPanelModel>,
api: { renderer: 'destructive' },
} as Partial<IDockviewPanel>;
cut.openPanel(panel as IDockviewPanel);
@ -107,6 +116,7 @@ describe('contentContainer', () => {
view: {
content: contentRenderer2,
} as Partial<IDockviewPanelModel>,
api: { renderer: 'destructive' },
} as Partial<IDockviewPanel>;
cut.openPanel(panel2 as IDockviewPanel);

View File

@ -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

View File

@ -29,7 +29,9 @@ describe('dockviewPanel', () => {
const group = new groupMock();
const model = <IDockviewPanelModel>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 = <IDockviewPanelModel>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 = <IDockviewPanelModel>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 = <IDockviewPanelModel>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 = <IDockviewPanelModel>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 = <IDockviewPanelModel>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' });

View File

@ -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<void>;
readonly onDidGroupChange: Event<void>;
readonly onDidRendererChange: Event<RendererChangedEvent>;
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<void>();
readonly onDidGroupChange = this._onDidGroupChange.event;
readonly _onDidRendererChange = new Emitter<RendererChangedEvent>();
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);
}

View File

@ -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);
})
);
}

View File

@ -63,6 +63,8 @@ export class Droptarget extends CompositeDisposable {
private readonly _onDrop = new Emitter<DroptargetEvent>();
readonly onDrop: Event<DroptargetEvent> = this._onDrop.event;
readonly dnd: DragAndDropObserver;
private static USED_EVENT_ID = '__dockview_droptarget_event_is_used__';
get state(): Position | undefined {
@ -90,9 +92,7 @@ export class Droptarget extends CompositeDisposable {
this.options.acceptedTargetZones
);
this.addDisposables(
this._onDrop,
new DragAndDropObserver(this.element, {
this.dnd = new DragAndDropObserver(this.element, {
onDragEnter: () => undefined,
onDragOver: (e) => {
if (this._acceptedTargetZonesSet.size === 0) {
@ -180,8 +180,9 @@ export class Droptarget extends CompositeDisposable {
this._onDrop.fire({ position: state, nativeEvent: e });
}
},
})
);
});
this.addDisposables(this._onDrop, this.dnd);
}
setTargetZones(acceptedTargetZones: Position[]): void {

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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<void>;
onDidBlur: Event<void>;
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,26 +118,44 @@ export class ContentContainer
this.element.style.display = 'none';
}
public openPanel(panel: IDockviewPanel): void {
if (this.panel === panel) {
return;
}
renderPanel(panel: IDockviewPanel): void {
const isActive = panel === this.group.activePanel;
let container: HTMLElement;
switch (panel.api.renderer) {
case 'destructive':
this.accessor.greadyRenderContainer.remove(panel);
if (isActive) {
if (this.panel) {
if (this.panel.view?.content) {
this._element.removeChild(this.panel.view.content.element);
this._element.appendChild(
this.panel.view.content.element
);
}
this.panel = undefined;
}
this.panel = panel;
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 (isActive) {
const _onDidFocus = panel.view.content.onDidFocus;
const _onDidBlur = panel.view.content.onDidBlur;
const focusTracker = trackFocus(container);
const disposable = new CompositeDisposable();
if (this.panel.view) {
const _onDidFocus = this.panel.view.content.onDidFocus;
const _onDidBlur = this.panel.view.content.onDidBlur;
const focusTracker = trackFocus(this._element);
disposable.addDisposables(
focusTracker,
focusTracker.onDidFocus(() => this._onDidFocus.fire()),
@ -96,7 +173,64 @@ export class ContentContainer
);
}
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) {
if (this.panel) {
if (this.accessor.options.defaultRenderer === 'destructive') {
this._element.removeChild(this.panel.view.content.element);
}
this.panel = undefined;
}
}

View File

@ -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({

View File

@ -10,6 +10,10 @@
width: 100%;
z-index: 1;
}
.dv-gready-render-container {
position: relative;
}
}
.groupview {

View File

@ -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<DockviewComponentOptions, 'orientation'>;
private watermark: IWatermarkRenderer | null = null;
readonly greadyRenderContainer: GreadyRenderContainer;
private readonly _onWillDragPanel = new Emitter<TabDragEvent>();
readonly onWillDragPanel: Event<TabDragEvent> = 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 ?? {},

View File

@ -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();
}

View File

@ -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 = {

View File

@ -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<P extends object = Parameters> {
@ -168,6 +171,7 @@ export type AddPanelOptions<P extends object = Parameters> = Omit<
> & {
component: string;
tabComponent?: string;
renderer?: DockviewPanelRenderer;
} & Partial<AddPanelOptionsUnion>;
type AddGroupOptionsWithPanel = {

View File

@ -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 };
}

View File

@ -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,
};
}

View File

@ -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,

View File

@ -57,7 +57,7 @@ export const DockviewDefaultTab: React.FunctionComponent<
onClick={onClick}
className="dockview-react-tab"
>
<span className="dockview-react-tab-title">{api.title}</span>
<span className="dockview-react-tab-title">{api.renderer}</span>
{!hideClose && (
<div
className="dv-react-tab-close-btn"

View File

@ -10,6 +10,7 @@ import {
ITabRenderer,
DockviewGroupPanel,
IHeaderActionsRenderer,
DockviewPanelRenderer,
} from 'dockview-core';
import { ReactPanelContentPart } from './reactContentPart';
import { ReactPanelHeaderPart } from './reactHeaderPart';
@ -76,6 +77,8 @@ export interface IDockviewReactProps {
minimumHeightWithinViewport?: number;
minimumWidthWithinViewport?: number;
};
debug?: boolean;
defaultRenderer?: DockviewPanelRenderer;
}
const DEFAULT_REACT_TAB = 'props.defaultTabComponent';
@ -175,6 +178,8 @@ export const DockviewReact = React.forwardRef(
singleTabMode: props.singleTabMode,
disableFloatingGroups: props.disableFloatingGroups,
floatingGroupBounds: props.floatingGroupBounds,
defaultRenderer: props.defaultRenderer,
debug: props.debug,
});
const { clientWidth, clientHeight } = domRef.current;

View File

@ -28,8 +28,9 @@ import DockviewWithIFrames from '@site/sandboxes/iframe-dockview/src/app';
import DockviewFloating from '@site/sandboxes/floatinggroup-dockview/src/app';
import DockviewLockedGroup from '@site/sandboxes/lockedgroup-dockview/src/app';
import DockviewKeyboard from '@site/sandboxes/keyboard-dockview/src/app';
import DockviewRenderMode from '@site/sandboxes/rendermode-dockview/src/app';
import { DocRef, Markdown } from '@site/src/components/ui/reference/docRef';
import { DocRef } from '@site/src/components/ui/reference/docRef';
import { attach as attachDockviewVanilla } from '@site/sandboxes/javascript/vanilla-dockview/src/app';
import { attach as attachSimpleDockview } from '@site/sandboxes/javascript/simple-dockview/src/app';
@ -816,6 +817,24 @@ api.group.api.setConstraints(...)
react={DockviewConstraints}
/>
## 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.
<MultiFrameworkContainer
height={500}
sandboxId="rendermode-dockview"
react={DockviewRenderMode}
/>
## iFrames
iFrames required special attention because of a particular behaviour in how iFrames render:

View File

@ -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<DockviewPanelRenderer>(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 <div style={{ padding: '20px' }}>{props.params.title}</div>;
const [mode, setMode] = useRenderer(props.api);
return (
<div style={{ height: '100%', overflow: 'auto', color: 'white' }}>
<div
style={{
height: '1000px',
padding: '20px',
overflow: 'auto',
}}
>
<div>{props.api.title}</div>
<input />
<div>
{mode}
<button
onClick={() => {
setMode(
mode === 'destructive'
? 'gready'
: 'destructive'
);
}}
>
Toggle render mode
</button>
</div>
</div>
</div>
);
},
};
@ -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}
/>
);
};

View File

@ -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"
]
}

View File

@ -0,0 +1,45 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="theme-color" content="#000000">
<!--
manifest.json provides metadata used when your web app is added to the
homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
</head>
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

View File

@ -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<DockviewPanelRenderer>(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 (
<div style={{ height: '100%', overflow: 'auto', color: 'white' }}>
<div
style={{
height: '1000px',
padding: '20px',
overflow: 'auto',
}}
>
<div>{props.api.title}</div>
<input />
<div>
{mode}
<button
onClick={() => {
setMode(
mode === 'destructive'
? 'gready'
: 'destructive'
);
}}
>
Toggle render mode
</button>
</div>
</div>
</div>
);
},
};
const DockviewDemo = (props: { theme?: string }) => {
const [value, setValue] = React.useState<string>('100');
const [api, setApi] = React.useState<DockviewApi | null>(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 (
<div
style={{ height: '100%', display: 'flex', flexDirection: 'column' }}
>
<div>
<button onClick={onSave}>Save</button>
<button onClick={onLoad}>Load</button>
<input
onChange={(event) => setValue(event.target.value)}
type="range"
min="1"
max="100"
value={value}
/>
</div>
<div style={{ height: `${value}%`, width: `${value}%` }}>
<DockviewReact
components={components}
onReady={onReady}
className={props.theme || 'dockview-theme-abyss'}
/>
</div>
</div>
);
};
export default DockviewDemo;

View File

@ -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(
<StrictMode>
<div className="app">
<App />
</div>
</StrictMode>
);
}

View File

@ -0,0 +1,16 @@
body {
margin: 0px;
color: white;
font-family: sans-serif;
text-align: center;
}
#root {
height: 100vh;
width: 100vw;
}
.app {
height: 100%;
}

View File

@ -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
}
}

View File

@ -112,8 +112,6 @@ const Container = () => {
</div>
</div>
);
return <App />;
};
export default Container;