feat: experimental work

This commit is contained in:
mathuo 2024-11-30 19:52:17 +00:00
parent 25489bf48e
commit 97e8a95c2b
No known key found for this signature in database
GPG Key ID: C6EEDEFD6CA07281
7 changed files with 439 additions and 68 deletions

View File

@ -679,6 +679,239 @@ describe('dockviewComponent', () => {
expect(viewQuery.length).toBe(1);
});
describe('serialization', () => {
test('reuseExistingPanels true', () => {
const parts: PanelContentPartTest[] = [];
dockview = new DockviewComponent(container, {
createComponent(options) {
switch (options.name) {
case 'default':
const part = new PanelContentPartTest(
options.id,
options.name
);
parts.push(part);
return part;
default:
throw new Error(`unsupported`);
}
},
});
dockview.layout(1000, 1000);
dockview.addPanel({ id: 'panel1', component: 'default' });
dockview.addPanel({ id: 'panel2', component: 'default' });
dockview.addPanel({ id: 'panel7', component: 'default' });
expect(parts.length).toBe(3);
expect(parts.map((part) => part.isDisposed)).toEqual([
false,
false,
false,
]);
dockview.fromJSON(
{
activeGroup: 'group-1',
grid: {
root: {
type: 'branch',
data: [
{
type: 'leaf',
data: {
views: ['panel1'],
id: 'group-1',
activeView: 'panel1',
},
size: 500,
},
{
type: 'branch',
data: [
{
type: 'leaf',
data: {
views: ['panel2', 'panel3'],
id: 'group-2',
},
size: 500,
},
{
type: 'leaf',
data: {
views: ['panel4'],
id: 'group-3',
},
size: 500,
},
],
size: 500,
},
],
size: 1000,
},
height: 1000,
width: 1000,
orientation: Orientation.VERTICAL,
},
panels: {
panel1: {
id: 'panel1',
contentComponent: 'default',
tabComponent: 'tab-default',
title: 'panel1',
},
panel2: {
id: 'panel2',
contentComponent: 'default',
title: 'panel2',
},
panel3: {
id: 'panel3',
contentComponent: 'default',
title: 'panel3',
renderer: 'onlyWhenVisible',
},
panel4: {
id: 'panel4',
contentComponent: 'default',
title: 'panel4',
renderer: 'always',
},
},
},
{ reuseExistingPanels: true }
);
expect(parts.map((part) => part.isDisposed)).toEqual([
false,
false,
true,
false,
false,
]);
});
test('reuseExistingPanels false', () => {
const parts: PanelContentPartTest[] = [];
dockview = new DockviewComponent(container, {
createComponent(options) {
switch (options.name) {
case 'default':
const part = new PanelContentPartTest(
options.id,
options.name
);
parts.push(part);
return part;
default:
throw new Error(`unsupported`);
}
},
});
dockview.layout(1000, 1000);
dockview.addPanel({ id: 'panel1', component: 'default' });
dockview.addPanel({ id: 'panel2', component: 'default' });
dockview.addPanel({ id: 'panel7', component: 'default' });
expect(parts.length).toBe(3);
expect(parts.map((part) => part.isDisposed)).toEqual([
false,
false,
false,
]);
dockview.fromJSON({
activeGroup: 'group-1',
grid: {
root: {
type: 'branch',
data: [
{
type: 'leaf',
data: {
views: ['panel1'],
id: 'group-1',
activeView: 'panel1',
},
size: 500,
},
{
type: 'branch',
data: [
{
type: 'leaf',
data: {
views: ['panel2', 'panel3'],
id: 'group-2',
},
size: 500,
},
{
type: 'leaf',
data: {
views: ['panel4'],
id: 'group-3',
},
size: 500,
},
],
size: 500,
},
],
size: 1000,
},
height: 1000,
width: 1000,
orientation: Orientation.VERTICAL,
},
panels: {
panel1: {
id: 'panel1',
contentComponent: 'default',
tabComponent: 'tab-default',
title: 'panel1',
},
panel2: {
id: 'panel2',
contentComponent: 'default',
title: 'panel2',
},
panel3: {
id: 'panel3',
contentComponent: 'default',
title: 'panel3',
renderer: 'onlyWhenVisible',
},
panel4: {
id: 'panel4',
contentComponent: 'default',
title: 'panel4',
renderer: 'always',
},
},
});
expect(parts.map((part) => part.isDisposed)).toEqual([
true,
true,
true,
false,
false,
false,
false,
]);
});
});
test('serialization', () => {
dockview.layout(1000, 1000);

View File

@ -209,6 +209,10 @@ export class TestPanel implements IDockviewPanel {
});
}
updateFromStateModel(state: GroupviewPanelState): void {
//
}
init(params: IGroupPanelInitParameters) {
this._params = params;
}

View File

@ -854,8 +854,11 @@ export class DockviewApi implements CommonApi<SerializedDockview> {
/**
* Create a component from a serialized object.
*/
fromJSON(data: SerializedDockview): void {
this.component.fromJSON(data);
fromJSON(
data: SerializedDockview,
options?: { reuseExistingPanels: boolean }
): void {
this.component.fromJSON(data, options);
}
/**

View File

@ -147,6 +147,7 @@ type MoveGroupOrPanelOptions = {
position: Position;
index?: number;
};
keepEmptyGroups?: boolean;
};
export interface FloatingGroupOptions {
@ -219,6 +220,7 @@ export interface IDockviewComponent extends IBaseGrid<DockviewGroupPanel> {
onWillClose?: (event: { id: string; window: Window }) => void;
}
): Promise<boolean>;
fromJSON(data: any, options?: { reuseExistingPanels: boolean }): void;
}
export class DockviewComponent
@ -381,17 +383,17 @@ export class DockviewComponent
this.updateWatermark();
}),
this.onDidAdd((event) => {
if (!this._moving) {
if (!this._isEventSuppressionEnabled) {
this._onDidAddGroup.fire(event);
}
}),
this.onDidRemove((event) => {
if (!this._moving) {
if (!this._isEventSuppressionEnabled) {
this._onDidRemoveGroup.fire(event);
}
}),
this.onDidActiveChange((event) => {
if (!this._moving) {
if (!this._isEventSuppressionEnabled) {
this._onDidActiveGroupChange.fire(event);
}
}),
@ -675,13 +677,13 @@ export class DockviewComponent
if (!options?.overridePopoutGroup && isGroupAddedToDom) {
if (itemToPopout instanceof DockviewPanel) {
this.movingLock(() => {
this.runWithSuppressedEvents(() => {
const panel =
referenceGroup.model.removePanel(itemToPopout);
group.model.openPanel(panel);
});
} else {
this.movingLock(() =>
this.runWithSuppressedEvents(() =>
moveGroupWithoutDestroying({
from: referenceGroup,
to: group,
@ -773,7 +775,7 @@ export class DockviewComponent
isGroupAddedToDom &&
this.getPanel(referenceGroup.id)
) {
this.movingLock(() =>
this.runWithSuppressedEvents(() =>
moveGroupWithoutDestroying({
from: group,
to: referenceGroup,
@ -830,7 +832,7 @@ export class DockviewComponent
group = this.createGroup();
this._onDidAddGroup.fire(group);
this.movingLock(() =>
this.runWithSuppressedEvents(() =>
this.removePanel(item, {
removeEmptyGroup: true,
skipDispose: true,
@ -838,7 +840,7 @@ export class DockviewComponent
})
);
this.movingLock(() =>
this.runWithSuppressedEvents(() =>
group.model.openPanel(item, { skipSetGroupActive: true })
);
} else {
@ -857,7 +859,7 @@ export class DockviewComponent
if (!skip) {
if (popoutReferenceGroup) {
this.movingLock(() =>
this.runWithSuppressedEvents(() =>
moveGroupWithoutDestroying({
from: item,
to: popoutReferenceGroup,
@ -955,7 +957,7 @@ export class DockviewComponent
const el = group.element.querySelector('.dv-void-container');
if (!el) {
throw new Error('failed to find drag handle');
throw new Error('dockview: failed to find drag handle');
}
overlay.setupDrag(<HTMLElement>el, {
@ -1051,7 +1053,7 @@ export class DockviewComponent
case 'right':
return this.createGroupAtLocation([this.gridview.length]); // insert into last position
default:
throw new Error(`unsupported position ${position}`);
throw new Error(`dockview: unsupported position ${position}`);
}
}
@ -1219,17 +1221,62 @@ export class DockviewComponent
return result;
}
fromJSON(data: SerializedDockview): void {
fromJSON(
data: SerializedDockview,
options?: { reuseExistingPanels: boolean }
): void {
const existingPanels = new Map<string, IDockviewPanel>();
let tempGroup: DockviewGroupPanel | undefined;
if (options?.reuseExistingPanels) {
/**
* What are we doing here?
*
* 1. Create a temporary group to hold any panels that currently exist and that also exist in the new layout
* 2. Remove that temporary group from the group mapping so that it doesn't get cleared when we clear the layout
*/
tempGroup = this.createGroup();
this._groups.delete(tempGroup.api.id);
const newPanels = Object.keys(data.panels);
for (const panel of this.panels) {
if (newPanels.includes(panel.api.id)) {
existingPanels.set(panel.api.id, panel);
}
}
this.runWithSuppressedEvents(() => {
Array.from(existingPanels.values()).forEach((panel) => {
this.moveGroupOrPanel({
from: {
groupId: panel.api.group.api.id,
panelId: panel.api.id,
},
to: {
group: tempGroup!,
position: 'center',
},
keepEmptyGroups: true,
});
});
});
}
this.clear();
if (typeof data !== 'object' || data === null) {
throw new Error('serialized layout must be a non-null object');
throw new Error(
'dockview: serialized layout must be a non-null object'
);
}
const { grid, panels, activeGroup } = data;
if (grid.root.type !== 'branch' || !Array.isArray(grid.root.data)) {
throw new Error('root must be of type branch');
throw new Error('dockview: root must be of type branch');
}
try {
@ -1243,7 +1290,9 @@ export class DockviewComponent
const { id, locked, hideHeader, views, activeView } = data;
if (typeof id !== 'string') {
throw new Error('group id must be of type string');
throw new Error(
'dockview: group id must be of type string'
);
}
const group = this.createGroup({
@ -1260,11 +1309,23 @@ export class DockviewComponent
* In running this section first we avoid firing lots of 'add' events in the event of a failure
* due to a corruption of input data.
*/
const panel = this._deserializer.fromJSON(
panels[child],
group
);
createdPanels.push(panel);
const existingPanel = existingPanels.get(child);
if (tempGroup && existingPanel) {
this.runWithSuppressedEvents(() => {
tempGroup!.model.removePanel(existingPanel);
});
createdPanels.push(existingPanel);
existingPanel.updateFromStateModel(panels[child]);
} else {
const panel = this._deserializer.fromJSON(
panels[child],
group
);
createdPanels.push(panel);
}
}
this._onDidAddGroup.fire(group);
@ -1276,10 +1337,21 @@ export class DockviewComponent
typeof activeView === 'string' &&
activeView === panel.id;
group.model.openPanel(panel, {
skipSetActive: !isActive,
skipSetGroupActive: true,
});
const hasExisting = existingPanels.has(panel.api.id);
if (hasExisting) {
this.runWithSuppressedEvents(() => {
group.model.openPanel(panel, {
skipSetActive: !isActive,
skipSetGroupActive: true,
});
});
} else {
group.model.openPanel(panel, {
skipSetActive: !isActive,
skipSetGroupActive: true,
});
}
}
if (!group.activePanel && group.panels.length > 0) {
@ -1426,14 +1498,16 @@ export class DockviewComponent
options: AddPanelOptions<T>
): DockviewPanel {
if (this.panels.find((_) => _.id === options.id)) {
throw new Error(`panel with id ${options.id} already exists`);
throw new Error(
`dockview: panel with id ${options.id} already exists`
);
}
let referenceGroup: DockviewGroupPanel | undefined;
if (options.position && options.floating) {
throw new Error(
'you can only provide one of: position, floating as arguments to .addPanel(...)'
'dockview: you can only provide one of: position, floating as arguments to .addPanel(...)'
);
}
@ -1454,7 +1528,7 @@ export class DockviewComponent
if (!referencePanel) {
throw new Error(
`referencePanel '${options.position.referencePanel}' does not exist`
`dockview: referencePanel '${options.position.referencePanel}' does not exist`
);
}
@ -1469,7 +1543,7 @@ export class DockviewComponent
if (!referenceGroup) {
throw new Error(
`referenceGroup '${options.position.referenceGroup}' does not exist`
`dockview: referenceGroup '${options.position.referenceGroup}' does not exist`
);
}
} else {
@ -1634,7 +1708,7 @@ export class DockviewComponent
if (!group) {
throw new Error(
`cannot remove panel ${panel.id}. it's missing a group.`
`dockview: cannot remove panel ${panel.id}. it's missing a group.`
);
}
@ -1700,7 +1774,7 @@ export class DockviewComponent
if (!referencePanel) {
throw new Error(
`reference panel ${options.referencePanel} does not exist`
`dockview: reference panel ${options.referencePanel} does not exist`
);
}
@ -1708,7 +1782,7 @@ export class DockviewComponent
if (!referenceGroup) {
throw new Error(
`reference group for reference panel ${options.referencePanel} does not exist`
`dockview: reference group for reference panel ${options.referencePanel} does not exist`
);
}
} else if (isGroupOptionsWithGroup(options)) {
@ -1719,7 +1793,7 @@ export class DockviewComponent
if (!referenceGroup) {
throw new Error(
`reference group ${options.referenceGroup} does not exist`
`dockview: reference group ${options.referenceGroup} does not exist`
);
}
} else {
@ -1832,7 +1906,7 @@ export class DockviewComponent
return floatingGroup.group;
}
throw new Error('failed to find floating group');
throw new Error('dockview: failed to find floating group');
}
if (group.api.location.type === 'popout') {
@ -1878,7 +1952,7 @@ export class DockviewComponent
return selectedGroup.popoutGroup;
}
throw new Error('failed to find popout group');
throw new Error('dockview: failed to find popout group');
}
const re = super.doRemoveGroup(group, options);
@ -1892,16 +1966,21 @@ export class DockviewComponent
return re;
}
private _moving = false;
private _isEventSuppressionEnabled = false;
movingLock<T>(func: () => T): T {
const isMoving = this._moving;
/**
* Code that runs within the provided function will not cause any events to fire. This is useful if you want
* to move things around as an intermediate step without raises any associated events
*/
runWithSuppressedEvents<T>(func: () => T): T {
const isMoving = this._isEventSuppressionEnabled;
try {
this._moving = true;
this._isEventSuppressionEnabled = true;
return func();
} finally {
this._moving = isMoving;
// return to the original state which isn't necessarily false since calls may be nested
this._isEventSuppressionEnabled = isMoving;
}
}
@ -1917,7 +1996,9 @@ export class DockviewComponent
: undefined;
if (!sourceGroup) {
throw new Error(`Failed to find group id ${sourceGroupId}`);
throw new Error(
`dockview: Failed to find group id ${sourceGroupId}`
);
}
if (sourceItemId === undefined) {
@ -1940,24 +2021,24 @@ export class DockviewComponent
* Dropping a panel within another group
*/
const removedPanel: IDockviewPanel | undefined = this.movingLock(
() =>
const removedPanel: IDockviewPanel | undefined =
this.runWithSuppressedEvents(() =>
sourceGroup.model.removePanel(sourceItemId, {
skipSetActive: false,
skipSetActiveGroup: true,
})
);
);
if (!removedPanel) {
throw new Error(`No panel with id ${sourceItemId}`);
throw new Error(`dockview: No panel with id ${sourceItemId}`);
}
if (sourceGroup.model.size === 0) {
if (!options.keepEmptyGroups && sourceGroup.model.size === 0) {
// remove the group and do not set a new group as active
this.doRemoveGroup(sourceGroup, { skipActive: true });
}
this.movingLock(() =>
this.runWithSuppressedEvents(() =>
destinationGroup.model.openPanel(removedPanel, {
index: destinationIndex,
skipSetGroupActive: true,
@ -2028,7 +2109,7 @@ export class DockviewComponent
)!;
const removedPanel: IDockviewPanel | undefined =
this.movingLock(() =>
this.runWithSuppressedEvents(() =>
popoutGroup.popoutGroup.model.removePanel(
popoutGroup.popoutGroup.panels[0],
{
@ -2041,7 +2122,7 @@ export class DockviewComponent
this.doRemoveGroup(sourceGroup, { skipActive: true });
const newGroup = this.createGroupAtLocation(targetLocation);
this.movingLock(() =>
this.runWithSuppressedEvents(() =>
newGroup.model.openPanel(removedPanel, {
skipSetActive: true,
})
@ -2056,7 +2137,7 @@ export class DockviewComponent
}
// source group will become empty so delete the group
const targetGroup = this.movingLock(() =>
const targetGroup = this.runWithSuppressedEvents(() =>
this.doRemoveGroup(sourceGroup, {
skipActive: true,
skipDispose: true,
@ -2073,7 +2154,9 @@ export class DockviewComponent
updatedReferenceLocation,
destinationTarget
);
this.movingLock(() => this.doAddGroup(targetGroup, location));
this.runWithSuppressedEvents(() =>
this.doAddGroup(targetGroup, location)
);
this.doSetGroupAndPanelActive(targetGroup);
this._onDidMovePanel.fire({
@ -2086,7 +2169,7 @@ export class DockviewComponent
* create a new group, add the panels to that new group and add the new group in an appropiate position
*/
const removedPanel: IDockviewPanel | undefined =
this.movingLock(() =>
this.runWithSuppressedEvents(() =>
sourceGroup.model.removePanel(sourceItemId, {
skipSetActive: false,
skipSetActiveGroup: true,
@ -2094,7 +2177,9 @@ export class DockviewComponent
);
if (!removedPanel) {
throw new Error(`No panel with id ${sourceItemId}`);
throw new Error(
`dockview: No panel with id ${sourceItemId}`
);
}
const dropLocation = getRelativeLocation(
@ -2104,7 +2189,7 @@ export class DockviewComponent
);
const group = this.createGroupAtLocation(dropLocation);
this.movingLock(() =>
this.runWithSuppressedEvents(() =>
group.model.openPanel(removedPanel, {
skipSetGroupActive: true,
})
@ -2127,7 +2212,7 @@ export class DockviewComponent
if (target === 'center') {
const activePanel = from.activePanel;
const panels = this.movingLock(() =>
const panels = this.runWithSuppressedEvents(() =>
[...from.panels].map((p) =>
from.model.removePanel(p.id, {
skipSetActive: true,
@ -2139,7 +2224,7 @@ export class DockviewComponent
this.doRemoveGroup(from, { skipActive: true });
}
this.movingLock(() => {
this.runWithSuppressedEvents(() => {
for (const panel of panels) {
to.model.openPanel(panel, {
skipSetActive: panel !== activePanel,
@ -2159,7 +2244,9 @@ export class DockviewComponent
(x) => x.group === from
);
if (!selectedFloatingGroup) {
throw new Error('failed to find floating group');
throw new Error(
'dockview: failed to find floating group'
);
}
selectedFloatingGroup.dispose();
break;
@ -2169,7 +2256,9 @@ export class DockviewComponent
(x) => x.popoutGroup === from
);
if (!selectedPopoutGroup) {
throw new Error('failed to find popout group');
throw new Error(
'dockview: failed to find popout group'
);
}
selectedPopoutGroup.disposable.dispose();
}
@ -2213,7 +2302,7 @@ export class DockviewComponent
const activePanel = this.activePanel;
if (
!this._moving &&
!this._isEventSuppressionEnabled &&
activePanel !== this._onDidActivePanelChange.value
) {
this._onDidActivePanelChange.fire(activePanel);
@ -2234,7 +2323,7 @@ export class DockviewComponent
}
if (
!this._moving &&
!this._isEventSuppressionEnabled &&
activePanel !== this._onDidActivePanelChange.value
) {
this._onDidActivePanelChange.fire(activePanel);
@ -2311,19 +2400,19 @@ export class DockviewComponent
this._onUnhandledDragOverEvent.fire(event);
}),
view.model.onDidAddPanel((event) => {
if (this._moving) {
if (this._isEventSuppressionEnabled) {
return;
}
this._onDidAddPanel.fire(event.panel);
}),
view.model.onDidRemovePanel((event) => {
if (this._moving) {
if (this._isEventSuppressionEnabled) {
return;
}
this._onDidRemovePanel.fire(event.panel);
}),
view.model.onDidActivePanelChange((event) => {
if (this._moving) {
if (this._isEventSuppressionEnabled) {
return;
}
if (event.panel !== this.activePanel) {

View File

@ -27,6 +27,7 @@ export interface IDockviewPanel extends IDisposable, IPanel {
group: DockviewGroupPanel,
options?: { skipSetActive?: boolean }
): void;
updateFromStateModel(state: GroupviewPanelState): void;
init(params: IGroupPanelInitParameters): void;
toJSON(): GroupviewPanelState;
setTitle(title: string): void;
@ -45,10 +46,10 @@ export class DockviewPanel
private _title: string | undefined;
private _renderer: DockviewPanelRenderer | undefined;
private readonly _minimumWidth: number | undefined;
private readonly _minimumHeight: number | undefined;
private readonly _maximumWidth: number | undefined;
private readonly _maximumHeight: number | undefined;
private _minimumWidth: number | undefined;
private _minimumHeight: number | undefined;
private _maximumWidth: number | undefined;
private _maximumHeight: number | undefined;
get params(): Parameters | undefined {
return this._params;
@ -209,6 +210,20 @@ export class DockviewPanel
});
}
updateFromStateModel(state: GroupviewPanelState): void {
this._maximumHeight = state.maximumHeight;
this._minimumHeight = state.minimumHeight;
this._maximumWidth = state.maximumWidth;
this._minimumWidth = state.minimumWidth;
this.update({ params: state.params ?? {} });
this.setTitle(state.title ?? this.id);
this.setRenderer(state.renderer ?? this.accessor.renderer);
// state.contentComponent;
// state.tabComponent;
}
public updateParentGroup(
group: DockviewGroupPanel,
options?: { skipSetActive?: boolean }

View File

@ -35,6 +35,12 @@ const components = {
const isDebug = React.useContext(DebugContext);
const metadata = usePanelApiMetadata(props.api);
const [firstRender, setFirstRender] = React.useState<string>('');
React.useEffect(() => {
setFirstRender(new Date().toISOString());
}, []);
return (
<div
style={{
@ -59,6 +65,8 @@ const components = {
{props.api.title}
</span>
<div>{firstRender}</div>
{isDebug && (
<div style={{ fontSize: '0.8em' }}>
<Option

View File

@ -110,6 +110,22 @@ export const GridActions = (props: {
}
};
const onLoad2 = () => {
const state = localStorage.getItem('dv-demo-state');
if (state) {
try {
props.api?.fromJSON(JSON.parse(state), {
keepExistingPanels: true,
});
setGap(props.api?.gap ?? 0);
} catch (err) {
console.error('failed to load state', err);
localStorage.removeItem('dv-demo-state');
}
}
};
const onSave = () => {
if (props.api) {
console.log(props.api.toJSON());
@ -192,6 +208,9 @@ export const GridActions = (props: {
<button className="text-button" onClick={onLoad}>
Load
</button>
<button className="text-button" onClick={onLoad2}>
Load2
</button>
<button className="text-button" onClick={onSave}>
Save
</button>