mirror of
https://github.com/mathuo/dockview
synced 2025-02-02 14:35:46 +00:00
feat: floating group persistance
This commit is contained in:
parent
c53d2690c3
commit
86be252e99
@ -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);
|
||||
});
|
||||
});
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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';
|
||||
|
@ -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;
|
||||
|
@ -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`;
|
||||
}
|
||||
|
@ -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
|
||||
*
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}),
|
||||
|
@ -8,7 +8,7 @@
|
||||
left: 0px;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
z-index: 9999;
|
||||
z-index: 9997;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
|
32
packages/docs/sandboxes/floatinggroup-dockview/package.json
Normal file
32
packages/docs/sandboxes/floatinggroup-dockview/package.json
Normal 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"
|
||||
]
|
||||
}
|
@ -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>
|
217
packages/docs/sandboxes/floatinggroup-dockview/src/app.tsx
Normal file
217
packages/docs/sandboxes/floatinggroup-dockview/src/app.tsx
Normal 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>;
|
||||
};
|
20
packages/docs/sandboxes/floatinggroup-dockview/src/index.tsx
Normal file
20
packages/docs/sandboxes/floatinggroup-dockview/src/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
body {
|
||||
margin: 0px;
|
||||
color: white;
|
||||
font-family: sans-serif;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#root {
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
}
|
||||
|
||||
.app {
|
||||
height: 100%;
|
||||
|
||||
}
|
18
packages/docs/sandboxes/floatinggroup-dockview/tsconfig.json
Normal file
18
packages/docs/sandboxes/floatinggroup-dockview/tsconfig.json
Normal 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
|
||||
}
|
||||
}
|
@ -71,7 +71,7 @@ export const DockviewPersistance = () => {
|
||||
event.api.fromJSON(layout);
|
||||
success = true;
|
||||
} catch (err) {
|
||||
//
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -251,8 +251,6 @@ export const EventsGridview = () => {
|
||||
},
|
||||
});
|
||||
|
||||
console.log('sdf');
|
||||
|
||||
api.addPanel({
|
||||
id: 'panel_4',
|
||||
component: 'default',
|
||||
|
Loading…
Reference in New Issue
Block a user