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 { Orientation } from '../../splitview/splitview';
import { CompositeDisposable } from '../../lifecycle'; import { CompositeDisposable } from '../../lifecycle';
import { Emitter } from '../../events'; import { Emitter } from '../../events';
import { IDockviewPanel } from '../../dockview/dockviewPanel'; import { DockviewPanel, IDockviewPanel } from '../../dockview/dockviewPanel';
import { DockviewGroupPanel } from '../../dockview/dockviewGroupPanel'; import { DockviewGroupPanel } from '../../dockview/dockviewGroupPanel';
class PanelContentPartTest implements IContentRenderer { 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

@ -35,10 +35,10 @@ export abstract class DragHandler extends CompositeDisposable {
this.addDisposables( this.addDisposables(
this._onDragStart, this._onDragStart,
addDisposableListener(this.el, 'dragstart', (event) => { addDisposableListener(this.el, 'dragstart', (event) => {
if (this.isCancelled(event)) { if (this.isCancelled(event)) {
event.preventDefault(); event.preventDefault();
return; return;
} }
const iframes = [ const iframes = [
...getElementsByTagName('iframe'), ...getElementsByTagName('iframe'),

View File

@ -8,6 +8,7 @@
height: 100%; height: 100%;
width: 100%; width: 100%;
z-index: 10000; z-index: 10000;
pointer-events: none;
> .drop-target-selection { > .drop-target-selection {
position: relative; position: relative;
@ -15,7 +16,9 @@
height: 100%; height: 100%;
width: 100%; width: 100%;
background-color: var(--dv-drag-over-background-color); 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; will-change: transform;
pointer-events: none; pointer-events: none;

View File

@ -54,6 +54,17 @@ export type CanDisplayOverlay =
| boolean | boolean
| ((dragEvent: DragEvent, state: Position) => 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 { export class Droptarget extends CompositeDisposable {
private targetElement: HTMLElement | undefined; private targetElement: HTMLElement | undefined;
private overlayElement: HTMLElement | undefined; private overlayElement: HTMLElement | undefined;
@ -114,7 +125,7 @@ export class Droptarget extends CompositeDisposable {
height height
); );
if (quadrant === null) { if (isEventMarked(e) || quadrant === null) {
// no drop target should be displayed // no drop target should be displayed
this.removeDropTarget(); this.removeDropTarget();
return; return;
@ -128,6 +139,8 @@ export class Droptarget extends CompositeDisposable {
return; return;
} }
markEvent(e);
if (!this.targetElement) { if (!this.targetElement) {
this.targetElement = document.createElement('div'); this.targetElement = document.createElement('div');
this.targetElement.className = 'drop-target-dropzone'; this.targetElement.className = 'drop-target-dropzone';

View File

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

View File

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

View File

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

View File

@ -9,7 +9,7 @@ import { DockviewComponent } from '../../dockviewComponent';
import { DockviewGroupPanel } from '../../dockviewGroupPanel'; import { DockviewGroupPanel } from '../../dockviewGroupPanel';
import { VoidContainer } from './voidContainer'; import { VoidContainer } from './voidContainer';
import { toggleClass } from '../../../dom'; import { toggleClass } from '../../../dom';
import { IDockviewPanel } from '../../dockviewPanel'; import { DockviewPanel, IDockviewPanel } from '../../dockviewPanel';
export interface TabDropIndexEvent { export interface TabDropIndexEvent {
readonly event: DragEvent; readonly event: DragEvent;
@ -187,6 +187,26 @@ export class TabsContainer
index: this.tabs.length, 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) => { addDisposableListener(this.tabContainer, 'mousedown', (event) => {
if (event.defaultPrevented) { if (event.defaultPrevented) {
return; return;
@ -263,6 +283,23 @@ export class TabsContainer
const disposable = CompositeDisposable.from( const disposable = CompositeDisposable.from(
tabToAdd.onChanged((event) => { 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 = const alreadyFocused =
panel.id === this.group.model.activePanel?.id && panel.id === this.group.model.activePanel?.id &&
this.group.model.isContentFocused; this.group.model.isContentFocused;

View File

@ -69,16 +69,6 @@ export class VoidContainer extends CompositeDisposable {
this.addDisposables( this.addDisposables(
handler, 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.voidDropTarget.onDrop((event) => {
this._onDrop.fire(event); this._onDrop.fire(event);
}), }),

View File

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

View File

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

View File

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

View File

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

View File

@ -18,6 +18,7 @@ import { IDisposable } from '../lifecycle';
import { Position } from '../dnd/droptarget'; import { Position } from '../dnd/droptarget';
import { IDockviewPanel } from './dockviewPanel'; import { IDockviewPanel } from './dockviewPanel';
import { FrameworkFactory } from '../panel/componentFactory'; import { FrameworkFactory } from '../panel/componentFactory';
import { Optional } from '../types';
export interface IHeaderActionsRenderer extends IDisposable { export interface IHeaderActionsRenderer extends IDisposable {
readonly element: HTMLElement; readonly element: HTMLElement;
@ -134,12 +135,32 @@ export function isPanelOptionsWithGroup(
return false; return false;
} }
export interface AddPanelOptions type AddPanelFloatingGroupUnion = {
extends Omit<PanelOptions, 'component' | 'tabComponent'> { 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; component: string;
tabComponent?: string; tabComponent?: string;
position?: AddPanelPositionOptions; } & Partial<AddPanelOptionsUnion>;
}
type AddGroupOptionsWithPanel = { type AddGroupOptionsWithPanel = {
referencePanel: string | IDockviewPanel; 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 DockviewResizeContainer from '@site/sandboxes/resizecontainer-dockview/src/app';
import DockviewTabheight from '@site/sandboxes/tabheight-dockview/src/app'; import DockviewTabheight from '@site/sandboxes/tabheight-dockview/src/app';
import DockviewWithIFrames from '@site/sandboxes/iframe-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 attachDockviewVanilla } from '@site/sandboxes/javascript/vanilla-dockview/src/app';
import { attach as attachSimpleDockview } from '@site/sandboxes/javascript/simple-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 /> <DockviewExternalDnd />
</Container> </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 ## Panels
### Add Panel ### Add Panel

View File

@ -39,13 +39,15 @@ const config = {
'docusaurus-plugin-sass', 'docusaurus-plugin-sass',
(context, options) => { (context, options) => {
return { return {
name: 'webpack', name: 'custom-webpack',
configureWebpack: (config, isServer, utils) => { configureWebpack: (config, isServer, utils) => {
return { return {
// externals: ['react', 'react-dom'], // externals: ['react', 'react-dom'],
devtool: 'source-map', devtool: 'source-map',
resolve: { resolve: {
...config.resolve,
alias: { alias: {
...config.resolve.alias,
react: path.join( react: path.join(
__dirname, __dirname,
'../../node_modules', '../../node_modules',
@ -57,9 +59,6 @@ const config = {
'react-dom' '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); event.api.fromJSON(layout);
success = true; success = true;
} catch (err) { } catch (err) {
// console.error(err);
} }
} }

View File

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