Merge pull request #262 from mathuo/230-explore-floating-groups

230 explore floating groups
This commit is contained in:
mathuo 2023-07-10 21:02:30 +01:00 committed by GitHub
commit 2dbfda25e9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
61 changed files with 4291 additions and 220 deletions

View File

@ -5,6 +5,12 @@ import { DockviewGroupPanel } from '../../dockview/dockviewGroupPanel';
describe('groupPanelApi', () => {
test('title', () => {
const accessor: Partial<DockviewComponent> = {
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
options: {},
};
const panelMock = jest.fn<DockviewPanel, []>(() => {
return {
update: jest.fn(),
@ -18,7 +24,11 @@ describe('groupPanelApi', () => {
const panel = new panelMock();
const group = new groupMock();
const cut = new DockviewPanelApiImpl(panel, group);
const cut = new DockviewPanelApiImpl(
panel,
group,
<DockviewComponent>accessor
);
cut.setTitle('test_title');
expect(panel.setTitle).toBeCalledTimes(1);
@ -44,7 +54,8 @@ describe('groupPanelApi', () => {
const cut = new DockviewPanelApiImpl(
<IDockviewPanel>groupPanel,
<DockviewGroupPanel>groupViewPanel
<DockviewGroupPanel>groupViewPanel,
<DockviewComponent>accessor
);
cut.updateParameters({ keyA: 'valueA' });
@ -73,7 +84,8 @@ describe('groupPanelApi', () => {
const cut = new DockviewPanelApiImpl(
<IDockviewPanel>groupPanel,
<DockviewGroupPanel>groupViewPanel
<DockviewGroupPanel>groupViewPanel,
<DockviewComponent>accessor
);
let events = 0;

View File

@ -3,6 +3,7 @@ import {
last,
pushToEnd,
pushToStart,
remove,
sequenceEquals,
tail,
} from '../array';
@ -47,4 +48,22 @@ describe('array', () => {
expect(sequenceEquals([1, 2, 3, 4], [1, 2, 3])).toBeFalsy();
expect(sequenceEquals([1, 2, 3, 4], [1, 2, 3, 4, 5])).toBeFalsy();
});
test('remove', () => {
const arr1 = [1, 2, 3, 4];
remove(arr1, 2);
expect(arr1).toEqual([1, 3, 4]);
const arr2 = [1, 2, 2, 3, 4];
remove(arr2, 2);
expect(arr2).toEqual([1, 2, 3, 4]);
const arr3 = [1];
remove(arr3, 2);
expect(arr3).toEqual([1]);
remove(arr3, 1);
expect(arr3).toEqual([]);
remove(arr3, 1);
expect(arr3).toEqual([]);
});
});

View File

@ -118,4 +118,62 @@ describe('abstractDragHandler', () => {
expect(webview.style.pointerEvents).toBe('auto');
expect(span.style.pointerEvents).toBeFalsy();
});
test('that .preventDefault() is called for cancelled events', () => {
const element = document.createElement('div');
const handler = new (class TestClass extends DragHandler {
constructor(el: HTMLElement) {
super(el);
}
protected isCancelled(_event: DragEvent): boolean {
return true;
}
getData(): IDisposable {
return {
dispose: () => {
// /
},
};
}
})(element);
const event = new Event('dragstart');
const spy = jest.spyOn(event, 'preventDefault');
fireEvent(element, event);
expect(spy).toBeCalledTimes(1);
handler.dispose();
});
test('that .preventDefault() is not called for non-cancelled events', () => {
const element = document.createElement('div');
const handler = new (class TestClass extends DragHandler {
constructor(el: HTMLElement) {
super(el);
}
protected isCancelled(_event: DragEvent): boolean {
return false;
}
getData(): IDisposable {
return {
dispose: () => {
// /
},
};
}
})(element);
const event = new Event('dragstart');
const spy = jest.spyOn(event, 'preventDefault');
fireEvent(element, event);
expect(spy).toBeCalledTimes(0);
handler.dispose();
});
});

View File

@ -34,6 +34,48 @@ describe('droptarget', () => {
jest.spyOn(element, 'clientWidth', 'get').mockImplementation(() => 200);
});
test('that dragover events are marked', () => {
droptarget = new Droptarget(element, {
canDisplayOverlay: () => true,
acceptedTargetZones: ['center'],
});
fireEvent.dragEnter(element);
const event = new Event('dragover');
fireEvent(element, event);
expect(
(event as any)['__dockview_droptarget_event_is_used__']
).toBeTruthy();
});
test('that the drop target is removed when receiving a marked dragover event', () => {
let position: Position | undefined = undefined;
droptarget = new Droptarget(element, {
canDisplayOverlay: () => true,
acceptedTargetZones: ['center'],
});
droptarget.onDrop((event) => {
position = event.position;
});
fireEvent.dragEnter(element);
fireEvent.dragOver(element);
const target = element.querySelector(
'.drop-target-dropzone'
) as HTMLElement;
fireEvent.drop(target);
expect(position).toBe('center');
const event = new Event('dragover');
(event as any)['__dockview_droptarget_event_is_used__'] = true;
fireEvent(element, event);
expect(element.querySelector('.drop-target-dropzone')).toBeNull();
});
test('directionToPosition', () => {
expect(directionToPosition('above')).toBe('top');
expect(directionToPosition('below')).toBe('bottom');

View File

@ -0,0 +1,101 @@
import { fireEvent } from '@testing-library/dom';
import { GroupDragHandler } from '../../dnd/groupDragHandler';
import { DockviewGroupPanel } from '../../dockview/dockviewGroupPanel';
import { LocalSelectionTransfer, PanelTransfer } from '../../dnd/dataTransfer';
describe('groupDragHandler', () => {
test('that the dnd transfer object is setup and torndown', () => {
const element = document.createElement('div');
const groupMock = jest.fn<DockviewGroupPanel, []>(() => {
const partial: Partial<DockviewGroupPanel> = {
id: 'test_group_id',
api: { isFloating: false } as any,
};
return partial as DockviewGroupPanel;
});
const group = new groupMock();
const cut = new GroupDragHandler(element, 'test_accessor_id', group);
fireEvent.dragStart(element, new Event('dragstart'));
expect(
LocalSelectionTransfer.getInstance<PanelTransfer>().hasData(
PanelTransfer.prototype
)
).toBeTruthy();
const transferObject =
LocalSelectionTransfer.getInstance<PanelTransfer>().getData(
PanelTransfer.prototype
)![0];
expect(transferObject).toBeTruthy();
expect(transferObject.viewId).toBe('test_accessor_id');
expect(transferObject.groupId).toBe('test_group_id');
expect(transferObject.panelId).toBeNull();
fireEvent.dragStart(element, new Event('dragend'));
expect(
LocalSelectionTransfer.getInstance<PanelTransfer>().hasData(
PanelTransfer.prototype
)
).toBeFalsy();
cut.dispose();
});
test('that the event is cancelled when isFloating and shiftKey=true', () => {
const element = document.createElement('div');
const groupMock = jest.fn<DockviewGroupPanel, []>(() => {
const partial: Partial<DockviewGroupPanel> = {
api: { isFloating: true } as any,
};
return partial as DockviewGroupPanel;
});
const group = new groupMock();
const cut = new GroupDragHandler(element, 'accessor_id', group);
const event = new KeyboardEvent('dragstart', { shiftKey: false });
const spy = jest.spyOn(event, 'preventDefault');
fireEvent(element, event);
expect(spy).toBeCalledTimes(1);
const event2 = new KeyboardEvent('dragstart', { shiftKey: true });
const spy2 = jest.spyOn(event2, 'preventDefault');
fireEvent(element, event);
expect(spy2).toBeCalledTimes(0);
cut.dispose();
});
test('that the event is never cancelled when the group is not floating', () => {
const element = document.createElement('div');
const groupMock = jest.fn<DockviewGroupPanel, []>(() => {
const partial: Partial<DockviewGroupPanel> = {
api: { isFloating: false } as any,
};
return partial as DockviewGroupPanel;
});
const group = new groupMock();
const cut = new GroupDragHandler(element, 'accessor_id', group);
const event = new KeyboardEvent('dragstart', { shiftKey: false });
const spy = jest.spyOn(event, 'preventDefault');
fireEvent(element, event);
expect(spy).toBeCalledTimes(0);
const event2 = new KeyboardEvent('dragstart', { shiftKey: true });
const spy2 = jest.spyOn(event2, 'preventDefault');
fireEvent(element, event);
expect(spy2).toBeCalledTimes(0);
cut.dispose();
});
});

View File

@ -0,0 +1,156 @@
import { Overlay } from '../../dnd/overlay';
describe('overlay', () => {
test('toJSON', () => {
const container = document.createElement('div');
const content = document.createElement('div');
document.body.appendChild(container);
container.appendChild(content);
const cut = new Overlay({
height: 200,
width: 100,
left: 10,
top: 20,
minimumInViewportWidth: 0,
minimumInViewportHeight: 0,
container,
content,
});
jest.spyOn(
container.childNodes.item(0) as HTMLElement,
'getBoundingClientRect'
).mockImplementation(() => {
return { left: 80, top: 100, width: 40, height: 50 } as any;
});
jest.spyOn(container, 'getBoundingClientRect').mockImplementation(
() => {
return { left: 20, top: 30, width: 100, height: 100 } as any;
}
);
expect(cut.toJSON()).toEqual({
top: 70,
left: 60,
width: 40,
height: 50,
});
});
test('that out-of-bounds dimensions are fixed', () => {
const container = document.createElement('div');
const content = document.createElement('div');
document.body.appendChild(container);
container.appendChild(content);
const cut = new Overlay({
height: 200,
width: 100,
left: -1000,
top: -1000,
minimumInViewportWidth: 0,
minimumInViewportHeight: 0,
container,
content,
});
jest.spyOn(
container.childNodes.item(0) as HTMLElement,
'getBoundingClientRect'
).mockImplementation(() => {
return { left: 80, top: 100, width: 40, height: 50 } as any;
});
jest.spyOn(container, 'getBoundingClientRect').mockImplementation(
() => {
return { left: 20, top: 30, width: 100, height: 100 } as any;
}
);
expect(cut.toJSON()).toEqual({
top: 70,
left: 60,
width: 40,
height: 50,
});
});
test('setBounds', () => {
const container = document.createElement('div');
const content = document.createElement('div');
document.body.appendChild(container);
container.appendChild(content);
const cut = new Overlay({
height: 1000,
width: 1000,
left: 0,
top: 0,
minimumInViewportWidth: 0,
minimumInViewportHeight: 0,
container,
content,
});
const element: HTMLElement = container.querySelector(
'.dv-resize-container'
)!;
expect(element).toBeTruthy();
jest.spyOn(element, 'getBoundingClientRect').mockImplementation(() => {
return { left: 300, top: 400, width: 1000, height: 1000 } as any;
});
jest.spyOn(container, 'getBoundingClientRect').mockImplementation(
() => {
return { left: 0, top: 0, width: 1000, height: 1000 } as any;
}
);
cut.setBounds({ height: 100, width: 200, left: 300, top: 400 });
expect(element.style.height).toBe('100px');
expect(element.style.width).toBe('200px');
expect(element.style.left).toBe('300px');
expect(element.style.top).toBe('400px');
});
test('that the resize handles are added', () => {
const container = document.createElement('div');
const content = document.createElement('div');
const cut = new Overlay({
height: 500,
width: 500,
left: 100,
top: 200,
minimumInViewportWidth: 0,
minimumInViewportHeight: 0,
container,
content,
});
expect(container.querySelector('.dv-resize-handle-top')).toBeTruthy();
expect(
container.querySelector('.dv-resize-handle-bottom')
).toBeTruthy();
expect(container.querySelector('.dv-resize-handle-left')).toBeTruthy();
expect(container.querySelector('.dv-resize-handle-right')).toBeTruthy();
expect(
container.querySelector('.dv-resize-handle-topleft')
).toBeTruthy();
expect(
container.querySelector('.dv-resize-handle-topright')
).toBeTruthy();
expect(
container.querySelector('.dv-resize-handle-bottomleft')
).toBeTruthy();
expect(
container.querySelector('.dv-resize-handle-bottomright')
).toBeTruthy();
cut.dispose();
});
});

View File

@ -8,6 +8,7 @@ import { DockviewGroupPanel } from '../../../../dockview/dockviewGroupPanel';
import { DockviewGroupPanelModel } from '../../../../dockview/dockviewGroupPanelModel';
import { fireEvent } from '@testing-library/dom';
import { TestPanel } from '../../dockviewGroupPanelModel.spec';
import { IDockviewPanel } from '../../../../dockview/dockviewPanel';
describe('tabsContainer', () => {
test('that an external event does not render a drop target and calls through to the group mode', () => {
@ -463,4 +464,169 @@ describe('tabsContainer', () => {
expect(query.length).toBe(1);
expect(query[0].children.length).toBe(0);
});
test('that a tab will become floating when clicked if not floating and shift is selected', () => {
const accessorMock = jest.fn<DockviewComponent, []>(() => {
return (<Partial<DockviewComponent>>{
options: {},
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
element: document.createElement('div'),
addFloatingGroup: jest.fn(),
}) as DockviewComponent;
});
const groupPanelMock = jest.fn<DockviewGroupPanel, []>(() => {
return (<Partial<DockviewGroupPanel>>{
api: { isFloating: false } as any,
}) as DockviewGroupPanel;
});
const accessor = new accessorMock();
const groupPanel = new groupPanelMock();
const cut = new TabsContainer(accessor, groupPanel);
const container = cut.element.querySelector('.void-container')!;
expect(container).toBeTruthy();
jest.spyOn(cut.element, 'getBoundingClientRect').mockImplementation(
() => {
return { top: 50, left: 100, width: 0, height: 0 } as any;
}
);
jest.spyOn(
accessor.element,
'getBoundingClientRect'
).mockImplementation(() => {
return { top: 10, left: 20, width: 0, height: 0 } as any;
});
const event = new KeyboardEvent('mousedown', { shiftKey: true });
const eventPreventDefaultSpy = jest.spyOn(event, 'preventDefault');
fireEvent(container, event);
expect(accessor.addFloatingGroup).toBeCalledWith(
groupPanel,
{
x: 100,
y: 60,
},
{ inDragMode: true }
);
expect(accessor.addFloatingGroup).toBeCalledTimes(1);
expect(eventPreventDefaultSpy).toBeCalledTimes(1);
const event2 = new KeyboardEvent('mousedown', { shiftKey: false });
const eventPreventDefaultSpy2 = jest.spyOn(event2, 'preventDefault');
fireEvent(container, event2);
expect(accessor.addFloatingGroup).toBeCalledTimes(1);
expect(eventPreventDefaultSpy2).toBeCalledTimes(0);
});
test('that a tab that is already floating cannot be floated again', () => {
const accessorMock = jest.fn<DockviewComponent, []>(() => {
return (<Partial<DockviewComponent>>{
options: {},
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
element: document.createElement('div'),
addFloatingGroup: jest.fn(),
}) as DockviewComponent;
});
const groupPanelMock = jest.fn<DockviewGroupPanel, []>(() => {
return (<Partial<DockviewGroupPanel>>{
api: { isFloating: true } as any,
}) as DockviewGroupPanel;
});
const accessor = new accessorMock();
const groupPanel = new groupPanelMock();
const cut = new TabsContainer(accessor, groupPanel);
const container = cut.element.querySelector('.void-container')!;
expect(container).toBeTruthy();
jest.spyOn(cut.element, 'getBoundingClientRect').mockImplementation(
() => {
return { top: 50, left: 100, width: 0, height: 0 } as any;
}
);
jest.spyOn(
accessor.element,
'getBoundingClientRect'
).mockImplementation(() => {
return { top: 10, left: 20, width: 0, height: 0 } as any;
});
const event = new KeyboardEvent('mousedown', { shiftKey: true });
const eventPreventDefaultSpy = jest.spyOn(event, 'preventDefault');
fireEvent(container, event);
expect(accessor.addFloatingGroup).toBeCalledTimes(0);
expect(eventPreventDefaultSpy).toBeCalledTimes(0);
const event2 = new KeyboardEvent('mousedown', { shiftKey: false });
const eventPreventDefaultSpy2 = jest.spyOn(event2, 'preventDefault');
fireEvent(container, event2);
expect(accessor.addFloatingGroup).toBeCalledTimes(0);
expect(eventPreventDefaultSpy2).toBeCalledTimes(0);
});
test('that selecting a tab which shift down will move that tab into a new floating group', () => {
const accessorMock = jest.fn<DockviewComponent, []>(() => {
return (<Partial<DockviewComponent>>{
options: {},
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
element: document.createElement('div'),
addFloatingGroup: jest.fn(),
getGroupPanel: jest.fn(),
}) as DockviewComponent;
});
const groupPanelMock = jest.fn<DockviewGroupPanel, []>(() => {
return (<Partial<DockviewGroupPanel>>{
api: { isFloating: true } as any,
}) as DockviewGroupPanel;
});
const accessor = new accessorMock();
const groupPanel = new groupPanelMock();
const cut = new TabsContainer(accessor, groupPanel);
const panelMock = jest.fn<IDockviewPanel, []>(() => {
const partial: Partial<IDockviewPanel> = {
id: 'test_id',
view: {
tab: {
element: document.createElement('div'),
} as any,
content: {
element: document.createElement('div'),
} as any,
} as any,
};
return partial as IDockviewPanel;
});
const panel = new panelMock();
cut.openPanel(panel);
const el = cut.element.querySelector('.tab')!;
expect(el).toBeTruthy();
const event = new KeyboardEvent('mousedown', { shiftKey: true });
const preventDefaultSpy = jest.spyOn(event, 'preventDefault');
fireEvent(el, event);
expect(preventDefaultSpy).toBeCalledTimes(1);
expect(accessor.addFloatingGroup).toBeCalledTimes(1);
});
});

View File

@ -247,8 +247,8 @@ describe('groupview', () => {
id: 'dockview-1',
removePanel: removePanelMock,
removeGroup: removeGroupMock,
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
onDidAddPanel: () => ({ dispose: jest.fn() }),
onDidRemovePanel: () => ({ dispose: jest.fn() }),
}) as DockviewComponent;
groupview = new DockviewGroupPanel(dockview, 'groupview-1', options);
@ -858,6 +858,47 @@ describe('groupview', () => {
).toBe(0);
});
test('that the watermark is removed when dispose is called', () => {
const groupviewMock = jest.fn<Partial<DockviewGroupPanelModel>, []>(
() => {
return {
canDisplayOverlay: jest.fn(),
};
}
);
const groupView = new groupviewMock() as DockviewGroupPanelModel;
const groupPanelMock = jest.fn<Partial<DockviewGroupPanel>, []>(() => {
return {
id: 'testgroupid',
model: groupView,
};
});
const container = document.createElement('div');
const cut = new DockviewGroupPanelModel(
container,
dockview,
'groupviewid',
{},
new groupPanelMock() as DockviewGroupPanel
);
cut.initialize();
expect(
container.getElementsByClassName('watermark-test-container').length
).toBe(1);
cut.dispose();
expect(
container.getElementsByClassName('watermark-test-container').length
).toBe(0);
});
test('that watermark is added', () => {
const groupviewMock = jest.fn<Partial<DockviewGroupPanelModel>, []>(
() => {

View File

@ -0,0 +1,21 @@
import { quasiDefaultPrevented, quasiPreventDefault } from '../dom';
describe('dom', () => {
test('quasiPreventDefault', () => {
const event = new Event('myevent');
expect((event as any)['dv-quasiPreventDefault']).toBeUndefined();
quasiPreventDefault(event);
expect((event as any)['dv-quasiPreventDefault']).toBe(true);
});
test('quasiDefaultPrevented', () => {
const event = new Event('myevent');
expect(quasiDefaultPrevented(event)).toBeFalsy();
(event as any)['dv-quasiPreventDefault'] = false;
expect(quasiDefaultPrevented(event)).toBeFalsy();
(event as any)['dv-quasiPreventDefault'] = true;
expect(quasiDefaultPrevented(event)).toBeTruthy();
});
});

View File

@ -690,4 +690,37 @@ describe('gridview', () => {
gridview.element.querySelectorAll('.mock-grid-view').length
).toBe(4);
});
test('that calling insertOrthogonalSplitviewAtRoot() for an empty view doesnt add any nodes', () => {
const gridview = new Gridview(
false,
{ separatorBorder: '' },
Orientation.HORIZONTAL
);
gridview.layout(1000, 1000);
expect(gridview.serialize()).toEqual({
height: 1000,
orientation: 'HORIZONTAL',
root: {
data: [],
size: 1000,
type: 'branch',
},
width: 1000,
});
gridview.insertOrthogonalSplitviewAtRoot();
expect(gridview.serialize()).toEqual({
height: 1000,
orientation: 'VERTICAL',
root: {
data: [],
size: 1000,
type: 'branch',
},
width: 1000,
});
});
});

View File

@ -2046,7 +2046,7 @@ describe('gridview', () => {
});
});
test('that a deep layout with fromJSON dimensions identical to the current dimensions loads', async () => {
test('that a deep HORIZONTAL layout with fromJSON dimensions identical to the current dimensions loads', async () => {
const container = document.createElement('div');
const gridview = new GridviewComponent({
@ -2056,12 +2056,12 @@ describe('gridview', () => {
components: { default: TestGridview },
});
gridview.layout(5000, 5000);
gridview.layout(6000, 5000);
gridview.fromJSON({
grid: {
height: 5000,
width: 5000,
width: 6000,
orientation: Orientation.HORIZONTAL,
root: {
type: 'branch',
@ -2069,7 +2069,7 @@ describe('gridview', () => {
data: [
{
type: 'leaf',
size: 1000,
size: 2000,
data: {
id: 'panel_1',
component: 'default',
@ -2078,7 +2078,7 @@ describe('gridview', () => {
},
{
type: 'branch',
size: 2000,
size: 3000,
data: [
{
type: 'branch',
@ -2095,7 +2095,7 @@ describe('gridview', () => {
},
{
type: 'branch',
size: 1000,
size: 2000,
data: [
{
type: 'leaf',
@ -2132,7 +2132,7 @@ describe('gridview', () => {
},
{
type: 'leaf',
size: 2000,
size: 1000,
data: {
id: 'panel_6',
component: 'default',
@ -2148,7 +2148,7 @@ describe('gridview', () => {
expect(JSON.parse(JSON.stringify(gridview.toJSON()))).toEqual({
grid: {
height: 5000,
width: 5000,
width: 6000,
orientation: Orientation.HORIZONTAL,
root: {
type: 'branch',
@ -2156,7 +2156,7 @@ describe('gridview', () => {
data: [
{
type: 'leaf',
size: 1000,
size: 2000,
data: {
id: 'panel_1',
component: 'default',
@ -2165,7 +2165,7 @@ describe('gridview', () => {
},
{
type: 'branch',
size: 2000,
size: 3000,
data: [
{
type: 'branch',
@ -2182,7 +2182,7 @@ describe('gridview', () => {
},
{
type: 'branch',
size: 1000,
size: 2000,
data: [
{
type: 'leaf',
@ -2217,9 +2217,374 @@ describe('gridview', () => {
},
],
},
{
type: 'leaf',
size: 1000,
data: {
id: 'panel_6',
component: 'default',
snap: false,
},
},
],
},
},
activePanel: 'panel_1',
});
gridview.layout(6000, 5000, true);
expect(JSON.parse(JSON.stringify(gridview.toJSON()))).toEqual({
grid: {
height: 5000,
width: 6000,
orientation: Orientation.HORIZONTAL,
root: {
type: 'branch',
size: 5000,
data: [
{
type: 'leaf',
size: 2000,
data: {
id: 'panel_1',
component: 'default',
snap: false,
},
},
{
type: 'branch',
size: 3000,
data: [
{
type: 'branch',
size: 4000,
data: [
{
type: 'leaf',
size: 1000,
data: {
id: 'panel_2',
component: 'default',
snap: false,
},
},
{
type: 'branch',
size: 2000,
data: [
{
type: 'leaf',
size: 2000,
data: {
id: 'panel_3',
component: 'default',
snap: false,
},
},
{
type: 'leaf',
size: 2000,
data: {
id: 'panel_4',
component: 'default',
snap: false,
},
},
],
},
],
},
{
type: 'leaf',
size: 1000,
data: {
id: 'panel_5',
component: 'default',
snap: false,
},
},
],
},
{
type: 'leaf',
size: 1000,
data: {
id: 'panel_6',
component: 'default',
snap: false,
},
},
],
},
},
activePanel: 'panel_1',
});
});
test('that a deep VERTICAL layout with fromJSON dimensions identical to the current dimensions loads', async () => {
const container = document.createElement('div');
const gridview = new GridviewComponent({
parentElement: container,
proportionalLayout: true,
orientation: Orientation.VERTICAL,
components: { default: TestGridview },
});
gridview.layout(5000, 6000);
gridview.fromJSON({
grid: {
height: 6000,
width: 5000,
orientation: Orientation.VERTICAL,
root: {
type: 'branch',
size: 5000,
data: [
{
type: 'leaf',
size: 2000,
data: {
id: 'panel_1',
component: 'default',
snap: false,
},
},
{
type: 'branch',
size: 3000,
data: [
{
type: 'branch',
size: 4000,
data: [
{
type: 'leaf',
size: 1000,
data: {
id: 'panel_2',
component: 'default',
snap: false,
},
},
{
type: 'branch',
size: 2000,
data: [
{
type: 'leaf',
size: 2000,
data: {
id: 'panel_3',
component: 'default',
snap: false,
},
},
{
type: 'leaf',
size: 2000,
data: {
id: 'panel_4',
component: 'default',
snap: false,
},
},
],
},
],
},
{
type: 'leaf',
size: 1000,
data: {
id: 'panel_5',
component: 'default',
snap: false,
},
},
],
},
{
type: 'leaf',
size: 1000,
data: {
id: 'panel_6',
component: 'default',
snap: false,
},
},
],
},
},
activePanel: 'panel_1',
});
expect(JSON.parse(JSON.stringify(gridview.toJSON()))).toEqual({
grid: {
height: 6000,
width: 5000,
orientation: Orientation.VERTICAL,
root: {
type: 'branch',
size: 5000,
data: [
{
type: 'leaf',
size: 2000,
data: {
id: 'panel_1',
component: 'default',
snap: false,
},
},
{
type: 'branch',
size: 3000,
data: [
{
type: 'branch',
size: 4000,
data: [
{
type: 'leaf',
size: 1000,
data: {
id: 'panel_2',
component: 'default',
snap: false,
},
},
{
type: 'branch',
size: 2000,
data: [
{
type: 'leaf',
size: 2000,
data: {
id: 'panel_3',
component: 'default',
snap: false,
},
},
{
type: 'leaf',
size: 2000,
data: {
id: 'panel_4',
component: 'default',
snap: false,
},
},
],
},
],
},
{
type: 'leaf',
size: 1000,
data: {
id: 'panel_5',
component: 'default',
snap: false,
},
},
],
},
{
type: 'leaf',
size: 1000,
data: {
id: 'panel_6',
component: 'default',
snap: false,
},
},
],
},
},
activePanel: 'panel_1',
});
gridview.layout(5000, 6000, true);
expect(JSON.parse(JSON.stringify(gridview.toJSON()))).toEqual({
grid: {
height: 6000,
width: 5000,
orientation: Orientation.VERTICAL,
root: {
type: 'branch',
size: 5000,
data: [
{
type: 'leaf',
size: 2000,
data: {
id: 'panel_1',
component: 'default',
snap: false,
},
},
{
type: 'branch',
size: 3000,
data: [
{
type: 'branch',
size: 4000,
data: [
{
type: 'leaf',
size: 1000,
data: {
id: 'panel_2',
component: 'default',
snap: false,
},
},
{
type: 'branch',
size: 2000,
data: [
{
type: 'leaf',
size: 2000,
data: {
id: 'panel_3',
component: 'default',
snap: false,
},
},
{
type: 'leaf',
size: 2000,
data: {
id: 'panel_4',
component: 'default',
snap: false,
},
},
],
},
],
},
{
type: 'leaf',
size: 1000,
data: {
id: 'panel_5',
component: 'default',
snap: false,
},
},
],
},
{
type: 'leaf',
size: 1000,
data: {
id: 'panel_6',
component: 'default',

View File

@ -435,7 +435,7 @@ export class DockviewApi implements CommonApi<SerializedDockview> {
return this.component.addPanel(options);
}
addGroup(options?: AddGroupOptions): IDockviewGroupPanel {
addGroup(options?: AddGroupOptions): DockviewGroupPanel {
return this.component.addGroup(options);
}
@ -459,6 +459,13 @@ export class DockviewApi implements CommonApi<SerializedDockview> {
return this.component.getPanel(id);
}
addFloatingGroup(
item: IDockviewPanel | DockviewGroupPanel,
coord?: { x: number; y: number }
): void {
return this.component.addFloatingGroup(item, coord);
}
fromJSON(data: SerializedDockview): void {
this.component.fromJSON(data);
}

View File

@ -0,0 +1,54 @@
import { Position } from '../dnd/droptarget';
import { DockviewComponent } from '../dockview/dockviewComponent';
import { DockviewGroupPanel } from '../dockview/dockviewGroupPanel';
import { Emitter, Event } from '../events';
import { GridviewPanelApi, GridviewPanelApiImpl } from './gridviewPanelApi';
export interface DockviewGroupPanelApi extends GridviewPanelApi {
readonly onDidFloatingStateChange: Event<DockviewGroupPanelFloatingChangeEvent>;
readonly isFloating: boolean;
moveTo(options: { group: DockviewGroupPanel; position?: Position }): void;
}
export interface DockviewGroupPanelFloatingChangeEvent {
readonly isFloating: boolean;
}
export class DockviewGroupPanelApiImpl extends GridviewPanelApiImpl {
private _group: DockviewGroupPanel | undefined;
readonly _onDidFloatingStateChange =
new Emitter<DockviewGroupPanelFloatingChangeEvent>();
readonly onDidFloatingStateChange: Event<DockviewGroupPanelFloatingChangeEvent> =
this._onDidFloatingStateChange.event;
get isFloating() {
if (!this._group) {
throw new Error(`DockviewGroupPanelApiImpl not initialized`);
}
return this._group.model.isFloating;
}
constructor(id: string, private readonly accessor: DockviewComponent) {
super(id);
this.addDisposables(this._onDidFloatingStateChange);
}
moveTo(options: { group: DockviewGroupPanel; position?: Position }): void {
if (!this._group) {
throw new Error(`DockviewGroupPanelApiImpl not initialized`);
}
this.accessor.moveGroupOrPanel(
options.group,
this._group.id,
undefined,
options.position ?? 'center'
);
}
initialize(group: DockviewGroupPanel): void {
this._group = group;
}
}

View File

@ -3,6 +3,8 @@ import { GridviewPanelApiImpl, GridviewPanelApi } from './gridviewPanelApi';
import { DockviewGroupPanel } from '../dockview/dockviewGroupPanel';
import { MutableDisposable } from '../lifecycle';
import { IDockviewPanel } from '../dockview/dockviewPanel';
import { DockviewComponent } from '../dockview/dockviewComponent';
import { Position } from '../dnd/droptarget';
export interface TitleEvent {
readonly title: string;
@ -24,6 +26,11 @@ export interface DockviewPanelApi
readonly onDidGroupChange: Event<void>;
close(): void;
setTitle(title: string): void;
moveTo(options: {
group: DockviewGroupPanel;
position?: Position;
index?: number;
}): void;
}
export class DockviewPanelApiImpl
@ -73,7 +80,11 @@ export class DockviewPanelApiImpl
return this._group;
}
constructor(private panel: IDockviewPanel, group: DockviewGroupPanel) {
constructor(
private panel: IDockviewPanel,
group: DockviewGroupPanel,
private readonly accessor: DockviewComponent
) {
super(panel.id);
this.initialize(panel);
@ -88,11 +99,25 @@ export class DockviewPanelApiImpl
);
}
public setTitle(title: string): void {
moveTo(options: {
group: DockviewGroupPanel;
position?: Position;
index?: number;
}): void {
this.accessor.moveGroupOrPanel(
options.group,
this._group.id,
this.panel.id,
options.position ?? 'center',
options.index
);
}
setTitle(title: string): void {
this.panel.setTitle(title);
}
public close(): void {
close(): void {
this.group.model.closePanel(this.panel);
}
}

View File

@ -61,3 +61,13 @@ export function firstIndex<T>(
return -1;
}
export function remove<T>(array: T[], value: T): boolean {
const index = array.findIndex((t) => t === value);
if (index > -1) {
array.splice(index, 1);
return true;
}
return false;
}

View File

@ -27,10 +27,19 @@ export abstract class DragHandler extends CompositeDisposable {
abstract getData(dataTransfer?: DataTransfer | null): IDisposable;
protected isCancelled(_event: DragEvent): boolean {
return false;
}
private configure(): void {
this.addDisposables(
this._onDragStart,
addDisposableListener(this.el, 'dragstart', (event) => {
if (this.isCancelled(event)) {
event.preventDefault();
return;
}
const iframes = [
...getElementsByTagName('iframe'),
...getElementsByTagName('webview'),

View File

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

View File

@ -58,10 +58,13 @@ export class Droptarget extends CompositeDisposable {
private targetElement: HTMLElement | undefined;
private overlayElement: HTMLElement | undefined;
private _state: Position | undefined;
private _acceptedTargetZonesSet: Set<Position>;
private readonly _onDrop = new Emitter<DroptargetEvent>();
readonly onDrop: Event<DroptargetEvent> = this._onDrop.event;
private static USED_EVENT_ID = '__dockview_droptarget_event_is_used__';
get state(): Position | undefined {
return this._state;
}
@ -83,7 +86,7 @@ export class Droptarget extends CompositeDisposable {
super();
// use a set to take advantage of #<set>.has
const acceptedTargetZonesSet = new Set(
this._acceptedTargetZonesSet = new Set(
this.options.acceptedTargetZones
);
@ -92,6 +95,11 @@ export class Droptarget extends CompositeDisposable {
new DragAndDropObserver(this.element, {
onDragEnter: () => undefined,
onDragOver: (e) => {
if (this._acceptedTargetZonesSet.size === 0) {
this.removeDropTarget();
return;
}
const width = this.element.clientWidth;
const height = this.element.clientHeight;
@ -106,14 +114,19 @@ export class Droptarget extends CompositeDisposable {
const y = e.clientY - rect.top;
const quadrant = this.calculateQuadrant(
acceptedTargetZonesSet,
this._acceptedTargetZonesSet,
x,
y,
width,
height
);
if (quadrant === null) {
/**
* If the event has already been used by another DropTarget instance
* then don't show a second drop target, only one target should be
* active at any one time
*/
if (this.isAlreadyUsed(e) || quadrant === null) {
// no drop target should be displayed
this.removeDropTarget();
return;
@ -121,12 +134,16 @@ export class Droptarget extends CompositeDisposable {
if (typeof this.options.canDisplayOverlay === 'boolean') {
if (!this.options.canDisplayOverlay) {
this.removeDropTarget();
return;
}
} else if (!this.options.canDisplayOverlay(e, quadrant)) {
this.removeDropTarget();
return;
}
this.markAsUsed(e);
if (!this.targetElement) {
this.targetElement = document.createElement('div');
this.targetElement.className = 'drop-target-dropzone';
@ -139,14 +156,6 @@ export class Droptarget extends CompositeDisposable {
this.element.append(this.targetElement);
}
if (this.options.acceptedTargetZones.length === 0) {
return;
}
if (!this.targetElement || !this.overlayElement) {
return;
}
this.toggleClasses(quadrant, width, height);
this.setState(quadrant);
@ -175,11 +184,30 @@ export class Droptarget extends CompositeDisposable {
);
}
public dispose(): void {
setTargetZones(acceptedTargetZones: Position[]): void {
this._acceptedTargetZonesSet = new Set(acceptedTargetZones);
}
dispose(): void {
this.removeDropTarget();
super.dispose();
}
/**
* Add a property to the event object for other potential listeners to check
*/
private markAsUsed(event: DragEvent): void {
(event as any)[Droptarget.USED_EVENT_ID] = true;
}
/**
* Check is the event has already been used by another instance od DropTarget
*/
private isAlreadyUsed(event: DragEvent): boolean {
const value = (event as any)[Droptarget.USED_EVENT_ID];
return typeof value === 'boolean' && value;
}
private toggleClasses(
quadrant: Position,
width: number,

View File

@ -1,4 +1,6 @@
import { DockviewGroupPanel } from '../dockview/dockviewGroupPanel';
import { quasiPreventDefault } from '../dom';
import { addDisposableListener } from '../events';
import { IDisposable } from '../lifecycle';
import { DragHandler } from './abstractDragHandler';
import { LocalSelectionTransfer, PanelTransfer } from './dataTransfer';
@ -14,6 +16,31 @@ export class GroupDragHandler extends DragHandler {
private readonly group: DockviewGroupPanel
) {
super(element);
this.addDisposables(
addDisposableListener(
element,
'mousedown',
(e) => {
if (e.shiftKey) {
/**
* You cannot call e.preventDefault() because that will prevent drag events from firing
* but we also need to stop any group overlay drag events from occuring
* Use a custom event marker that can be checked by the overlay drag events
*/
quasiPreventDefault(e);
}
},
true
)
);
}
override isCancelled(_event: DragEvent): boolean {
if (this.group.api.isFloating && !_event.shiftKey) {
return true;
}
return false;
}
getData(dataTransfer: DataTransfer | null): IDisposable {

View File

@ -0,0 +1,122 @@
.dv-debug {
.dv-resize-container {
.dv-resize-handle-top {
background-color: red;
}
.dv-resize-handle-bottom {
background-color: green;
}
.dv-resize-handle-left {
background-color: yellow;
}
.dv-resize-handle-right {
background-color: blue;
}
.dv-resize-handle-topleft,
.dv-resize-handle-topright,
.dv-resize-handle-bottomleft,
.dv-resize-handle-bottomright {
background-color: cyan;
}
}
}
.dv-resize-container {
position: absolute;
z-index: 997;
&.dv-bring-to-front {
z-index: 998;
}
border: 1px solid var(--dv-tab-divider-color);
box-shadow: var(--dv-floating-box-shadow);
&.dv-resize-container-dragging {
opacity: 0.5;
}
.dv-resize-handle-top {
height: 4px;
width: calc(100% - 8px);
left: 4px;
top: -2px;
z-index: 999;
position: absolute;
cursor: ns-resize;
}
.dv-resize-handle-bottom {
height: 4px;
width: calc(100% - 8px);
left: 4px;
bottom: -2px;
z-index: 999;
position: absolute;
cursor: ns-resize;
}
.dv-resize-handle-left {
height: calc(100% - 8px);
width: 4px;
left: -2px;
top: 4px;
z-index: 999;
position: absolute;
cursor: ew-resize;
}
.dv-resize-handle-right {
height: calc(100% - 8px);
width: 4px;
right: -2px;
top: 4px;
z-index: 999;
position: absolute;
cursor: ew-resize;
}
.dv-resize-handle-topleft {
height: 4px;
width: 4px;
top: -2px;
left: -2px;
z-index: 999;
position: absolute;
cursor: nw-resize;
}
.dv-resize-handle-topright {
height: 4px;
width: 4px;
right: -2px;
top: -2px;
z-index: 999;
position: absolute;
cursor: ne-resize;
}
.dv-resize-handle-bottomleft {
height: 4px;
width: 4px;
left: -2px;
bottom: -2px;
z-index: 999;
position: absolute;
cursor: sw-resize;
}
.dv-resize-handle-bottomright {
height: 4px;
width: 4px;
right: -2px;
bottom: -2px;
z-index: 999;
position: absolute;
cursor: se-resize;
}
}

View File

@ -0,0 +1,449 @@
import { quasiDefaultPrevented, toggleClass } from '../dom';
import {
Emitter,
Event,
addDisposableListener,
addDisposableWindowListener,
} from '../events';
import { CompositeDisposable, MutableDisposable } from '../lifecycle';
import { clamp } from '../math';
const bringElementToFront = (() => {
let previous: HTMLElement | null = null;
function pushToTop(element: HTMLElement) {
if (previous !== element && previous !== null) {
toggleClass(previous, 'dv-bring-to-front', false);
}
toggleClass(element, 'dv-bring-to-front', true);
previous = element;
}
return pushToTop;
})();
export class Overlay extends CompositeDisposable {
private _element: HTMLElement = document.createElement('div');
private readonly _onDidChange = new Emitter<void>();
readonly onDidChange: Event<void> = this._onDidChange.event;
private static MINIMUM_HEIGHT = 20;
private static MINIMUM_WIDTH = 20;
constructor(
private readonly options: {
height: number;
width: number;
left: number;
top: number;
container: HTMLElement;
content: HTMLElement;
minimumInViewportWidth: number;
minimumInViewportHeight: number;
}
) {
super();
this.addDisposables(this._onDidChange);
this.setupOverlay();
this.setupResize('top');
this.setupResize('bottom');
this.setupResize('left');
this.setupResize('right');
this.setupResize('topleft');
this.setupResize('topright');
this.setupResize('bottomleft');
this.setupResize('bottomright');
this._element.appendChild(this.options.content);
this.options.container.appendChild(this._element);
// if input bad resize within acceptable boundaries
this.renderWithinBoundaryConditions();
}
setBounds(
bounds: Partial<{
height: number;
width: number;
top: number;
left: number;
}>
): void {
if (typeof bounds.height === 'number') {
this._element.style.height = `${bounds.height}px`;
}
if (typeof bounds.width === 'number') {
this._element.style.width = `${bounds.width}px`;
}
if (typeof bounds.top === 'number') {
this._element.style.top = `${bounds.top}px`;
}
if (typeof bounds.left === 'number') {
this._element.style.left = `${bounds.left}px`;
}
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,
};
}
renderWithinBoundaryConditions(): void {
const containerRect = this.options.container.getBoundingClientRect();
const overlayRect = this._element.getBoundingClientRect();
// a minimum width of minimumViewportWidth must be inside the viewport
const xOffset = Math.max(
0,
overlayRect.width - this.options.minimumInViewportWidth
);
// a minimum height of minimumViewportHeight must be inside the viewport
const yOffset = Math.max(
0,
overlayRect.height - this.options.minimumInViewportHeight
);
const left = clamp(
overlayRect.left - containerRect.left,
-xOffset,
Math.max(0, containerRect.width - overlayRect.width + xOffset)
);
const top = clamp(
overlayRect.top - containerRect.top,
-yOffset,
Math.max(0, containerRect.height - overlayRect.height + yOffset)
);
this._element.style.left = `${left}px`;
this._element.style.top = `${top}px`;
}
setupDrag(
dragTarget: HTMLElement,
options: { inDragMode: boolean } = { inDragMode: false }
): void {
const move = new MutableDisposable();
const track = () => {
let offset: { x: number; y: number } | null = null;
move.value = new CompositeDisposable(
addDisposableWindowListener(window, 'mousemove', (e) => {
const containerRect =
this.options.container.getBoundingClientRect();
const x = e.clientX - containerRect.left;
const y = e.clientY - containerRect.top;
toggleClass(
this._element,
'dv-resize-container-dragging',
true
);
const overlayRect = this._element.getBoundingClientRect();
if (offset === null) {
offset = {
x: e.clientX - overlayRect.left,
y: e.clientY - overlayRect.top,
};
}
const xOffset = Math.max(
0,
overlayRect.width - this.options.minimumInViewportWidth
);
const yOffset = Math.max(
0,
overlayRect.height -
this.options.minimumInViewportHeight
);
const left = clamp(
x - offset.x,
-xOffset,
Math.max(
0,
containerRect.width - overlayRect.width + xOffset
)
);
const top = clamp(
y - offset.y,
-yOffset,
Math.max(
0,
containerRect.height - overlayRect.height + yOffset
)
);
this._element.style.left = `${left}px`;
this._element.style.top = `${top}px`;
}),
addDisposableWindowListener(window, 'mouseup', () => {
toggleClass(
this._element,
'dv-resize-container-dragging',
false
);
move.dispose();
this._onDidChange.fire();
})
);
};
this.addDisposables(
move,
addDisposableListener(dragTarget, 'mousedown', (event) => {
if (event.defaultPrevented) {
event.preventDefault();
return;
}
// if somebody has marked this event then treat as a defaultPrevented
// without actually calling event.preventDefault()
if (quasiDefaultPrevented(event)) {
return;
}
track();
}),
addDisposableListener(
this.options.content,
'mousedown',
(event) => {
if (event.defaultPrevented) {
return;
}
// if somebody has marked this event then treat as a defaultPrevented
// without actually calling event.preventDefault()
if (quasiDefaultPrevented(event)) {
return;
}
if (event.shiftKey) {
track();
}
}
),
addDisposableListener(
this.options.content,
'mousedown',
() => {
bringElementToFront(this._element);
},
true
)
);
bringElementToFront(this._element);
if (options.inDragMode) {
track();
}
}
private setupOverlay(): void {
this._element.style.height = `${this.options.height}px`;
this._element.style.width = `${this.options.width}px`;
this._element.style.left = `${this.options.left}px`;
this._element.style.top = `${this.options.top}px`;
this._element.className = 'dv-resize-container';
}
private setupResize(
direction:
| 'top'
| 'bottom'
| 'left'
| 'right'
| 'topleft'
| 'topright'
| 'bottomleft'
| 'bottomright'
): void {
const resizeHandleElement = document.createElement('div');
resizeHandleElement.className = `dv-resize-handle-${direction}`;
this._element.appendChild(resizeHandleElement);
const move = new MutableDisposable();
this.addDisposables(
move,
addDisposableListener(resizeHandleElement, 'mousedown', (e) => {
e.preventDefault();
let startPosition: {
originalY: number;
originalHeight: number;
originalX: number;
originalWidth: number;
} | null = null;
move.value = new CompositeDisposable(
addDisposableWindowListener(window, 'mousemove', (e) => {
const containerRect =
this.options.container.getBoundingClientRect();
const overlayRect =
this._element.getBoundingClientRect();
const y = e.clientY - containerRect.top;
const x = e.clientX - containerRect.left;
if (startPosition === null) {
// record the initial dimensions since as all subsequence moves are relative to this
startPosition = {
originalY: y,
originalHeight: overlayRect.height,
originalX: x,
originalWidth: overlayRect.width,
};
}
let top: number | null = null;
let height: number | null = null;
let left: number | null = null;
let width: number | null = null;
function moveTop() {
top = clamp(
y,
0,
Math.max(
0,
startPosition!.originalY +
startPosition!.originalHeight -
Overlay.MINIMUM_HEIGHT
)
);
height =
startPosition!.originalY +
startPosition!.originalHeight -
top;
}
function moveBottom() {
top =
startPosition!.originalY -
startPosition!.originalHeight;
height = clamp(
y - top,
Overlay.MINIMUM_HEIGHT,
Math.max(
0,
containerRect.height -
startPosition!.originalY +
startPosition!.originalHeight
)
);
}
function moveLeft() {
left = clamp(
x,
0,
Math.max(
0,
startPosition!.originalX +
startPosition!.originalWidth -
Overlay.MINIMUM_WIDTH
)
);
width =
startPosition!.originalX +
startPosition!.originalWidth -
left;
}
function moveRight() {
left =
startPosition!.originalX -
startPosition!.originalWidth;
width = clamp(
x - left,
Overlay.MINIMUM_WIDTH,
Math.max(
0,
containerRect.width -
startPosition!.originalX +
startPosition!.originalWidth
)
);
}
switch (direction) {
case 'top':
moveTop();
break;
case 'bottom':
moveBottom();
break;
case 'left':
moveLeft();
break;
case 'right':
moveRight();
break;
case 'topleft':
moveTop();
moveLeft();
break;
case 'topright':
moveTop();
moveRight();
break;
case 'bottomleft':
moveBottom();
moveLeft();
break;
case 'bottomright':
moveBottom();
moveRight();
break;
}
if (height !== null) {
this._element.style.height = `${height}px`;
}
if (top !== null) {
this._element.style.top = `${top}px`;
}
if (left !== null) {
this._element.style.left = `${left}px`;
}
if (width !== null) {
this._element.style.width = `${width}px`;
}
}),
addDisposableWindowListener(window, 'mouseup', () => {
move.dispose();
this._onDidChange.fire();
})
);
})
);
}
override dispose(): void {
this._element.remove();
super.dispose();
}
}

View File

@ -6,7 +6,7 @@ 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';
@ -38,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();
@ -79,6 +79,7 @@ export class Tab extends CompositeDisposable implements ITab {
if (event.defaultPrevented) {
return;
}
/**
* TODO: alternative to stopPropagation
*

View File

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

View File

@ -28,6 +28,7 @@ export class VoidContainer extends CompositeDisposable {
this._element = document.createElement('div');
this._element.className = 'void-container';
this._element.id = 'dv-group-float-drag-handle';
this._element.tabIndex = 0;
this._element.draggable = true;

View File

@ -1,7 +1,7 @@
import { GroupviewPanelState } from './types';
import { DockviewGroupPanel } from './dockviewGroupPanel';
import { DockviewPanel, IDockviewPanel } from './dockviewPanel';
import { IDockviewComponent } from './dockviewComponent';
import { DockviewComponent } from './dockviewComponent';
import { DockviewPanelModel } from './dockviewPanelModel';
import { DockviewApi } from '../api/component.api';
@ -21,7 +21,7 @@ interface LegacyState extends GroupviewPanelState {
}
export class DefaultDockviewDeserialzier implements IPanelDeserializer {
constructor(private readonly layout: IDockviewComponent) {}
constructor(private readonly layout: DockviewComponent) {}
public fromJSON(
panelData: GroupviewPanelState,

View File

@ -1,14 +1,15 @@
.dv-dockview {
position: relative;
background-color: var(--dv-group-view-background-color);
position: relative;
background-color: var(--dv-group-view-background-color);
.dv-watermark-container {
position: absolute;
top: 0px;
left: 0px;
height: 100%;
width: 100%;
}
.dv-watermark-container {
position: absolute;
top: 0px;
left: 0px;
height: 100%;
width: 100%;
z-index: 1;
}
}
.groupview {

View File

@ -5,7 +5,7 @@ import {
ISerializedLeafNode,
} from '../gridview/gridview';
import { directionToPosition, Droptarget, Position } from '../dnd/droptarget';
import { tail, sequenceEquals } from '../array';
import { tail, sequenceEquals, remove } from '../array';
import { DockviewPanel, IDockviewPanel } from './dockviewPanel';
import { CompositeDisposable } from '../lifecycle';
import { Event, Emitter } from '../events';
@ -41,15 +41,26 @@ import {
GroupPanelViewState,
GroupviewDropEvent,
} from './dockviewGroupPanelModel';
import { DockviewGroupPanel, IDockviewGroupPanel } from './dockviewGroupPanel';
import { DockviewGroupPanel } from './dockviewGroupPanel';
import { DockviewPanelModel } from './dockviewPanelModel';
import { getPanelData } from '../dnd/dataTransfer';
import { Overlay } from '../dnd/overlay';
import { toggleClass } from '../dom';
import {
DockviewFloatingGroupPanel,
IDockviewFloatingGroupPanel,
} from './dockviewFloatingGroupPanel';
export interface PanelReference {
update: (event: { params: { [key: string]: any } }) => void;
remove: () => void;
}
export interface SerializedFloatingGroup {
data: GroupPanelViewState;
position: { height: number; width: number; left: number; top: number };
}
export interface SerializedDockview {
grid: {
root: SerializedGridObject<GroupPanelViewState>;
@ -57,8 +68,9 @@ export interface SerializedDockview {
width: number;
orientation: Orientation;
};
panels: { [key: string]: GroupviewPanelState };
panels: Record<string, GroupviewPanelState>;
activeGroup?: string;
floatingGroups?: SerializedFloatingGroup[];
}
export type DockviewComponentUpdateOptions = Pick<
@ -84,6 +96,7 @@ export interface IDockviewComponent extends IBaseGrid<DockviewGroupPanel> {
readonly activePanel: IDockviewPanel | undefined;
readonly totalPanels: number;
readonly panels: IDockviewPanel[];
readonly floatingGroups: IDockviewFloatingGroupPanel[];
readonly onDidDrop: Event<DockviewDropEvent>;
readonly orientation: Orientation;
updateOptions(options: DockviewComponentUpdateOptions): void;
@ -102,7 +115,7 @@ export interface IDockviewComponent extends IBaseGrid<DockviewGroupPanel> {
getGroupPanel: (id: string) => IDockviewPanel | undefined;
createWatermarkComponent(): IWatermarkRenderer;
// lifecycle
addGroup(options?: AddGroupOptions): IDockviewGroupPanel;
addGroup(options?: AddGroupOptions): DockviewGroupPanel;
closeAllGroups(): void;
// events
moveToNext(options?: MovementOptions): void;
@ -116,6 +129,10 @@ export interface IDockviewComponent extends IBaseGrid<DockviewGroupPanel> {
readonly onDidAddPanel: Event<IDockviewPanel>;
readonly onDidLayoutFromJSON: Event<void>;
readonly onDidActivePanelChange: Event<IDockviewPanel | undefined>;
addFloatingGroup(
item: IDockviewPanel | DockviewGroupPanel,
coord?: { x: number; y: number }
): void;
}
export class DockviewComponent
@ -147,6 +164,8 @@ export class DockviewComponent
readonly onDidActivePanelChange: Event<IDockviewPanel | undefined> =
this._onDidActivePanelChange.event;
readonly floatingGroups: DockviewFloatingGroupPanel[] = [];
get orientation(): Orientation {
return this.gridview.orientation;
}
@ -181,7 +200,7 @@ export class DockviewComponent
parentElement: options.parentElement,
});
this.element.classList.add('dv-dockview');
toggleClass(this.gridview.element, 'dv-dockview', true);
this.addDisposables(
this._onDidDrop,
@ -229,6 +248,13 @@ export class DockviewComponent
if (data.viewId !== this.id) {
return false;
}
if (position === 'center') {
// center drop target is only allowed if there are no panels in the grid
// floating panels are allowed
return this.gridview.length === 0;
}
return true;
}
@ -243,7 +269,7 @@ export class DockviewComponent
return false;
},
acceptedTargetZones: ['top', 'bottom', 'left', 'right'],
acceptedTargetZones: ['top', 'bottom', 'left', 'right', 'center'],
overlayModel: {
activationSize: { type: 'pixels', value: 10 },
size: { type: 'pixels', value: 20 },
@ -278,6 +304,85 @@ export class DockviewComponent
this.updateWatermark();
}
addFloatingGroup(
item: DockviewPanel | DockviewGroupPanel,
coord?: { x?: number; y?: number; height?: number; width?: number },
options?: { skipRemoveGroup?: boolean; inDragMode: boolean }
): void {
let group: DockviewGroupPanel;
if (item instanceof DockviewPanel) {
group = this.createGroup();
this.removePanel(item, {
removeEmptyGroup: true,
skipDispose: true,
});
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 overlayLeft =
typeof coord?.x === 'number' ? Math.max(coord.x, 0) : 100;
const overlayTop =
typeof coord?.y === 'number' ? Math.max(coord.y, 0) : 100;
const overlay = new Overlay({
container: this.gridview.element,
content: group.element,
height: coord?.height ?? 300,
width: coord?.width ?? 300,
left: overlayLeft,
top: overlayTop,
minimumInViewportWidth: 100,
minimumInViewportHeight: 100,
});
const el = group.element.querySelector('#dv-group-float-drag-handle');
if (el) {
overlay.setupDrag(el as HTMLElement, {
inDragMode:
typeof options?.inDragMode === 'boolean'
? options.inDragMode
: false,
});
}
const floatingGroupPanel = new DockviewFloatingGroupPanel(
group,
overlay
);
floatingGroupPanel.addDisposables(
overlay.onDidChange(() => {
this._bufferOnDidLayoutChange.fire();
}),
{
dispose: () => {
group.model.isFloating = false;
remove(this.floatingGroups, floatingGroupPanel);
this.updateWatermark();
},
}
);
this.floatingGroups.push(floatingGroupPanel);
this.updateWatermark();
}
private orthogonalize(position: Position): DockviewGroupPanel {
switch (position) {
case 'top':
@ -303,6 +408,7 @@ export class DockviewComponent
switch (position) {
case 'top':
case 'left':
case 'center':
return this.createGroupAtLocation([0]); // insert into first position
case 'bottom':
case 'right':
@ -326,6 +432,21 @@ export class DockviewComponent
this.layout(this.gridview.width, this.gridview.height, true);
}
override layout(
width: number,
height: number,
forceResize?: boolean | undefined
): void {
super.layout(width, height, forceResize);
if (this.floatingGroups) {
for (const floating of this.floatingGroups) {
// ensure floting groups stay within visible boundaries
floating.overlay.renderWithinBoundaryConditions();
}
}
}
focus(): void {
this.activeGroup?.focus();
}
@ -397,11 +518,26 @@ export class DockviewComponent
return collection;
}, {} as { [key: string]: GroupviewPanelState });
return {
const floats: SerializedFloatingGroup[] = this.floatingGroups.map(
(floatingGroup) => {
return {
data: floatingGroup.group.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 {
@ -417,48 +553,67 @@ export class DockviewComponent
const width = this.width;
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, {
fromJSON: (node: ISerializedLeafNode<GroupPanelViewState>) => {
const { id, locked, hideHeader, views, activeView } = 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;
return createGroupFromSerializedState(node.data);
},
});
this.layout(width, height);
this.layout(width, height, true);
const serializedFloatingGroups = data.floatingGroups ?? [];
for (const serializedFloatingGroup of serializedFloatingGroups) {
const { data, position } = serializedFloatingGroup;
const group = createGroupFromSerializedState(data);
this.addFloatingGroup(
group,
{
x: position.left,
y: position.top,
height: position.height,
width: position.width,
},
{ skipRemoveGroup: true, inDragMode: false }
);
}
for (const floatingGroup of this.floatingGroups) {
floatingGroup.overlay.renderWithinBoundaryConditions();
}
if (typeof activeGroup === 'string') {
const panel = this.getPanel(activeGroup);
@ -478,7 +633,7 @@ export class DockviewComponent
for (const group of groups) {
// remove the group will automatically remove the panels
this.removeGroup(group, true);
this.removeGroup(group, { skipActive: true });
}
if (hasActiveGroup) {
@ -500,13 +655,19 @@ export class DockviewComponent
}
}
addPanel(options: AddPanelOptions): IDockviewPanel {
addPanel(options: AddPanelOptions): DockviewPanel {
if (this.panels.find((_) => _.id === options.id)) {
throw new Error(`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(...)'
);
}
if (options.position) {
if (isPanelOptionsWithPanel(options.position)) {
const referencePanel =
@ -545,13 +706,29 @@ export class DockviewComponent
referenceGroup = this.activeGroup;
}
let panel: IDockviewPanel;
let panel: DockviewPanel;
if (referenceGroup) {
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, {
inDragMode: false,
skipRemoveGroup: true,
});
} else if (referenceGroup.api.isFloating || target === 'center') {
panel = this.createPanel(options, referenceGroup);
referenceGroup.model.openPanel(panel);
} else {
@ -565,10 +742,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, {
inDragMode: false,
skipRemoveGroup: true,
});
} else {
const group = this.createGroupAtLocation();
panel = this.createPanel(options, group);
group.model.openPanel(panel);
}
@ -592,7 +785,9 @@ export class DockviewComponent
group.model.removePanel(panel);
panel.dispose();
if (!options.skipDispose) {
panel.dispose();
}
if (group.size === 0 && options.removeEmptyGroup) {
this.removeGroup(group);
@ -614,7 +809,7 @@ export class DockviewComponent
}
private updateWatermark(): void {
if (this.groups.length === 0) {
if (this.groups.filter((x) => !x.api.isFloating).length === 0) {
if (!this.watermark) {
this.watermark = this.createWatermarkComponent();
@ -626,7 +821,7 @@ export class DockviewComponent
watermarkContainer.className = 'dv-watermark-container';
watermarkContainer.appendChild(this.watermark.element);
this.element.appendChild(watermarkContainer);
this.gridview.element.appendChild(watermarkContainer);
}
} else if (this.watermark) {
this.watermark.element.parentElement!.remove();
@ -696,17 +891,51 @@ export class DockviewComponent
}
}
removeGroup(group: DockviewGroupPanel, skipActive = false): void {
removeGroup(
group: DockviewGroupPanel,
options?:
| {
skipActive?: boolean;
skipDispose?: boolean;
}
| undefined
): void {
const panels = [...group.panels]; // reassign since group panels will mutate
for (const panel of panels) {
this.removePanel(panel, {
removeEmptyGroup: false,
skipDispose: false,
skipDispose: options?.skipDispose ?? false,
});
}
super.doRemoveGroup(group, { skipActive });
this.doRemoveGroup(group, options);
}
protected override doRemoveGroup(
group: DockviewGroupPanel,
options?:
| {
skipActive?: boolean;
skipDispose?: boolean;
}
| undefined
): DockviewGroupPanel {
const floatingGroup = this.floatingGroups.find(
(_) => _.group === group
);
if (floatingGroup) {
if (!options?.skipDispose) {
floatingGroup.group.dispose();
this._groups.delete(group.id);
}
floatingGroup.dispose();
return floatingGroup.group;
}
return super.doRemoveGroup(group, options);
}
moveGroupOrPanel(
@ -757,34 +986,44 @@ export class DockviewComponent
if (sourceGroup && sourceGroup.size < 2) {
const [targetParentLocation, to] = tail(targetLocation);
const sourceLocation = getGridLocation(sourceGroup.element);
const [sourceParentLocation, from] = tail(sourceLocation);
if (
sequenceEquals(sourceParentLocation, targetParentLocation)
) {
// special case when 'swapping' two views within same grid location
// if a group has one tab - we are essentially moving the 'group'
// which is equivalent to swapping two views in this case
this.gridview.moveView(sourceParentLocation, from, to);
} else {
// source group will become empty so delete the group
const targetGroup = this.doRemoveGroup(sourceGroup, {
skipActive: true,
skipDispose: true,
});
const isFloating = this.floatingGroups.find(
(x) => x.group === sourceGroup
);
// after deleting the group we need to re-evaulate the ref location
const updatedReferenceLocation = getGridLocation(
destinationGroup.element
);
const location = getRelativeLocation(
this.gridview.orientation,
updatedReferenceLocation,
destinationTarget
);
this.doAddGroup(targetGroup, location);
if (!isFloating) {
const sourceLocation = getGridLocation(sourceGroup.element);
const [sourceParentLocation, from] = tail(sourceLocation);
if (
sequenceEquals(
sourceParentLocation,
targetParentLocation
)
) {
// special case when 'swapping' two views within same grid location
// if a group has one tab - we are essentially moving the 'group'
// which is equivalent to swapping two views in this case
this.gridview.moveView(sourceParentLocation, from, to);
}
}
// source group will become empty so delete the group
const targetGroup = this.doRemoveGroup(sourceGroup, {
skipActive: true,
skipDispose: true,
});
// after deleting the group we need to re-evaulate the ref location
const updatedReferenceLocation = getGridLocation(
destinationGroup.element
);
const location = getRelativeLocation(
this.gridview.orientation,
updatedReferenceLocation,
destinationTarget
);
this.doAddGroup(targetGroup, location);
} else {
const groupItem: IDockviewPanel | undefined =
sourceGroup?.model.removePanel(sourceItemId) ||
@ -828,7 +1067,17 @@ export class DockviewComponent
});
}
} else {
this.gridview.removeView(getGridLocation(sourceGroup.element));
const floatingGroup = this.floatingGroups.find(
(x) => x.group === sourceGroup
);
if (floatingGroup) {
floatingGroup.dispose();
} else {
this.gridview.removeView(
getGridLocation(sourceGroup.element)
);
}
const referenceLocation = getGridLocation(
referenceGroup.element
@ -921,7 +1170,7 @@ export class DockviewComponent
private createPanel(
options: AddPanelOptions,
group: DockviewGroupPanel
): IDockviewPanel {
): DockviewPanel {
const contentComponent = options.component;
const tabComponent =
options.tabComponent || this.options.defaultTabComponent;

View File

@ -0,0 +1,37 @@
import { Overlay } from '../dnd/overlay';
import { CompositeDisposable } from '../lifecycle';
import { DockviewGroupPanel, IDockviewGroupPanel } from './dockviewGroupPanel';
export interface IDockviewFloatingGroupPanel {
readonly group: IDockviewGroupPanel;
position(
bounds: Partial<{
top: number;
left: number;
height: number;
width: number;
}>
): void;
}
export class DockviewFloatingGroupPanel
extends CompositeDisposable
implements IDockviewFloatingGroupPanel
{
constructor(readonly group: DockviewGroupPanel, readonly overlay: Overlay) {
super();
this.addDisposables(overlay);
}
position(
bounds: Partial<{
top: number;
left: number;
height: number;
width: number;
}>
): void {
this.overlay.setBounds(bounds);
}
}

View File

@ -1,6 +1,5 @@
import { IFrameworkPart } from '../panel/types';
import { DockviewComponent } from '../dockview/dockviewComponent';
import { GridviewPanelApi } from '../api/gridviewPanelApi';
import {
DockviewGroupPanelModel,
GroupOptions,
@ -9,8 +8,13 @@ import {
} from './dockviewGroupPanelModel';
import { GridviewPanel, IGridviewPanel } from '../gridview/gridviewPanel';
import { IDockviewPanel } from '../dockview/dockviewPanel';
import {
DockviewGroupPanelApi,
DockviewGroupPanelApiImpl,
} from '../api/dockviewGroupPanelApi';
export interface IDockviewGroupPanel extends IGridviewPanel {
export interface IDockviewGroupPanel
extends IGridviewPanel<DockviewGroupPanelApi> {
model: IDockviewGroupPanelModel;
locked: boolean;
readonly size: number;
@ -20,13 +24,11 @@ export interface IDockviewGroupPanel extends IGridviewPanel {
export type IDockviewGroupPanelPublic = IDockviewGroupPanel;
export type DockviewGroupPanelApi = GridviewPanelApi;
export class DockviewGroupPanel
extends GridviewPanel
extends GridviewPanel<DockviewGroupPanelApiImpl>
implements IDockviewGroupPanel
{
private readonly _model: IDockviewGroupPanelModel;
private readonly _model: DockviewGroupPanelModel;
get panels(): IDockviewPanel[] {
return this._model.panels;
@ -40,7 +42,7 @@ export class DockviewGroupPanel
return this._model.size;
}
get model(): IDockviewGroupPanelModel {
get model(): DockviewGroupPanelModel {
return this._model;
}
@ -61,10 +63,17 @@ export class DockviewGroupPanel
id: string,
options: GroupOptions
) {
super(id, 'groupview_default', {
minimumHeight: 100,
minimumWidth: 100,
});
super(
id,
'groupview_default',
{
minimumHeight: 100,
minimumWidth: 100,
},
new DockviewGroupPanelApiImpl(id, accessor)
);
this.api.initialize(this); // cannot use 'this' after after 'super' call
this._model = new DockviewGroupPanelModel(
this.element,
@ -94,7 +103,6 @@ export class DockviewGroupPanel
}
toJSON(): any {
// TODO fix typing
return this.model.toJSON();
}
}

View File

@ -137,6 +137,7 @@ export class DockviewGroupPanelModel
private watermark?: IWatermarkRenderer;
private _isGroupActive = false;
private _locked = false;
private _isFloating = false;
private _rightHeaderActions: IHeaderActionsRenderer | undefined;
private _leftHeaderActions: IHeaderActionsRenderer | undefined;
@ -224,6 +225,24 @@ export class DockviewGroupPanelModel
);
}
get isFloating(): boolean {
return this._isFloating;
}
set isFloating(value: boolean) {
this._isFloating = value;
this.dropTarget.setTargetZones(
value ? ['center'] : ['top', 'bottom', 'left', 'right', 'center']
);
toggleClass(this.container, 'dv-groupview-floating', value);
this.groupPanel.api._onDidFloatingStateChange.fire({
isFloating: this.isFloating,
});
}
constructor(
private readonly container: HTMLElement,
private accessor: DockviewComponent,
@ -233,7 +252,7 @@ export class DockviewGroupPanelModel
) {
super();
this.container.classList.add('groupview');
toggleClass(this.container, 'groupview', true);
this.tabsContainer = new TabsContainer(this.accessor, this.groupPanel);
@ -248,6 +267,10 @@ export class DockviewGroupPanelModel
const data = getPanelData();
if (!data && event.shiftKey && !this.isFloating) {
return false;
}
if (data && data.viewId === this.accessor.id) {
if (data.groupId === this.id) {
if (position === 'center') {
@ -773,6 +796,7 @@ export class DockviewGroupPanelModel
public dispose(): void {
super.dispose();
this.watermark?.element.remove();
this.watermark?.dispose?.();
for (const panel of this.panels) {

View File

@ -8,7 +8,7 @@ import { DockviewGroupPanel } from './dockviewGroupPanel';
import { CompositeDisposable, IDisposable } from '../lifecycle';
import { IPanel, PanelUpdateEvent, Parameters } from '../panel/types';
import { IDockviewPanelModel } from './dockviewPanelModel';
import { IDockviewComponent } from './dockviewComponent';
import { DockviewComponent } from './dockviewComponent';
export interface IDockviewPanel extends IDisposable, IPanel {
readonly view: IDockviewPanelModel;
@ -47,7 +47,7 @@ export class DockviewPanel
constructor(
public readonly id: string,
accessor: IDockviewComponent,
accessor: DockviewComponent,
private readonly containerApi: DockviewApi,
group: DockviewGroupPanel,
readonly view: IDockviewPanelModel
@ -55,7 +55,7 @@ export class DockviewPanel
super();
this._group = group;
this.api = new DockviewPanelApiImpl(this, this._group);
this.api = new DockviewPanelApiImpl(this, this._group, accessor);
this.addDisposables(
this.api.onActiveChange(() => {

View File

@ -8,16 +8,14 @@ import {
IWatermarkRenderer,
DockviewDropTargets,
} from './types';
import {
DockviewGroupPanel,
DockviewGroupPanelApi,
} from './dockviewGroupPanel';
import { DockviewGroupPanel } from './dockviewGroupPanel';
import { ISplitviewStyles, Orientation } from '../splitview/splitview';
import { PanelTransfer } from '../dnd/dataTransfer';
import { IDisposable } from '../lifecycle';
import { Position } from '../dnd/droptarget';
import { IDockviewPanel } from './dockviewPanel';
import { FrameworkFactory } from '../panel/componentFactory';
import { DockviewGroupPanelApi } from '../api/dockviewGroupPanelApi';
export interface IHeaderActionsRenderer extends IDisposable {
readonly element: HTMLElement;
@ -87,6 +85,7 @@ export interface DockviewComponentOptions extends DockviewRenderFunctions {
) => IHeaderActionsRenderer;
singleTabMode?: 'fullwidth' | 'default';
parentElement?: HTMLElement;
disableFloatingGroups?: boolean;
}
export interface PanelOptions {
@ -134,12 +133,32 @@ export function isPanelOptionsWithGroup(
return false;
}
export interface AddPanelOptions
extends Omit<PanelOptions, 'component' | 'tabComponent'> {
type AddPanelFloatingGroupUnion = {
floating:
| {
height?: number;
width?: number;
x?: number;
y?: number;
}
| true;
position: never;
};
type AddPanelPositionUnion = {
floating: false | never;
position: AddPanelPositionOptions;
};
type AddPanelOptionsUnion = AddPanelFloatingGroupUnion | AddPanelPositionUnion;
export type AddPanelOptions = Omit<
PanelOptions,
'component' | 'tabComponent'
> & {
component: string;
tabComponent?: string;
position?: AddPanelPositionOptions;
}
} & Partial<AddPanelOptionsUnion>;
type AddGroupOptionsWithPanel = {
referencePanel: string | IDockviewPanel;

View File

@ -1,5 +1,5 @@
import {
Event,
Event as DockviewEvent,
Emitter,
addDisposableListener,
addDisposableWindowListener,
@ -87,8 +87,8 @@ export function getElementsByTagName(tag: string): HTMLElement[] {
}
export interface IFocusTracker extends IDisposable {
readonly onDidFocus: Event<void>;
readonly onDidBlur: Event<void>;
readonly onDidFocus: DockviewEvent<void>;
readonly onDidBlur: DockviewEvent<void>;
refreshState?(): void;
}
@ -101,10 +101,10 @@ export function trackFocus(element: HTMLElement | Window): IFocusTracker {
*/
class FocusTracker extends CompositeDisposable implements IFocusTracker {
private readonly _onDidFocus = new Emitter<void>();
public readonly onDidFocus: Event<void> = this._onDidFocus.event;
public readonly onDidFocus: DockviewEvent<void> = this._onDidFocus.event;
private readonly _onDidBlur = new Emitter<void>();
public readonly onDidBlur: Event<void> = this._onDidBlur.event;
public readonly onDidBlur: DockviewEvent<void> = this._onDidBlur.event;
private _refreshStateHandler: () => void;
@ -172,3 +172,16 @@ class FocusTracker extends CompositeDisposable implements IFocusTracker {
this._refreshStateHandler();
}
}
// quasi: apparently, but not really; seemingly
const QUASI_PREVENT_DEFAULT_KEY = 'dv-quasiPreventDefault';
// mark an event directly for other listeners to check
export function quasiPreventDefault(event: Event): void {
(event as any)[QUASI_PREVENT_DEFAULT_KEY] = true;
}
// check if this event has been marked
export function quasiDefaultPrevented(event: Event): boolean {
return (event as any)[QUASI_PREVENT_DEFAULT_KEY];
}

View File

@ -74,7 +74,7 @@ export class Emitter<T> implements IDisposable {
static ENABLE_TRACKING = false;
static readonly MEMORY_LEAK_WATCHER = new LeakageMonitor();
static setLeakageMonitorEnabled(isEnabled: boolean) {
static setLeakageMonitorEnabled(isEnabled: boolean): void {
if (isEnabled !== Emitter.ENABLE_TRACKING) {
Emitter.MEMORY_LEAK_WATCHER.clear();
}

View File

@ -149,7 +149,7 @@ export class BranchNode extends CompositeDisposable implements IView {
: true,
};
}),
size: this.size,
size: this.orthogonalSize,
};
this.children = childDescriptors.map((c) => c.node);
@ -235,7 +235,7 @@ export class BranchNode extends CompositeDisposable implements IView {
this._size = orthogonalSize;
this._orthogonalSize = size;
this.splitview.layout(this.size, this.orthogonalSize);
this.splitview.layout(orthogonalSize, size);
}
public addChild(

View File

@ -400,8 +400,9 @@ export class Gridview implements IDisposable {
orientation,
this.proportionalLayout,
this.styles,
orthogonalSize, // <- size - flips at each depth
node.size, // <- orthogonal size - flips at each depth
orthogonalSize, // <- size - flips at each depth
children
);
} else {
@ -455,7 +456,9 @@ export class Gridview implements IDisposable {
this.root.size
);
if (oldRoot.children.length === 1) {
if (oldRoot.children.length === 0) {
// no data so no need to add anything back in
} else if (oldRoot.children.length === 1) {
// can remove one level of redundant branching if there is only a single child
const childReference = oldRoot.children[0];
const child = oldRoot.removeChild(0); // remove to prevent disposal when disposing of unwanted root

View File

@ -219,7 +219,7 @@ export class GridviewComponent
},
});
this.layout(width, height);
this.layout(width, height, true);
queue.forEach((f) => f());

View File

@ -28,8 +28,8 @@ export interface GridviewInitParameters extends PanelInitParameters {
isVisible?: boolean;
}
export interface IGridviewPanel
extends BasePanelViewExported<GridviewPanelApi> {
export interface IGridviewPanel<T extends GridviewPanelApi = GridviewPanelApi>
extends BasePanelViewExported<T> {
readonly minimumWidth: number;
readonly maximumWidth: number;
readonly minimumHeight: number;
@ -38,8 +38,10 @@ export interface IGridviewPanel
readonly snap: boolean;
}
export abstract class GridviewPanel
extends BasePanelView<GridviewPanelApiImpl>
export abstract class GridviewPanel<
T extends GridviewPanelApiImpl = GridviewPanelApiImpl
>
extends BasePanelView<T>
implements IGridPanelComponentView, IGridviewPanel
{
private _evaluatedMinimumWidth = 0;
@ -134,9 +136,10 @@ export abstract class GridviewPanel
maximumWidth?: number;
minimumHeight?: number;
maximumHeight?: number;
}
},
api?: T
) {
super(id, component, new GridviewPanelApiImpl(id));
super(id, component, api ?? <T>new GridviewPanelApiImpl(id));
if (typeof options?.minimumWidth === 'number') {
this._minimumWidth = options.minimumWidth;

View File

@ -1,7 +1,5 @@
export * from './dnd/dataTransfer';
export { watchElementResize } from './dom';
/**
* Events, Emitters and Disposables are very common concepts that most codebases will contain.
* We export them with a 'Dockview' prefix here to prevent accidental use by others.
@ -71,6 +69,10 @@ export {
SplitviewPanelApi,
} from './api/splitviewPanelApi';
export { ExpansionEvent, PaneviewPanelApi } from './api/paneviewPanelApi';
export {
DockviewGroupPanelApi,
DockviewGroupPanelFloatingChangeEvent,
} from './api/dockviewGroupPanelApi';
export {
CommonApi,
SplitviewApi,

View File

@ -5,7 +5,7 @@ export const clamp = (value: number, min: number, max: number): number => {
return Math.min(max, Math.max(value, min));
};
export const sequentialNumberGenerator = () => {
export const sequentialNumberGenerator = (): { next: () => string } => {
let value = 1;
return { next: () => (value++).toString() };
};

View File

@ -1,3 +1,24 @@
.dv-debug {
.split-view-container {
.sash-container {
.sash {
&.enabled {
background-color: black;
}
&.disabled {
background-color: orange;
}
&.maximum {
background-color: green;
}
&.minimum {
background-color: red;
}
}
}
}
}
.split-view-container {
position: relative;
overflow: hidden;
@ -12,22 +33,6 @@
}
}
// debug
// .sash {
// &.enabled {
// background-color: black;
// }
// &.disabled {
// background-color: orange;
// }
// &.maximum {
// background-color: green;
// }
// &.minimum {
// background-color: red;
// }
// }
&.horizontal {
height: 100%;

View File

@ -7,6 +7,7 @@
--dv-drag-over-border-color: white;
--dv-tabs-container-scrollbar-color: #888;
--dv-icon-hover-background-color: rgba(90, 93, 94, 0.31);
--dv-floating-box-shadow: 8px 8px 8px 0px rgba(83, 89, 93, 0.5);
}
@mixin dockview-theme-dark-mixin {
@ -225,3 +226,124 @@
.dockview-theme-dracula {
@include dockview-theme-dracula-mixin();
}
@mixin dockview-design-replit-mixin {
&.dv-dockview {
padding: 3px;
}
.view:has(> .groupview) {
padding: 3px;
}
.dv-resize-container:has(> .groupview) {
border-radius: 8px;
}
.groupview {
overflow: hidden;
border-radius: 10px;
.tabs-and-actions-container {
.tab {
margin: 4px;
border-radius: 8px;
.dockview-svg {
height: 8px;
width: 8px;
}
&:hover {
background-color: #e4e5e6 !important;
}
}
border-bottom: 1px solid rgba(128, 128, 128, 0.35);
}
.content-container {
background-color: #fcfcfc;
}
&.active-group {
border: 1px solid rgba(128, 128, 128, 0.35);
}
&.inactive-group {
border: 1px solid transparent;
}
}
.vertical > .sash-container > .sash {
&::after {
content: '';
height: 4px;
width: 40px;
border-radius: 2px;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: var(--dv-separator-handle-background-color);
position: absolute;
}
&:hover {
&::after {
background-color: var(
--dv-separator-handle-hover-background-color
);
}
}
}
.horizontal > .sash-container > .sash {
&::after {
content: '';
height: 40px;
width: 4px;
border-radius: 2px;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: var(--dv-separator-handle-background-color);
position: absolute;
}
&:hover {
&::after {
background-color: var(
--dv-separator-handle-hover-background-color
);
}
}
}
}
.dockview-theme-replit {
@include dockview-theme-core-mixin();
@include dockview-design-replit-mixin();
//
--dv-group-view-background-color: #ebeced;
//
--dv-tabs-and-actions-container-background-color: #fcfcfc;
//
--dv-activegroup-visiblepanel-tab-background-color: #f0f1f2;
--dv-activegroup-hiddenpanel-tab-background-color: ##fcfcfc;
--dv-inactivegroup-visiblepanel-tab-background-color: #f0f1f2;
--dv-inactivegroup-hiddenpanel-tab-background-color: #fcfcfc;
--dv-tab-divider-color: transparent;
//
--dv-activegroup-visiblepanel-tab-color: rgb(51, 51, 51);
--dv-activegroup-hiddenpanel-tab-color: rgb(51, 51, 51);
--dv-inactivegroup-visiblepanel-tab-color: rgb(51, 51, 51);
--dv-inactivegroup-hiddenpanel-tab-color: rgb(51, 51, 51);
//
--dv-separator-border: transparent;
--dv-paneview-header-border-color: rgb(51, 51, 51);
--dv-background-color: #ebeced;
/////
--dv-separator-handle-background-color: #cfd1d3;
--dv-separator-handle-hover-background-color: #babbbb;
}

View File

@ -28,6 +28,7 @@ import DockviewExternalDnd from '@site/sandboxes/externaldnd-dockview/src/app';
import DockviewResizeContainer from '@site/sandboxes/resizecontainer-dockview/src/app';
import DockviewTabheight from '@site/sandboxes/tabheight-dockview/src/app';
import DockviewWithIFrames from '@site/sandboxes/iframe-dockview/src/app';
import DockviewFloating from '@site/sandboxes/floatinggroup-dockview/src/app';
import { attach as attachDockviewVanilla } from '@site/sandboxes/javascript/vanilla-dockview/src/app';
import { attach as attachSimpleDockview } from '@site/sandboxes/javascript/simple-dockview/src/app';
@ -361,6 +362,33 @@ 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.
Floating groups can be interacted with whilst holding the `shift` key activating the `event.shiftKey` boolean property on `KeyboardEvent` events.
> Float an existing tab by holding `shift` whilst interacting with the tab
<img style={{ width: '60%' }} src={useBaseUrl('/img/float_add.svg')} />
> Move a floating tab by holding `shift` whilst moving the cursor or dragging the empty
> header space
<img style={{ width: '60%' }} src={useBaseUrl('/img/float_move.svg')} />
> Move an entire floating group by holding `shift` whilst dragging the empty header space
<img style={{ width: '60%' }} src={useBaseUrl('/img/float_group.svg')} />
Floating groups can be programatically added through the dockview `api` method `api.addFloatingGroup(...)` and you can check whether
a group is floating via the `group.api.isFloating` property. See examples for full code.
<Container height={600} sandboxId="floatinggroup-dockview">
<DockviewFloating />
</Container>
## Panels
### Add Panel
@ -471,6 +499,21 @@ panel.api.updateParameters({
});
```
### Move panel
You can programatically move a panel using the panel `api`.
```ts
panel.api.moveTo({ group, position, index });
```
An equivalent method for moving groups is avaliable on the group `api`.
```ts
const group = panel.api.group;
group.api.moveTo({ group, position });
```
### Panel Rendering
By default `DockviewReact` only adds to the DOM those panels that are visible,

View File

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

View File

@ -191,7 +191,7 @@ const LeftControls = (props: IDockviewHeaderActionsProps) => {
);
};
const DockviewDemo = () => {
const DockviewDemo = (props: { theme?: string }) => {
const onReady = (event: DockviewReadyEvent) => {
event.api.addPanel({
id: 'panel_1',
@ -249,7 +249,7 @@ const DockviewDemo = () => {
rightHeaderActionsComponent={RightControls}
leftHeaderActionsComponent={LeftControls}
onReady={onReady}
className="dockview-theme-abyss"
className={props.theme || 'dockview-theme-abyss'}
/>
);
};

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,266 @@
import {
DockviewApi,
DockviewGroupPanel,
DockviewReact,
DockviewReadyEvent,
IDockviewHeaderActionsProps,
IDockviewPanelProps,
SerializedDockview,
} from 'dockview';
import * as React from 'react';
import { Icon } from './utils';
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 addPanel(api: DockviewApi) {
api.addPanel({
id: (++panelCount).toString(),
title: `Tab ${panelCount}`,
component: 'default',
});
}
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 Floating Group
</button>
</div>
<div
style={{
flexGrow: 1,
overflow: 'hidden',
}}
>
<DockviewReact
onReady={onReady}
components={components}
watermarkComponent={Watermark}
leftHeaderActionsComponent={LeftComponent}
rightHeaderActionsComponent={RightComponent}
className="dockview-theme-abyss"
/>
</div>
</div>
);
};
const LeftComponent = (props: IDockviewHeaderActionsProps) => {
const onClick = () => {
addPanel(props.containerApi);
};
return (
<div style={{ height: '100%', color: 'white', padding: '0px 4px' }}>
<Icon onClick={onClick} icon={'add'} />
</div>
);
};
const RightComponent = (props: IDockviewHeaderActionsProps) => {
const [floating, setFloating] = React.useState<boolean>(
props.api.isFloating
);
React.useEffect(() => {
const disposable = props.group.api.onDidFloatingStateChange((event) => [
setFloating(event.isFloating),
]);
return () => {
disposable.dispose();
};
}, [props.group.api]);
const onClick = () => {
if (floating) {
const group = props.containerApi.addGroup();
props.group.api.moveTo({ group });
} else {
props.containerApi.addFloatingGroup(props.group);
}
};
return (
<div style={{ height: '100%', color: 'white', padding: '0px 4px' }}>
<Icon
onClick={onClick}
icon={floating ? 'jump_to_element' : 'back_to_tab'}
/>
</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,30 @@
import * as React from 'react';
export const Icon = (props: {
icon: string;
title?: string;
onClick?: (event: React.MouseEvent) => void;
}) => {
return (
<div
title={props.title}
style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
width: '30px',
height: '100%',
fontSize: '18px',
}}
onClick={props.onClick}
>
<span
style={{ fontSize: 'inherit', cursor: 'pointer' }}
className="material-symbols-outlined"
>
{props.icon}
</span>
</div>
);
};

View File

@ -0,0 +1,18 @@
{
"compilerOptions": {
"outDir": "build/dist",
"module": "esnext",
"target": "es5",
"lib": ["es6", "dom"],
"sourceMap": true,
"allowJs": true,
"jsx": "react-jsx",
"moduleResolution": "node",
"rootDir": "src",
"forceConsistentCasingInFileNames": true,
"noImplicitReturns": true,
"noImplicitThis": true,
"noImplicitAny": true,
"strictNullChecks": true
}
}

View File

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

View File

@ -15,7 +15,7 @@ const components = {
},
};
export const App: React.FC = () => {
export const App: React.FC = (props: { theme?: string }) => {
const onReady = (event: DockviewReadyEvent) => {
const panel = event.api.addPanel({
id: 'panel_1',
@ -88,7 +88,7 @@ export const App: React.FC = () => {
<DockviewReact
components={components}
onReady={onReady}
className="dockview-theme-abyss"
className={props.theme || 'dockview-theme-abyss'}
/>
);
};

View File

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

View File

@ -1,5 +1,6 @@
import * as React from 'react';
import './codeSandboxButton.scss';
import { ThemePicker } from './container';
const BASE_SANDBOX_URL =
'https://codesandbox.io/s/github/mathuo/dockview/tree/master/packages/docs/sandboxes';
@ -40,26 +41,29 @@ export const CodeSandboxButton = (props: { id: string }) => {
}, [props.id]);
return (
<span
className="codesandbox-button"
style={{ display: 'flex', alignItems: 'center' }}
>
<span className="codesandbox-button-pretext">{`Open in `}</span>
<a
href={url}
target={'_blank'}
className="codesandbox-button-content"
<>
<ThemePicker />
<span
className="codesandbox-button"
style={{ display: 'flex', alignItems: 'center' }}
>
<span
style={{
fontWeight: 'bold',
paddingRight: '4px',
}}
<span className="codesandbox-button-pretext">{`Open in `}</span>
<a
href={url}
target={'_blank'}
className="codesandbox-button-content"
>
CodeSandbox
</span>
<CloseButton />
</a>
</span>
<span
style={{
fontWeight: 'bold',
paddingRight: '4px',
}}
>
CodeSandbox
</span>
<CloseButton />
</a>
</span>
</>
);
};

View File

@ -69,6 +69,71 @@ const JavascriptIcon = (props: { height: number; width: number }) => {
);
};
const themes = [
'dockview-theme-dark',
'dockview-theme-light',
'dockview-theme-vs',
'dockview-theme-dracula',
'dockview-theme-replit',
];
function useLocalStorageItem(key: string, defaultValue: string): string {
const [item, setItem] = React.useState<string | null>(
localStorage.getItem(key)
);
React.useEffect(() => {
const listener = (event: StorageEvent) => {
setItem(localStorage.getItem(key));
};
window.addEventListener('storage', listener);
setItem(localStorage.getItem(key));
return () => {
window.removeEventListener('storage', listener);
};
}, [key]);
return item === null ? defaultValue : item;
}
export const ThemePicker = () => {
const [theme, setTheme] = React.useState<string>(
localStorage.getItem('dv-theme-class-name') || themes[0]
);
React.useEffect(() => {
localStorage.setItem('dv-theme-class-name', theme);
window.dispatchEvent(new StorageEvent('storage'));
}, [theme]);
return (
<div
style={{
height: '20px',
display: 'flex',
alignItems: 'center',
padding: '0px 0px 0px 4px',
}}
>
<span style={{ paddingRight: '4px' }}>{'Theme: '}</span>
<select
style={{ backgroundColor: 'inherit', color: 'inherit' }}
onChange={(e) => setTheme(e.target.value)}
value={theme}
>
{themes.map((theme) => (
<option key={theme} value={theme}>
{theme}
</option>
))}
</select>
</div>
);
};
export const MultiFrameworkContainer = (props: {
react: React.FC;
typescript: (parent: HTMLElement) => { dispose: () => void };
@ -81,6 +146,11 @@ export const MultiFrameworkContainer = (props: {
const [animation, setAnimation] = React.useState<boolean>(false);
const theme = useLocalStorageItem(
'dv-theme-class-name',
'dockview-theme-abyss'
);
React.useEffect(() => {
setAnimation(true);
@ -139,7 +209,7 @@ export const MultiFrameworkContainer = (props: {
<Spinner />
</div>
)}
{framework === 'React' && <props.react />}
{framework === 'React' && <props.react theme={theme} />}
</div>
<div
style={{

20
packages/docs/static/img/float_add.svg vendored Normal file
View File

@ -0,0 +1,20 @@
<svg width="156" height="76" viewBox="0 0 156 76" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect y="14" width="156" height="62" fill="#000C18"/>
<rect width="156" height="14" fill="#1C1C2A"/>
<rect width="30" height="14" fill="#10192C"/>
<rect x="31" width="30" height="14" fill="#10192C"/>
<rect x="30" width="1" height="14" fill="#2B2B4A"/>
<rect x="61" width="1" height="14" fill="#2B2B4A"/>
<rect x="41" y="54" width="30" height="14" fill="#000C18"/>
<rect x="33" y="5" width="15" height="4" rx="2" fill="#777777"/>
<rect x="2" y="5" width="6" height="4" rx="2" fill="#777777"/>
<rect x="10" y="5" width="18" height="4" rx="2" fill="#777777"/>
<rect x="68.5" y="7.5" width="83" height="60" fill="#000C18" stroke="#2B2B4A"/>
<rect x="100" y="8" width="51" height="14" fill="#1C1C2A"/>
<rect x="83" y="13" width="12" height="4" rx="2" fill="white"/>
<rect x="73" y="13" width="7" height="4" rx="2" fill="white"/>
<rect x="99" y="8" width="1" height="14" fill="#2B2B4A"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M70.3445 8.26544C70.5198 8.14263 70.5104 7.87986 70.3266 7.76998L66.1804 5.29049C65.9614 5.15953 65.6906 5.34916 65.7388 5.59974L66.6506 10.344C66.691 10.5542 66.9347 10.653 67.1101 10.5302L67.7716 10.067C67.8669 10.0002 67.9142 9.88362 67.8922 9.76929L67.6024 8.26123C67.5542 8.01064 67.825 7.82101 68.044 7.95197L69.362 8.74015C69.4619 8.79989 69.5876 8.79538 69.683 8.7286L70.3445 8.26544Z" fill="white"/>
<rect x="69.5" y="9.5" width="4" height="2" stroke="white" stroke-dasharray="0.25 0.25"/>
<path d="M70.9414 4.47461V4.81445H68.9922V4.47461H70.9414ZM70.1484 3.64453V5.71484H69.7871V3.64453H70.1484ZM73.9473 5.28125C73.9473 5.21484 73.9368 5.15625 73.916 5.10547C73.8965 5.05339 73.8613 5.00651 73.8105 4.96484C73.7611 4.92318 73.6921 4.88346 73.6035 4.8457C73.5163 4.80794 73.4056 4.76953 73.2715 4.73047C73.1309 4.6888 73.0039 4.64258 72.8906 4.5918C72.7773 4.53971 72.6803 4.48047 72.5996 4.41406C72.5189 4.34766 72.457 4.27148 72.4141 4.18555C72.3711 4.09961 72.3496 4.0013 72.3496 3.89062C72.3496 3.77995 72.3724 3.67773 72.418 3.58398C72.4635 3.49023 72.5286 3.40885 72.6133 3.33984C72.6992 3.26953 72.8014 3.21484 72.9199 3.17578C73.0384 3.13672 73.1706 3.11719 73.3164 3.11719C73.5299 3.11719 73.7109 3.1582 73.8594 3.24023C74.0091 3.32096 74.123 3.42708 74.2012 3.55859C74.2793 3.6888 74.3184 3.82812 74.3184 3.97656H73.9434C73.9434 3.86979 73.9206 3.77539 73.875 3.69336C73.8294 3.61003 73.7604 3.54492 73.668 3.49805C73.5755 3.44987 73.4583 3.42578 73.3164 3.42578C73.1823 3.42578 73.0716 3.44596 72.9844 3.48633C72.8971 3.52669 72.832 3.58138 72.7891 3.65039C72.7474 3.7194 72.7266 3.79818 72.7266 3.88672C72.7266 3.94661 72.7389 4.0013 72.7637 4.05078C72.7897 4.09896 72.8294 4.14388 72.8828 4.18555C72.9375 4.22721 73.0065 4.26562 73.0898 4.30078C73.1745 4.33594 73.2754 4.36979 73.3926 4.40234C73.554 4.44792 73.6934 4.4987 73.8105 4.55469C73.9277 4.61068 74.0241 4.67383 74.0996 4.74414C74.1764 4.81315 74.2331 4.89193 74.2695 4.98047C74.3073 5.06771 74.3262 5.16667 74.3262 5.27734C74.3262 5.39323 74.3027 5.49805 74.2559 5.5918C74.209 5.68555 74.1419 5.76562 74.0547 5.83203C73.9674 5.89844 73.8626 5.94987 73.7402 5.98633C73.6191 6.02148 73.4837 6.03906 73.334 6.03906C73.2025 6.03906 73.0729 6.02083 72.9453 5.98438C72.819 5.94792 72.7038 5.89323 72.5996 5.82031C72.4967 5.7474 72.4141 5.65755 72.3516 5.55078C72.2904 5.44271 72.2598 5.31771 72.2598 5.17578H72.6348C72.6348 5.27344 72.6536 5.35742 72.6914 5.42773C72.7292 5.49674 72.7806 5.55404 72.8457 5.59961C72.9121 5.64518 72.987 5.67904 73.0703 5.70117C73.1549 5.72201 73.2428 5.73242 73.334 5.73242C73.4655 5.73242 73.5768 5.71419 73.668 5.67773C73.7591 5.64128 73.8281 5.58919 73.875 5.52148C73.9232 5.45378 73.9473 5.3737 73.9473 5.28125ZM76.6641 4.37891V4.68555H75.125V4.37891H76.6641ZM75.1836 3.15625V6H74.8066V3.15625H75.1836ZM76.9922 3.15625V6H76.6172V3.15625H76.9922ZM78.0664 3.15625V6H77.6895V3.15625H78.0664ZM79.1289 3.15625V6H78.752V3.15625H79.1289ZM80.3203 4.43555V4.74414H79.0469V4.43555H80.3203ZM80.5137 3.15625V3.46484H79.0469V3.15625H80.5137ZM82.0527 3.15625V6H81.6816V3.15625H82.0527ZM82.9668 3.15625V3.46484H80.7695V3.15625H82.9668ZM84.6797 3.15625V6H84.3027V3.15625H84.6797ZM86.3965 3.15625L85.2148 4.48242L84.5508 5.17188L84.4883 4.76953L84.9883 4.21875L85.9434 3.15625H86.3965ZM86.0332 6L84.9805 4.61328L85.2051 4.31445L86.4824 6H86.0332ZM88.6211 5.69336V6H87.1152V5.69336H88.6211ZM87.1914 3.15625V6H86.8145V3.15625H87.1914ZM88.4219 4.37891V4.68555H87.1152V4.37891H88.4219ZM88.6016 3.15625V3.46484H87.1152V3.15625H88.6016ZM89.2188 3.15625L89.957 4.58398L90.6973 3.15625H91.125L90.1445 4.9375V6H89.7676V4.9375L88.7871 3.15625H89.2188Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 4.6 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 18 KiB

29
packages/docs/static/img/float_move.svg vendored Normal file
View File

@ -0,0 +1,29 @@
<svg width="156" height="76" viewBox="0 0 156 76" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect y="14" width="156" height="62" fill="#000C18"/>
<rect width="156" height="14" fill="#1C1C2A"/>
<rect width="30" height="14" fill="#10192C"/>
<rect x="31" width="30" height="14" fill="#10192C"/>
<rect x="30" width="1" height="14" fill="#2B2B4A"/>
<rect x="61" width="1" height="14" fill="#2B2B4A"/>
<rect x="41" y="54" width="30" height="14" fill="#000C18"/>
<rect x="33" y="5" width="15" height="4" rx="2" fill="#777777"/>
<rect x="2" y="5" width="6" height="4" rx="2" fill="#777777"/>
<rect x="10" y="5" width="18" height="4" rx="2" fill="#777777"/>
<g opacity="0.3">
<rect x="28.5" y="20.5" width="83" height="37" fill="#000C18" stroke="#2B2B4A"/>
<rect x="60" y="21" width="51" height="14" fill="#1C1C2A"/>
<rect x="43" y="26" width="12" height="4" rx="2" fill="white"/>
<rect x="33" y="26" width="7" height="4" rx="2" fill="white"/>
<rect x="59" y="21" width="1" height="14" fill="#2B2B4A"/>
</g>
<rect x="11.5" y="29.5" width="83" height="38" fill="#000C18" stroke="#2B2B4A"/>
<rect x="43" y="30" width="51" height="14" fill="#1C1C2A"/>
<rect x="26" y="35" width="12" height="4" rx="2" fill="white"/>
<rect x="16" y="35" width="7" height="4" rx="2" fill="white"/>
<rect x="42" y="30" width="1" height="14" fill="#2B2B4A"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M70.3445 58.2654C70.5198 58.1426 70.5104 57.8799 70.3266 57.77L66.1804 55.2905C65.9614 55.1595 65.6906 55.3492 65.7388 55.5997L66.6506 60.344C66.691 60.5542 66.9347 60.653 67.1101 60.5302L67.7716 60.067C67.8669 60.0002 67.9142 59.8836 67.8922 59.7693L67.6024 58.2612C67.5542 58.0106 67.825 57.821 68.044 57.952L69.362 58.7401C69.4619 58.7999 69.5876 58.7954 69.683 58.7286L70.3445 58.2654Z" fill="white"/>
<rect x="69.5" y="59.5" width="4" height="2" stroke="white" stroke-dasharray="0.25 0.25"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M77.3445 37.2654C77.5198 37.1426 77.5104 36.8799 77.3266 36.77L73.1804 34.2905C72.9614 34.1595 72.6906 34.3492 72.7388 34.5997L73.6506 39.344C73.691 39.5542 73.9347 39.653 74.1101 39.5302L74.7716 39.067C74.8669 39.0002 74.9142 38.8836 74.8922 38.7693L74.6024 37.2612C74.5542 37.0106 74.825 36.821 75.044 36.952L76.362 37.7401C76.4619 37.7999 76.5876 37.7954 76.683 37.7286L77.3445 37.2654Z" fill="white"/>
<rect x="76.5" y="38.5" width="4" height="2" stroke="white" stroke-dasharray="0.25 0.25"/>
<path d="M70.9414 54.4746V54.8145H68.9922V54.4746H70.9414ZM70.1484 53.6445V55.7148H69.7871V53.6445H70.1484ZM73.9473 55.2812C73.9473 55.2148 73.9368 55.1562 73.916 55.1055C73.8965 55.0534 73.8613 55.0065 73.8105 54.9648C73.7611 54.9232 73.6921 54.8835 73.6035 54.8457C73.5163 54.8079 73.4056 54.7695 73.2715 54.7305C73.1309 54.6888 73.0039 54.6426 72.8906 54.5918C72.7773 54.5397 72.6803 54.4805 72.5996 54.4141C72.5189 54.3477 72.457 54.2715 72.4141 54.1855C72.3711 54.0996 72.3496 54.0013 72.3496 53.8906C72.3496 53.7799 72.3724 53.6777 72.418 53.584C72.4635 53.4902 72.5286 53.4089 72.6133 53.3398C72.6992 53.2695 72.8014 53.2148 72.9199 53.1758C73.0384 53.1367 73.1706 53.1172 73.3164 53.1172C73.5299 53.1172 73.7109 53.1582 73.8594 53.2402C74.0091 53.321 74.123 53.4271 74.2012 53.5586C74.2793 53.6888 74.3184 53.8281 74.3184 53.9766H73.9434C73.9434 53.8698 73.9206 53.7754 73.875 53.6934C73.8294 53.61 73.7604 53.5449 73.668 53.498C73.5755 53.4499 73.4583 53.4258 73.3164 53.4258C73.1823 53.4258 73.0716 53.446 72.9844 53.4863C72.8971 53.5267 72.832 53.5814 72.7891 53.6504C72.7474 53.7194 72.7266 53.7982 72.7266 53.8867C72.7266 53.9466 72.7389 54.0013 72.7637 54.0508C72.7897 54.099 72.8294 54.1439 72.8828 54.1855C72.9375 54.2272 73.0065 54.2656 73.0898 54.3008C73.1745 54.3359 73.2754 54.3698 73.3926 54.4023C73.554 54.4479 73.6934 54.4987 73.8105 54.5547C73.9277 54.6107 74.0241 54.6738 74.0996 54.7441C74.1764 54.8132 74.2331 54.8919 74.2695 54.9805C74.3073 55.0677 74.3262 55.1667 74.3262 55.2773C74.3262 55.3932 74.3027 55.498 74.2559 55.5918C74.209 55.6855 74.1419 55.7656 74.0547 55.832C73.9674 55.8984 73.8626 55.9499 73.7402 55.9863C73.6191 56.0215 73.4837 56.0391 73.334 56.0391C73.2025 56.0391 73.0729 56.0208 72.9453 55.9844C72.819 55.9479 72.7038 55.8932 72.5996 55.8203C72.4967 55.7474 72.4141 55.6576 72.3516 55.5508C72.2904 55.4427 72.2598 55.3177 72.2598 55.1758H72.6348C72.6348 55.2734 72.6536 55.3574 72.6914 55.4277C72.7292 55.4967 72.7806 55.554 72.8457 55.5996C72.9121 55.6452 72.987 55.679 73.0703 55.7012C73.1549 55.722 73.2428 55.7324 73.334 55.7324C73.4655 55.7324 73.5768 55.7142 73.668 55.6777C73.7591 55.6413 73.8281 55.5892 73.875 55.5215C73.9232 55.4538 73.9473 55.3737 73.9473 55.2812ZM76.6641 54.3789V54.6855H75.125V54.3789H76.6641ZM75.1836 53.1562V56H74.8066V53.1562H75.1836ZM76.9922 53.1562V56H76.6172V53.1562H76.9922ZM78.0664 53.1562V56H77.6895V53.1562H78.0664ZM79.1289 53.1562V56H78.752V53.1562H79.1289ZM80.3203 54.4355V54.7441H79.0469V54.4355H80.3203ZM80.5137 53.1562V53.4648H79.0469V53.1562H80.5137ZM82.0527 53.1562V56H81.6816V53.1562H82.0527ZM82.9668 53.1562V53.4648H80.7695V53.1562H82.9668ZM84.6797 53.1562V56H84.3027V53.1562H84.6797ZM86.3965 53.1562L85.2148 54.4824L84.5508 55.1719L84.4883 54.7695L84.9883 54.2188L85.9434 53.1562H86.3965ZM86.0332 56L84.9805 54.6133L85.2051 54.3145L86.4824 56H86.0332ZM88.6211 55.6934V56H87.1152V55.6934H88.6211ZM87.1914 53.1562V56H86.8145V53.1562H87.1914ZM88.4219 54.3789V54.6855H87.1152V54.3789H88.4219ZM88.6016 53.1562V53.4648H87.1152V53.1562H88.6016ZM89.2188 53.1562L89.957 54.584L90.6973 53.1562H91.125L90.1445 54.9375V56H89.7676V54.9375L88.7871 53.1562H89.2188Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 5.5 KiB