feat: floating group persistance

This commit is contained in:
mathuo 2023-06-21 21:07:11 +01:00
parent c53d2690c3
commit 86be252e99
No known key found for this signature in database
GPG Key ID: C6EEDEFD6CA07281
24 changed files with 699 additions and 125 deletions

View File

@ -8,7 +8,7 @@ import { PanelUpdateEvent } from '../../panel/types';
import { Orientation } from '../../splitview/splitview';
import { CompositeDisposable } from '../../lifecycle';
import { Emitter } from '../../events';
import { IDockviewPanel } from '../../dockview/dockviewPanel';
import { DockviewPanel, IDockviewPanel } from '../../dockview/dockviewPanel';
import { DockviewGroupPanel } from '../../dockview/dockviewGroupPanel';
class PanelContentPartTest implements IContentRenderer {
@ -2619,4 +2619,32 @@ describe('dockviewComponent', () => {
},
});
});
test('floating: group is removed', async () => {
const container = document.createElement('div');
const dockview = new DockviewComponent({
parentElement: container,
components: {
default: PanelContentPartTest,
},
tabComponents: {
test_tab_id: PanelTabPartTest,
},
orientation: Orientation.HORIZONTAL,
});
dockview.layout(1000, 500);
expect(dockview.groups.length).toBe(0);
const panel = dockview.addPanel({
id: 'panel_1',
component: 'default',
floating: true,
});
expect(dockview.groups.length).toBe(1);
dockview.removePanel(panel);
expect(dockview.groups.length).toBe(0);
});
});

View File

@ -8,6 +8,7 @@
height: 100%;
width: 100%;
z-index: 10000;
pointer-events: none;
> .drop-target-selection {
position: relative;
@ -15,7 +16,9 @@
height: 100%;
width: 100%;
background-color: var(--dv-drag-over-background-color);
transition: top 70ms ease-out,left 70ms ease-out,width 70ms ease-out,height 70ms ease-out,opacity .15s ease-out;
transition: top 70ms ease-out, left 70ms ease-out,
width 70ms ease-out, height 70ms ease-out,
opacity 0.15s ease-out;
will-change: transform;
pointer-events: none;

View File

@ -54,6 +54,17 @@ export type CanDisplayOverlay =
| boolean
| ((dragEvent: DragEvent, state: Position) => boolean);
const eventMarkTag = 'dv_droptarget_marked';
function markEvent(event: DragEvent): void {
(event as any)[eventMarkTag] = true;
}
function isEventMarked(event: DragEvent) {
const value = (event as any)[eventMarkTag];
return typeof value === 'boolean' && value;
}
export class Droptarget extends CompositeDisposable {
private targetElement: HTMLElement | undefined;
private overlayElement: HTMLElement | undefined;
@ -114,7 +125,7 @@ export class Droptarget extends CompositeDisposable {
height
);
if (quadrant === null) {
if (isEventMarked(e) || quadrant === null) {
// no drop target should be displayed
this.removeDropTarget();
return;
@ -128,6 +139,8 @@ export class Droptarget extends CompositeDisposable {
return;
}
markEvent(e);
if (!this.targetElement) {
this.targetElement = document.createElement('div');
this.targetElement.className = 'drop-target-dropzone';

View File

@ -17,7 +17,7 @@ export class GroupDragHandler extends DragHandler {
}
override isCancelled(_event: DragEvent): boolean {
if (this.group.model.isFloating) {
if (this.group.model.isFloating && !_event.shiftKey) {
return true;
}
return false;

View File

@ -1,5 +1,10 @@
import { toggleClass } from '../dom';
import { addDisposableListener, addDisposableWindowListener } from '../events';
import {
Emitter,
Event,
addDisposableListener,
addDisposableWindowListener,
} from '../events';
import { CompositeDisposable, MutableDisposable } from '../lifecycle';
import { clamp } from '../math';
@ -21,6 +26,9 @@ const bringElementToFront = (() => {
export class Overlay extends CompositeDisposable {
private _element: HTMLElement = document.createElement('div');
private readonly _onDidChange = new Emitter<void>();
readonly onDidChange: Event<void> = this._onDidChange.event;
constructor(
private readonly options: {
height: number;
@ -35,6 +43,8 @@ export class Overlay extends CompositeDisposable {
) {
super();
this.addDisposables(this._onDidChange);
this.setupOverlay();
// this.setupDrag(true,this._element);
this.setupResize('top');
@ -52,6 +62,18 @@ export class Overlay extends CompositeDisposable {
// this.renderWithinBoundaryConditions();
}
toJSON(): { top: number; left: number; height: number; width: number } {
const container = this.options.container.getBoundingClientRect();
const element = this._element.getBoundingClientRect();
return {
top: element.top - container.top,
left: element.left - container.left,
width: element.width,
height: element.height,
};
}
private setupResize(
direction:
| 'top'
@ -216,6 +238,7 @@ export class Overlay extends CompositeDisposable {
}),
addDisposableWindowListener(window, 'mouseup', () => {
move.dispose();
this._onDidChange.fire();
})
);
})
@ -296,24 +319,37 @@ export class Overlay extends CompositeDisposable {
);
move.dispose();
this._onDidChange.fire();
})
);
};
this.addDisposables(
move,
addDisposableListener(dragTarget, 'mousedown', (_) => {
if (_.defaultPrevented) {
addDisposableListener(dragTarget, 'mousedown', (event) => {
if (
// event.shiftKey ||
event.defaultPrevented
) {
event.preventDefault();
return;
}
track();
}),
addDisposableListener(this.options.content, 'mousedown', (_) => {
if (_.shiftKey) {
addDisposableListener(
this.options.content,
'mousedown',
(event) => {
if (event.defaultPrevented) {
return;
}
if (event.shiftKey) {
track();
}
}),
}
),
addDisposableListener(
this.options.content,
'mousedown',
@ -324,29 +360,32 @@ export class Overlay extends CompositeDisposable {
)
);
bringElementToFront(this._element);
if (connect) {
track();
}
}
renderWithinBoundaryConditions(): void {
const rect = this.options.container.getBoundingClientRect();
const rect2 = this._element.getBoundingClientRect();
const containerRect = this.options.container.getBoundingClientRect();
const overlayRect = this._element.getBoundingClientRect();
const xOffset = Math.max(0, overlayRect.width - this.options.minX);
const yOffset = Math.max(0, overlayRect.height - this.options.minY);
const left = clamp(
Math.max(this.options.left, 0),
0,
Math.max(0, rect.width - rect2.width)
this.options.left,
-xOffset,
Math.max(0, containerRect.width - overlayRect.width + xOffset)
);
const top = clamp(
Math.max(this.options.top, 0),
0,
Math.max(0, rect.height - rect2.height)
this.options.top,
-yOffset,
Math.max(0, containerRect.height - overlayRect.height + yOffset)
);
console.log(new Error().stack);
this._element.style.left = `${left}px`;
this._element.style.top = `${top}px`;
}

View File

@ -6,12 +6,11 @@ import {
PanelTransfer,
} from '../../../dnd/dataTransfer';
import { toggleClass } from '../../../dom';
import { IDockviewComponent } from '../../dockviewComponent';
import { DockviewComponent } from '../../dockviewComponent';
import { DockviewDropTargets, ITabRenderer } from '../../types';
import { DockviewGroupPanel } from '../../dockviewGroupPanel';
import { DroptargetEvent, Droptarget } from '../../../dnd/droptarget';
import { DragHandler } from '../../../dnd/abstractDragHandler';
import { DockviewPanel } from '../../dockviewPanel';
export interface ITab extends IDisposable {
readonly panelId: string;
@ -39,7 +38,7 @@ export class Tab extends CompositeDisposable implements ITab {
constructor(
public readonly panelId: string,
private readonly accessor: IDockviewComponent,
private readonly accessor: DockviewComponent,
private readonly group: DockviewGroupPanel
) {
super();
@ -77,22 +76,10 @@ export class Tab extends CompositeDisposable implements ITab {
this.addDisposables(
addDisposableListener(this._element, 'mousedown', (event) => {
if (event.shiftKey) {
event.preventDefault();
const panel = this.accessor.getGroupPanel(this.panelId);
const { top, left } = this.element.getBoundingClientRect();
this.accessor.addFloating(panel as DockviewPanel, {
x: left,
y: top,
});
}
if (event.defaultPrevented) {
return;
}
/**
* TODO: alternative to stopPropagation
*

View File

@ -9,7 +9,7 @@ import { DockviewComponent } from '../../dockviewComponent';
import { DockviewGroupPanel } from '../../dockviewGroupPanel';
import { VoidContainer } from './voidContainer';
import { toggleClass } from '../../../dom';
import { IDockviewPanel } from '../../dockviewPanel';
import { DockviewPanel, IDockviewPanel } from '../../dockviewPanel';
export interface TabDropIndexEvent {
readonly event: DragEvent;
@ -187,6 +187,26 @@ export class TabsContainer
index: this.tabs.length,
});
}),
addDisposableListener(
this.voidContainer.element,
'mousedown',
(event) => {
if (event.shiftKey && !this.group.model.isFloating) {
event.preventDefault();
const { top, left } =
this.element.getBoundingClientRect();
const { top: rootTop, left: rootLeft } =
this.accessor.element.getBoundingClientRect();
this.accessor.addFloatingGroup(this.group, {
x: left - rootLeft + 20,
y: top - rootTop + 20,
});
event.preventDefault();
}
}
),
addDisposableListener(this.tabContainer, 'mousedown', (event) => {
if (event.defaultPrevented) {
return;
@ -263,6 +283,23 @@ export class TabsContainer
const disposable = CompositeDisposable.from(
tabToAdd.onChanged((event) => {
if (event.shiftKey) {
event.preventDefault();
const panel = this.accessor.getGroupPanel(tabToAdd.panelId);
const { top, left } =
tabToAdd.element.getBoundingClientRect();
const { top: rootTop, left: rootLeft } =
this.accessor.element.getBoundingClientRect();
this.accessor.addFloatingGroup(panel as DockviewPanel, {
x: left - rootLeft,
y: top - rootTop,
});
return;
}
const alreadyFocused =
panel.id === this.group.model.activePanel?.id &&
this.group.model.isContentFocused;

View File

@ -69,16 +69,6 @@ export class VoidContainer extends CompositeDisposable {
this.addDisposables(
handler,
addDisposableListener(this._element, 'mousedown', (event) => {
if (event.shiftKey && !this.group.model.isFloating) {
event.preventDefault();
this.accessor.addFloating(this.group, {
x: event.clientX + 20,
y: event.clientY + 20,
});
}
}),
this.voidDropTarget.onDrop((event) => {
this._onDrop.fire(event);
}),

View File

@ -8,7 +8,7 @@
left: 0px;
height: 100%;
width: 100%;
z-index: 9999;
z-index: 9997;
}
}

View File

@ -52,6 +52,11 @@ export interface PanelReference {
remove: () => void;
}
export interface SerializedFloatingGroup {
data: GroupPanelViewState;
position: { height: number; width: number; left: number; top: number };
}
export interface SerializedDockview {
grid: {
root: SerializedGridObject<GroupPanelViewState>;
@ -59,8 +64,9 @@ export interface SerializedDockview {
width: number;
orientation: Orientation;
};
panels: { [key: string]: GroupviewPanelState };
panels: Record<string, GroupviewPanelState>;
activeGroup?: string;
floatingGroups?: SerializedFloatingGroup[];
}
export type DockviewComponentUpdateOptions = Pick<
@ -118,7 +124,7 @@ export interface IDockviewComponent extends IBaseGrid<DockviewGroupPanel> {
readonly onDidAddPanel: Event<IDockviewPanel>;
readonly onDidLayoutFromJSON: Event<void>;
readonly onDidActivePanelChange: Event<IDockviewPanel | undefined>;
addFloating(
addFloatingGroup(
item: DockviewPanel | DockviewGroupPanel,
coord?: { x: number; y: number }
): void;
@ -156,7 +162,7 @@ export class DockviewComponent
private readonly floatingGroups: {
instance: DockviewGroupPanel;
disposable: IDisposable;
render: () => void;
overlay: Overlay;
}[] = [];
get orientation(): Orientation {
@ -290,9 +296,10 @@ export class DockviewComponent
this.updateWatermark();
}
addFloating(
addFloatingGroup(
item: DockviewPanel | DockviewGroupPanel,
coord?: { x: number; y: number }
coord?: { x?: number; y?: number; height?: number; width?: number },
options?: { skipRemoveGroup: boolean; connect: boolean }
): void {
let group: DockviewGroupPanel;
@ -307,23 +314,30 @@ export class DockviewComponent
group.model.openPanel(item);
} else {
group = item;
const skip =
typeof options?.skipRemoveGroup === 'boolean' &&
options.skipRemoveGroup;
if (!skip) {
this.doRemoveGroup(item, { skipDispose: true });
}
}
group.model.isFloating = true;
const { left, top } = this.element.getBoundingClientRect();
const overlayLeft =
typeof coord?.x === 'number' ? Math.max(coord.x - left, 0) : 100;
typeof coord?.x === 'number' ? Math.max(coord.x, 0) : 100;
const overlayTop =
typeof coord?.y === 'number' ? Math.max(0, coord.y - top) : 100;
typeof coord?.y === 'number' ? Math.max(coord.y, 0) : 100;
const overlay = new Overlay({
container: this.gridview.element,
content: group.element,
height: 300,
width: 300,
height: coord?.height ?? 300,
width: coord?.width ?? 300,
left: overlayLeft,
top: overlayTop,
minX: 100,
@ -333,20 +347,28 @@ export class DockviewComponent
const el = group.element.querySelector('#dv-group-float-drag-handle');
if (el) {
overlay.setupDrag(true, el as HTMLElement);
overlay.setupDrag(
typeof options?.connect === 'boolean' ? options.connect : true,
el as HTMLElement
);
}
const instance = {
instance: group,
render: () => {
overlay.renderWithinBoundaryConditions();
},
disposable: new CompositeDisposable(overlay, {
overlay,
disposable: new CompositeDisposable(
overlay,
overlay.onDidChange(() => {
this._bufferOnDidLayoutChange.fire();
}),
{
dispose: () => {
group.model.isFloating = false;
remove(this.floatingGroups, instance);
},
}),
}
),
};
this.floatingGroups.push(instance);
@ -409,7 +431,7 @@ export class DockviewComponent
if (this.floatingGroups) {
for (const floating of this.floatingGroups) {
floating.render();
floating.overlay.renderWithinBoundaryConditions();
}
}
}
@ -485,11 +507,26 @@ export class DockviewComponent
return collection;
}, {} as { [key: string]: GroupviewPanelState });
const floats: SerializedFloatingGroup[] = this.floatingGroups.map(
(floatingGroup) => {
return {
data: floatingGroup.instance.toJSON() as GroupPanelViewState,
position: floatingGroup.overlay.toJSON(),
};
}
);
const result: SerializedDockview = {
grid: data,
panels,
activeGroup: this.activeGroup?.id,
};
if (floats.length > 0) {
result.floatingGroups = floats;
}
return result;
}
fromJSON(data: SerializedDockview): void {
@ -505,9 +542,8 @@ export class DockviewComponent
const width = this.width;
const height = this.height;
this.gridview.deserialize(grid, {
fromJSON: (node: ISerializedLeafNode<GroupPanelViewState>) => {
const { id, locked, hideHeader, views, activeView } = node.data;
const createGroupFromSerializedState = (data: GroupPanelViewState) => {
const { id, locked, hideHeader, views, activeView } = data;
const group = this.createGroup({
id,
@ -518,14 +554,10 @@ export class DockviewComponent
this._onDidAddGroup.fire(group);
for (const child of views) {
const panel = this._deserializer.fromJSON(
panels[child],
group
);
const panel = this._deserializer.fromJSON(panels[child], group);
const isActive =
typeof activeView === 'string' &&
activeView === panel.id;
typeof activeView === 'string' && activeView === panel.id;
group.model.openPanel(panel, {
skipSetPanelActive: !isActive,
@ -534,20 +566,46 @@ export class DockviewComponent
}
if (!group.activePanel && group.panels.length > 0) {
group.model.openPanel(
group.panels[group.panels.length - 1],
{
group.model.openPanel(group.panels[group.panels.length - 1], {
skipSetGroupActive: true,
}
);
});
}
return group;
};
this.gridview.deserialize(grid, {
fromJSON: (node: ISerializedLeafNode<GroupPanelViewState>) => {
return createGroupFromSerializedState(node.data);
},
});
this.layout(width, height);
const serializedFloatingGroups = data.floatingGroups || [];
for (const serializedFloatingGroup of serializedFloatingGroups) {
const { data, position } = serializedFloatingGroup;
const group = createGroupFromSerializedState(data);
const { left, top } = this.element.getBoundingClientRect();
this.addFloatingGroup(
group,
{
x: position.left,
y: position.top,
height: position.height,
width: position.width,
},
{ skipRemoveGroup: true, connect: false }
);
}
for (const floatingGroup of this.floatingGroups) {
floatingGroup.overlay.renderWithinBoundaryConditions();
}
if (typeof activeGroup === 'string') {
const panel = this.getPanel(activeGroup);
if (panel) {
@ -595,6 +653,12 @@ export class DockviewComponent
let referenceGroup: DockviewGroupPanel | undefined;
if (options.position && options.floating) {
throw new Error(
'you can only provide one of: position, floating as arguments to .addPanel(...)'
);
}
if (options.position) {
if (isPanelOptionsWithPanel(options.position)) {
const referencePanel =
@ -639,7 +703,23 @@ export class DockviewComponent
const target = toTarget(
<Direction>options.position?.direction || 'within'
);
if (target === 'center') {
if (options.floating) {
const group = this.createGroup();
panel = this.createPanel(options, group);
group.model.openPanel(panel);
const o =
typeof options.floating === 'object' &&
options.floating !== null
? options.floating
: {};
this.addFloatingGroup(group, o, {
connect: false,
skipRemoveGroup: true,
});
} else if (referenceGroup.model.isFloating || target === 'center') {
panel = this.createPanel(options, referenceGroup);
referenceGroup.model.openPanel(panel);
} else {
@ -653,10 +733,26 @@ export class DockviewComponent
panel = this.createPanel(options, group);
group.model.openPanel(panel);
}
} else if (options.floating) {
const group = this.createGroup();
panel = this.createPanel(options, group);
group.model.openPanel(panel);
const o =
typeof options.floating === 'object' &&
options.floating !== null
? options.floating
: {};
this.addFloatingGroup(group, o, {
connect: false,
skipRemoveGroup: true,
});
} else {
const group = this.createGroupAtLocation();
panel = this.createPanel(options, group);
group.model.openPanel(panel);
}
@ -704,7 +800,7 @@ export class DockviewComponent
}
private updateWatermark(): void {
if (this.groups.length === 0) {
if (this.groups.filter((x) => !x.model.isFloating).length === 0) {
if (!this.watermark) {
this.watermark = this.createWatermarkComponent();
@ -823,8 +919,10 @@ export class DockviewComponent
if (floatingGroup) {
if (!options?.skipDispose) {
floatingGroup.instance.dispose();
this._groups.delete(group.id);
}
floatingGroup.disposable.dispose();
return floatingGroup.instance;
}

View File

@ -4,6 +4,7 @@ import { GridviewPanelApi } from '../api/gridviewPanelApi';
import {
DockviewGroupPanelModel,
GroupOptions,
GroupPanelViewState,
IDockviewGroupPanelModel,
IHeader,
} from './dockviewGroupPanelModel';
@ -94,7 +95,6 @@ export class DockviewGroupPanel
}
toJSON(): any {
// TODO fix typing
return this.model.toJSON();
}
}

View File

@ -261,6 +261,10 @@ export class DockviewGroupPanelModel
return false;
}
if (event.shiftKey && !this.isFloating) {
return false;
}
const data = getPanelData();
if (data && data.viewId === this.accessor.id) {

View File

@ -18,6 +18,7 @@ import { IDisposable } from '../lifecycle';
import { Position } from '../dnd/droptarget';
import { IDockviewPanel } from './dockviewPanel';
import { FrameworkFactory } from '../panel/componentFactory';
import { Optional } from '../types';
export interface IHeaderActionsRenderer extends IDisposable {
readonly element: HTMLElement;
@ -134,12 +135,32 @@ export function isPanelOptionsWithGroup(
return false;
}
export interface AddPanelOptions
extends Omit<PanelOptions, 'component' | 'tabComponent'> {
type AddPanelFloatingGroupUnion = {
floating:
| {
height?: number;
width?: number;
x?: number;
y?: number;
}
| true;
position: never;
};
type AddPanelPositionUnion = {
floating: false | never;
position: AddPanelPositionOptions;
};
type AddPanelOptionsUnion = AddPanelFloatingGroupUnion | AddPanelPositionUnion;
export type AddPanelOptions = Omit<
PanelOptions,
'component' | 'tabComponent'
> & {
component: string;
tabComponent?: string;
position?: AddPanelPositionOptions;
}
} & Partial<AddPanelOptionsUnion>;
type AddGroupOptionsWithPanel = {
referencePanel: string | IDockviewPanel;

View File

@ -28,6 +28,7 @@ import DockviewExternalDnd from '@site/sandboxes/externaldnd-dockview/src/app';
import DockviewResizeContainer from '@site/sandboxes/resizecontainer-dockview/src/app';
import DockviewTabheight from '@site/sandboxes/tabheight-dockview/src/app';
import DockviewWithIFrames from '@site/sandboxes/iframe-dockview/src/app';
import DockviewFloating from '@site/sandboxes/floatinggroup-dockview/src/app';
import { attach as attachDockviewVanilla } from '@site/sandboxes/javascript/vanilla-dockview/src/app';
import { attach as attachSimpleDockview } from '@site/sandboxes/javascript/simple-dockview/src/app';
@ -361,6 +362,15 @@ any drag and drop logic for other controls.
<DockviewExternalDnd />
</Container>
## Floating Groups
Dockview has built-in support for floating groups. Each floating container can contain a single group with many panels
and you can have as many floating containers as needed. You cannot dock multiple groups together in the same floating container.
<Container height={600} sandboxId="floatinggroup-dockview">
<DockviewFloating />
</Container>
## Panels
### Add Panel

View File

@ -39,13 +39,15 @@ const config = {
'docusaurus-plugin-sass',
(context, options) => {
return {
name: 'webpack',
name: 'custom-webpack',
configureWebpack: (config, isServer, utils) => {
return {
// externals: ['react', 'react-dom'],
devtool: 'source-map',
resolve: {
...config.resolve,
alias: {
...config.resolve.alias,
react: path.join(
__dirname,
'../../node_modules',
@ -57,9 +59,6 @@ const config = {
'react-dom'
),
},
fallback: {
timers: false,
},
},
};
},

View File

@ -0,0 +1,32 @@
{
"name": "floatinggroup-dockview",
"description": "",
"keywords": [
"dockview"
],
"version": "1.0.0",
"main": "src/index.tsx",
"dependencies": {
"dockview": "*",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11",
"typescript": "^4.9.5",
"react-scripts": "*"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject"
},
"browserslist": [
">0.2%",
"not dead",
"not ie <= 11",
"not op_mini all"
]
}

View File

@ -0,0 +1,44 @@
<!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,217 @@
import {
DockviewApi,
DockviewReact,
DockviewReadyEvent,
IDockviewPanelProps,
SerializedDockview,
} from 'dockview';
import * as React from 'react';
const components = {
default: (props: IDockviewPanelProps<{ title: string }>) => {
return (
<div
style={{
height: '100%',
padding: '20px',
background: 'var(--dv-group-view-background-color)',
}}
>
{props.params.title}
</div>
);
},
};
const counter = (() => {
let i = 0;
return {
next: () => ++i,
};
})();
function loadDefaultLayout(api: DockviewApi) {
api.addPanel({
id: 'panel_1',
component: 'default',
});
api.addPanel({
id: 'panel_2',
component: 'default',
});
api.addPanel({
id: 'panel_3',
component: 'default',
});
const panel4 = api.addPanel({
id: 'panel_4',
component: 'default',
floating: true,
});
api.addPanel({
id: 'panel_5',
component: 'default',
floating: false,
position: { referencePanel: panel4 },
});
api.addPanel({
id: 'panel_6',
component: 'default',
});
}
let panelCount = 0;
function addFloatingPanel(api: DockviewApi) {
api.addPanel({
id: (++panelCount).toString(),
title: `Tab ${panelCount}`,
component: 'default',
floating: true,
});
}
function addFloatingPanel2(api: DockviewApi) {
api.addPanel({
id: (++panelCount).toString(),
title: `Tab ${panelCount}`,
component: 'default',
floating: { width: 250, height: 150, x: 50, y: 50 },
});
}
function safeParse<T>(value: any): T | null {
try {
return JSON.parse(value) as T;
} catch (err) {
return null;
}
}
const useLocalStorage = <T,>(
key: string
): [T | null, (setter: T | null) => void] => {
const [state, setState] = React.useState<T | null>(
safeParse(localStorage.getItem(key))
);
React.useEffect(() => {
const _state = localStorage.getItem('key');
try {
if (_state !== null) {
setState(JSON.parse(_state));
}
} catch (err) {
//
}
}, [key]);
return [
state,
(_state: T | null) => {
if (_state === null) {
localStorage.removeItem(key);
} else {
localStorage.setItem(key, JSON.stringify(_state));
setState(_state);
}
},
];
};
export const DockviewPersistance = () => {
const [api, setApi] = React.useState<DockviewApi>();
const [layout, setLayout] =
useLocalStorage<SerializedDockview>('floating.layout');
const load = (api: DockviewApi) => {
api.clear();
if (layout) {
try {
api.fromJSON(layout);
} catch (err) {
console.error(err);
api.clear();
loadDefaultLayout(api);
}
} else {
loadDefaultLayout(api);
}
};
const onReady = (event: DockviewReadyEvent) => {
load(event.api);
setApi(event.api);
};
return (
<div
style={{
display: 'flex',
flexDirection: 'column',
height: '100%',
}}
>
<div style={{ height: '25px' }}>
<button
onClick={() => {
if (api) {
setLayout(api.toJSON());
}
}}
>
Save
</button>
<button
onClick={() => {
if (api) {
load(api);
}
}}
>
Load
</button>
<button
onClick={() => {
api!.clear();
setLayout(null);
}}
>
Clear
</button>
<button
onClick={() => {
addFloatingPanel2(api!);
}}
>
Add Layout
</button>
</div>
<div
style={{
flexGrow: 1,
overflow: 'hidden',
}}
>
<DockviewReact
onReady={onReady}
components={components}
watermarkComponent={Watermark}
className="dockview-theme-abyss"
/>
</div>
</div>
);
};
export default DockviewPersistance;
const Watermark = () => {
return <div style={{ color: 'white', padding: '8px' }}>watermark</div>;
};

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

@ -71,7 +71,7 @@ export const DockviewPersistance = () => {
event.api.fromJSON(layout);
success = true;
} catch (err) {
//
console.error(err);
}
}

View File

@ -251,8 +251,6 @@ export const EventsGridview = () => {
},
});
console.log('sdf');
api.addPanel({
id: 'panel_4',
component: 'default',