Merge pull request #87 from mathuo/84-add-ability-to-have-fixed-panel-with-no-tab-in-dockviewreact

84 add ability to have fixed panel with no tab in dockviewreact
This commit is contained in:
mathuo 2022-05-04 20:43:17 +01:00 committed by GitHub
commit d7d6dc2635
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 508 additions and 141 deletions

1
.gitignore vendored
View File

@ -11,3 +11,4 @@ storybook-static/
.rollup.cache/ .rollup.cache/
test-report.xml test-report.xml
*.code-workspace *.code-workspace
yarn-error.log

View File

@ -25,7 +25,8 @@
"prepack": "npm run rebuild && npm run test", "prepack": "npm run rebuild && npm run test",
"rebuild": "npm run clean && npm run build", "rebuild": "npm run clean && npm run build",
"test": "cross-env ../../node_modules/.bin/jest --selectProjects dockview", "test": "cross-env ../../node_modules/.bin/jest --selectProjects dockview",
"test:cov": "cross-env ../../node_modules/.bin/jest --selectProjects dockview --coverage" "test:cov": "cross-env ../../node_modules/.bin/jest --selectProjects dockview --coverage",
"dev-publish": "node ./scripts/publishExperimental.js"
}, },
"files": [ "files": [
"dist", "dist",

View File

@ -0,0 +1,63 @@
const cp = require('child_process');
const fs = require('fs-extra');
const path = require('path');
const rootDir = path.join(__dirname, '..');
const publishDir = path.join(rootDir, '__publish__');
cp.execSync('npm run clean', { cwd: rootDir, stdio: 'inherit' });
cp.execSync('npm run test', { cwd: __dirname, stdio: 'inherit' });
cp.execSync('npm run build', { cwd: rootDir, stdio: 'inherit' });
if (fs.existsSync(publishDir)) {
fs.removeSync(publishDir);
}
fs.mkdirSync(publishDir);
if (!fs.existsSync(path.join(publishDir, 'dist'))) {
fs.mkdirSync(path.join(publishDir, 'dist'));
}
const package = JSON.parse(
fs.readFileSync(path.join(rootDir, 'package.json')).toString()
);
for (const file of package.files) {
fs.copySync(path.join(rootDir, file), path.join(publishDir, file));
}
const result = cp
.execSync('git rev-parse --short HEAD', {
cwd: rootDir,
})
.toString()
.replace(/\n/g, '');
function formatDate() {
const date = new Date();
function pad(value) {
if (value.toString().length === 1) {
return `0${value}`;
}
return value;
}
return `${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(
date.getDate()
)}`;
}
package.version = `0.0.0-experimental-${result}-${formatDate()}`;
package.scripts = {};
fs.writeFileSync(
path.join(publishDir, 'package.json'),
JSON.stringify(package, null, 4)
);
const command = 'npm publish --tag experimental';
cp.execSync(command, { cwd: publishDir, stdio: 'inherit' });
fs.removeSync(publishDir);

View File

@ -20,6 +20,11 @@ describe('droptarget', () => {
beforeEach(() => { beforeEach(() => {
element = document.createElement('div'); element = document.createElement('div');
jest.spyOn(element, 'clientHeight', 'get').mockImplementation(
() => 100
);
jest.spyOn(element, 'clientWidth', 'get').mockImplementation(() => 200);
}); });
test('non-directional', () => { test('non-directional', () => {

View File

@ -0,0 +1,95 @@
import {
DefaultDeserializer,
PanelDeserializerOptions,
} from '../../dockview/deserializer';
import { DockviewComponent } from '../../dockview/dockviewComponent';
import { Groupview } from '../../groupview/groupview';
import { GroupviewPanel } from '../../groupview/groupviewPanel';
describe('deserializer', () => {
test('fromJSON', () => {
const openPanel = jest.fn();
const model = jest.fn<Groupview, []>(() => {
const result: Partial<Groupview> = {
openPanel,
};
return result as Groupview;
});
const panel1 = jest.fn();
const panel2 = jest.fn();
const groupMock = jest.fn<GroupviewPanel, []>(() => {
const result: Partial<GroupviewPanel> = {
model: new model(),
panels: <any>[panel1, panel2],
activePanel: null,
};
return result as GroupviewPanel;
});
const group = new groupMock();
const createGroup = jest.fn().mockReturnValue(new groupMock());
const dockviewComponentMock = jest.fn<DockviewComponent, []>(() => {
const value: Partial<DockviewComponent> = {
createGroup,
};
return <DockviewComponent>value;
});
const createPanel = jest
.fn()
.mockImplementation((child) => ({ id: child }));
const panelDeserializer: PanelDeserializerOptions = {
createPanel,
};
const dockviewComponent = new dockviewComponentMock();
const cut = new DefaultDeserializer(
dockviewComponent,
panelDeserializer
);
cut.fromJSON({
type: 'leaf',
size: 100,
visible: true,
data: {
hideHeader: true,
locked: true,
id: 'id',
views: ['view1', 'view2'],
activeView: 'view2',
},
});
expect(createGroup).toBeCalledWith({
id: 'id',
locked: true,
hideHeader: true,
});
expect(createGroup).toBeCalledTimes(1);
expect(createPanel).toBeCalledWith('view1', group);
expect(createPanel).toBeCalledWith('view2', group);
expect(createPanel).toBeCalledTimes(2);
expect(openPanel).toBeCalledWith(
{ id: 'view1' },
{ skipSetActive: true }
);
expect(openPanel).toBeCalledWith(
{ id: 'view2' },
{ skipSetActive: false }
);
expect(openPanel).toBeCalledWith(panel2);
expect(openPanel).toBeCalledTimes(3);
});
});

View File

@ -619,7 +619,7 @@ describe('dockviewComponent', () => {
data: { data: {
views: ['panel2', 'panel3'], views: ['panel2', 'panel3'],
id: 'group-2', id: 'group-2',
activeView: 'panel2', activeView: 'panel3',
}, },
size: 500, size: 500,
}, },

View File

@ -1,19 +1,26 @@
import { DockviewComponent } from '../..'; import { DockviewComponent } from '../../dockview/dockviewComponent';
import { DockviewApi } from '../../api/component.api'; import { DockviewApi } from '../../api/component.api';
import { IGroupPanelView } from '../../dockview/defaultGroupPanelView'; import { IGroupPanelView } from '../../dockview/defaultGroupPanelView';
import { DockviewGroupPanel } from '../../dockview/dockviewGroupPanel'; import { DockviewGroupPanel } from '../../dockview/dockviewGroupPanel';
import { GroupviewPanel } from '../../groupview/groupviewPanel';
describe('dockviewGroupPanel', () => { describe('dockviewGroupPanel', () => {
test('update title', () => { test('update title', () => {
const dockviewApiMock = jest.fn<DockviewApi, []>(() => { const dockviewApiMock = jest.fn<DockviewApi, []>(() => {
return {} as any; return {
onDidActiveChange: jest.fn(),
} as any;
}); });
const accessorMock = jest.fn<DockviewComponent, []>(() => { const accessorMock = jest.fn<DockviewComponent, []>(() => {
return {} as any; return {} as any;
}); });
const groupMock = jest.fn<GroupviewPanel, []>(() => {
return {} as any;
});
const api = new dockviewApiMock(); const api = new dockviewApiMock();
const accessor = new accessorMock(); const accessor = new accessorMock();
const cut = new DockviewGroupPanel('fake-id', accessor, api); const group = new groupMock();
const cut = new DockviewGroupPanel('fake-id', accessor, api, group);
let latestTitle: string | undefined = undefined; let latestTitle: string | undefined = undefined;
@ -41,10 +48,14 @@ describe('dockviewGroupPanel', () => {
const accessorMock = jest.fn<DockviewComponent, []>(() => { const accessorMock = jest.fn<DockviewComponent, []>(() => {
return {} as any; return {} as any;
}); });
const groupMock = jest.fn<GroupviewPanel, []>(() => {
return {} as any;
});
const api = new dockviewApiMock(); const api = new dockviewApiMock();
const accessor = new accessorMock(); const accessor = new accessorMock();
const group = new groupMock();
const cut = new DockviewGroupPanel('fake-id', accessor, api); const cut = new DockviewGroupPanel('fake-id', accessor, api, group);
let latestSuppressClosable: boolean | undefined = undefined; let latestSuppressClosable: boolean | undefined = undefined;
@ -77,10 +88,14 @@ describe('dockviewGroupPanel', () => {
const accessorMock = jest.fn<DockviewComponent, []>(() => { const accessorMock = jest.fn<DockviewComponent, []>(() => {
return {} as any; return {} as any;
}); });
const groupMock = jest.fn<GroupviewPanel, []>(() => {
return {} as any;
});
const api = new dockviewApiMock(); const api = new dockviewApiMock();
const accessor = new accessorMock(); const accessor = new accessorMock();
const group = new groupMock();
const cut = new DockviewGroupPanel('fake-id', accessor, api); const cut = new DockviewGroupPanel('fake-id', accessor, api, group);
const viewMock = jest.fn<IGroupPanelView, []>(() => { const viewMock = jest.fn<IGroupPanelView, []>(() => {
return { return {

View File

@ -15,7 +15,11 @@ import {
} from '../../groupview/types'; } from '../../groupview/types';
import { PanelUpdateEvent } from '../../panel/types'; import { PanelUpdateEvent } from '../../panel/types';
import { GroupviewPanel } from '../../groupview/groupviewPanel'; import { GroupviewPanel } from '../../groupview/groupviewPanel';
import { GroupChangeKind2, GroupOptions } from '../../groupview/groupview'; import {
GroupChangeKind2,
GroupOptions,
Groupview,
} from '../../groupview/groupview';
import { DockviewPanelApi } from '../../api/groupPanelApi'; import { DockviewPanelApi } from '../../api/groupPanelApi';
import { import {
DefaultGroupPanelView, DefaultGroupPanelView,
@ -463,4 +467,59 @@ describe('groupview', () => {
const panel = cut.addPanel({ id: 'id', component: 'component' }); const panel = cut.addPanel({ id: 'id', component: 'component' });
disposable.dispose(); disposable.dispose();
}); });
test('toJSON() default', () => {
const dockviewComponent = new DockviewComponent(
document.createElement('div'),
{
components: {
component: TestContentPart,
},
}
);
const cut = new Groupview(
document.createElement('div'),
dockviewComponent,
'id',
{},
null
);
expect(cut.toJSON()).toEqual({
views: [],
activeView: undefined,
id: 'id',
});
});
test('toJSON() locked and hideHeader', () => {
const dockviewComponent = new DockviewComponent(
document.createElement('div'),
{
components: {
component: TestContentPart,
},
}
);
const cut = new Groupview(
document.createElement('div'),
dockviewComponent,
'id',
{},
null
);
cut.locked = true;
cut.header.hidden = true;
expect(cut.toJSON()).toEqual({
views: [],
activeView: undefined,
id: 'id',
locked: true,
hideHeader: true,
});
});
}); });

View File

@ -17,7 +17,7 @@ export interface SuppressClosableEvent {
* because it belongs to a groupview * because it belongs to a groupview
*/ */
export interface DockviewPanelApi extends Omit<GridviewPanelApi, 'setVisible'> { export interface DockviewPanelApi extends Omit<GridviewPanelApi, 'setVisible'> {
readonly group: GroupviewPanel | undefined; readonly group: GroupviewPanel;
readonly isGroupActive: boolean; readonly isGroupActive: boolean;
readonly title: string; readonly title: string;
readonly suppressClosable: boolean; readonly suppressClosable: boolean;
@ -29,7 +29,7 @@ export class DockviewPanelApiImpl
extends GridviewPanelApiImpl extends GridviewPanelApiImpl
implements DockviewPanelApi implements DockviewPanelApi
{ {
private _group: GroupviewPanel | undefined; private _group: GroupviewPanel;
readonly _onDidTitleChange = new Emitter<TitleEvent>(); readonly _onDidTitleChange = new Emitter<TitleEvent>();
readonly onDidTitleChange = this._onDidTitleChange.event; readonly onDidTitleChange = this._onDidTitleChange.event;
@ -60,7 +60,7 @@ export class DockviewPanelApiImpl
return !!this.group?.isActive; return !!this.group?.isActive;
} }
set group(value: GroupviewPanel | undefined) { set group(value: GroupviewPanel) {
const isOldGroupActive = this.isGroupActive; const isOldGroupActive = this.isGroupActive;
this._group = value; this._group = value;
@ -78,13 +78,13 @@ export class DockviewPanelApiImpl
} }
} }
get group(): GroupviewPanel | undefined { get group(): GroupviewPanel {
return this._group; return this._group;
} }
constructor(private panel: IGroupPanel, group: GroupviewPanel | undefined) { constructor(private panel: IGroupPanel, group: GroupviewPanel) {
super(panel.id); super(panel.id);
this.group = group; this._group = group;
this.addDisposables( this.addDisposables(
this.disposable, this.disposable,

View File

@ -26,7 +26,9 @@ function isBooleanValue(
return typeof canDisplayOverlay === 'boolean'; return typeof canDisplayOverlay === 'boolean';
} }
export type CanDisplayOverlay = boolean | ((dragEvent: DragEvent) => boolean); export type CanDisplayOverlay =
| boolean
| ((dragEvent: DragEvent, state: Quadrant | null) => boolean);
export class Droptarget extends CompositeDisposable { export class Droptarget extends CompositeDisposable {
private target: HTMLElement | undefined; private target: HTMLElement | undefined;
@ -62,11 +64,29 @@ export class Droptarget extends CompositeDisposable {
new DragAndDropObserver(this.element, { new DragAndDropObserver(this.element, {
onDragEnter: () => undefined, onDragEnter: () => undefined,
onDragOver: (e) => { onDragOver: (e) => {
const width = this.element.clientWidth;
const height = this.element.clientHeight;
if (width === 0 || height === 0) {
return; // avoid div!0
}
const x = e.offsetX;
const y = e.offsetY;
const xp = (100 * x) / width;
const yp = (100 * y) / height;
const quadrant = this.calculateQuadrant(
this.options.validOverlays,
xp,
yp
);
if (isBooleanValue(this.options.canDisplayOverlay)) { if (isBooleanValue(this.options.canDisplayOverlay)) {
if (!this.options.canDisplayOverlay) { if (!this.options.canDisplayOverlay) {
return; return;
} }
} else if (!this.options.canDisplayOverlay(e)) { } else if (!this.options.canDisplayOverlay(e, quadrant)) {
return; return;
} }
@ -90,24 +110,6 @@ export class Droptarget extends CompositeDisposable {
return; return;
} }
const width = this.target.clientWidth;
const height = this.target.clientHeight;
if (width === 0 || height === 0) {
return; // avoid div!0
}
const x = e.offsetX;
const y = e.offsetY;
const xp = (100 * x) / width;
const yp = (100 * y) / height;
const quadrant = this.calculateQuadrant(
this.options.validOverlays,
xp,
yp
);
const isSmallX = width < 100; const isSmallX = width < 100;
const isSmallY = height < 100; const isSmallY = height < 100;

View File

@ -4,36 +4,52 @@ import {
IViewDeserializer, IViewDeserializer,
} from '../gridview/gridview'; } from '../gridview/gridview';
import { GroupviewPanelState, IGroupPanel } from '../groupview/groupPanel'; import { GroupviewPanelState, IGroupPanel } from '../groupview/groupPanel';
import { GroupPanelViewState } from '../groupview/groupview';
import { GroupviewPanel } from '../groupview/groupviewPanel';
import { DockviewComponent } from './dockviewComponent'; import { DockviewComponent } from './dockviewComponent';
export interface IPanelDeserializer { export interface IPanelDeserializer {
fromJSON(panelData: GroupviewPanelState): IGroupPanel; fromJSON(
panelData: GroupviewPanelState,
group: GroupviewPanel
): IGroupPanel;
}
export interface PanelDeserializerOptions {
createPanel: (id: string, group: GroupviewPanel) => IGroupPanel;
} }
export class DefaultDeserializer implements IViewDeserializer { export class DefaultDeserializer implements IViewDeserializer {
constructor( constructor(
private readonly layout: DockviewComponent, private readonly layout: DockviewComponent,
private panelDeserializer: { private panelDeserializer: PanelDeserializerOptions
createPanel: (id: string) => IGroupPanel;
}
) {} ) {}
public fromJSON(node: ISerializedLeafNode): IGridView { public fromJSON(node: ISerializedLeafNode<GroupPanelViewState>): IGridView {
const children = node.data.views; const data = node.data;
const active = node.data.activeView; const children = data.views;
const active = data.activeView;
const panels: IGroupPanel[] = []; const group = this.layout.createGroup({
id: data.id,
locked: !!data.locked,
hideHeader: !!data.hideHeader,
});
for (const child of children) { for (const child of children) {
const panel = this.panelDeserializer.createPanel(child); const panel = this.panelDeserializer.createPanel(child, group);
panels.push(panel); const isActive = typeof active === 'string' && active === panel.id;
group.model.openPanel(panel, {
skipSetActive: !isActive,
});
} }
return this.layout.createGroup({ if (!group.activePanel && group.panels.length > 0) {
panels, group.model.openPanel(group.panels[group.panels.length - 1]);
activePanel: panels.find((p) => p.id === active), }
id: node.data.id,
}); return group;
} }
} }

View File

@ -63,7 +63,7 @@ export interface SerializedDockview {
} }
export type DockviewComponentUpdateOptions = Pick< export type DockviewComponentUpdateOptions = Pick<
DockviewComponentOptions, DockviewComponentOptions,
| 'orientation' | 'orientation'
| 'components' | 'components'
| 'frameworkComponents' | 'frameworkComponents'
@ -160,7 +160,7 @@ export class DockviewComponent
} }
get panels(): IGroupPanel[] { get panels(): IGroupPanel[] {
return this.groups.flatMap((group) => group.model.panels); return this.groups.flatMap((group) => group.panels);
} }
get deserializer(): IPanelDeserializer | undefined { get deserializer(): IPanelDeserializer | undefined {
@ -182,13 +182,13 @@ export class DockviewComponent
return undefined; return undefined;
} }
return activeGroup.model.activePanel; return activeGroup.activePanel;
} }
set tabHeight(height: number | undefined) { set tabHeight(height: number | undefined) {
this.options.tabHeight = height; this.options.tabHeight = height;
this._groups.forEach((value) => { this._groups.forEach((value) => {
value.value.model.tabHeight = height; value.value.model.header.height = height;
}); });
} }
@ -263,9 +263,6 @@ export class DockviewComponent
} }
setActivePanel(panel: IGroupPanel): void { setActivePanel(panel: IGroupPanel): void {
if (!panel.group) {
throw new Error(`Panel ${panel.id} has no associated group`);
}
this.doSetGroupActive(panel.group); this.doSetGroupActive(panel.group);
panel.group.model.openPanel(panel); panel.group.model.openPanel(panel);
} }
@ -280,9 +277,9 @@ export class DockviewComponent
if (options.includePanel && options.group) { if (options.includePanel && options.group) {
if ( if (
options.group.model.activePanel !== options.group.activePanel !==
options.group.model.panels[ options.group.panels[
options.group.model.panels.length - 1 options.group.panels.length - 1
] ]
) { ) {
options.group.model.moveToNext({ suppressRoll: true }); options.group.model.moveToNext({ suppressRoll: true });
@ -291,7 +288,7 @@ export class DockviewComponent
} }
const location = getGridLocation(options.group.element); const location = getGridLocation(options.group.element);
const next = this.gridview.next(location)?.view as GroupviewPanel; const next = <GroupviewPanel>this.gridview.next(location)?.view
this.doSetGroupActive(next); this.doSetGroupActive(next);
} }
@ -305,8 +302,8 @@ export class DockviewComponent
if (options.includePanel && options.group) { if (options.includePanel && options.group) {
if ( if (
options.group.model.activePanel !== options.group.activePanel !==
options.group.model.panels[0] options.group.panels[0]
) { ) {
options.group.model.moveToPrevious({ suppressRoll: true }); options.group.model.moveToPrevious({ suppressRoll: true });
return; return;
@ -367,9 +364,9 @@ export class DockviewComponent
this.gridview.deserialize( this.gridview.deserialize(
grid, grid,
new DefaultDeserializer(this, { new DefaultDeserializer(this, {
createPanel: (id) => { createPanel: (id, group) => {
const panelData = panels[id]; const panelData = panels[id];
return this.deserializer!.fromJSON(panelData); return this.deserializer!.fromJSON(panelData, group);
}, },
}) })
); );
@ -411,8 +408,6 @@ export class DockviewComponent
throw new Error(`panel with id ${options.id} already exists`); throw new Error(`panel with id ${options.id} already exists`);
} }
const panel = this.createPanel(options);
let referenceGroup: GroupviewPanel | undefined; let referenceGroup: GroupviewPanel | undefined;
if (options.position?.referencePanel) { if (options.position?.referencePanel) {
@ -431,9 +426,12 @@ export class DockviewComponent
referenceGroup = this.activeGroup; referenceGroup = this.activeGroup;
} }
let panel: IGroupPanel
if (referenceGroup) { if (referenceGroup) {
const target = toTarget(options.position?.direction || 'within'); const target = toTarget(options.position?.direction || 'within');
if (target === Position.Center) { if (target === Position.Center) {
panel = this.createPanel(options, referenceGroup)
referenceGroup.model.openPanel(panel); referenceGroup.model.openPanel(panel);
} else { } else {
const location = getGridLocation(referenceGroup.element); const location = getGridLocation(referenceGroup.element);
@ -442,10 +440,14 @@ export class DockviewComponent
location, location,
target target
); );
this.addPanelToNewGroup(panel, relativeLocation); const group = this.createGroupAtLocation(relativeLocation);
panel = this.createPanel(options, group)
group.model.openPanel(panel);
} }
} else { } else {
this.addPanelToNewGroup(panel); const group = this.createGroupAtLocation();
panel = this.createPanel(options, group);
group.model.openPanel(panel);
} }
return panel; return panel;
@ -474,7 +476,7 @@ export class DockviewComponent
if ( if (
!retainGroupForWatermark && !retainGroupForWatermark &&
group.model.size === 0 && group.size === 0 &&
options.removeEmptyGroup options.removeEmptyGroup
) { ) {
this.removeGroup(group); this.removeGroup(group);
@ -532,7 +534,7 @@ export class DockviewComponent
} }
removeGroup(group: GroupviewPanel, skipActive = false): void { removeGroup(group: GroupviewPanel, skipActive = false): void {
const panels = [...group.model.panels]; // reassign since group panels will mutate const panels = [...group.panels]; // reassign since group panels will mutate
for (const panel of panels) { for (const panel of panels) {
this.removePanel(panel, { this.removePanel(panel, {
@ -577,7 +579,7 @@ export class DockviewComponent
target target
); );
if (sourceGroup && sourceGroup.model.size < 2) { if (sourceGroup && sourceGroup.size < 2) {
const [targetParentLocation, to] = tail(targetLocation); const [targetParentLocation, to] = tail(targetLocation);
const sourceLocation = getGridLocation(sourceGroup.element); const sourceLocation = getGridLocation(sourceGroup.element);
const [sourceParentLocation, from] = tail(sourceLocation); const [sourceParentLocation, from] = tail(sourceLocation);
@ -622,7 +624,8 @@ export class DockviewComponent
target target
); );
this.addPanelToNewGroup(groupItem, dropLocation); const group = this.createGroupAtLocation( dropLocation);
group.model.openPanel(groupItem);
} }
} }
} }
@ -634,9 +637,9 @@ export class DockviewComponent
const isGroupAlreadyFocused = this._activeGroup === group; const isGroupAlreadyFocused = this._activeGroup === group;
super.doSetGroupActive(group, skipFocus); super.doSetGroupActive(group, skipFocus);
if (!isGroupAlreadyFocused && this._activeGroup?.model.activePanel) { if (!isGroupAlreadyFocused && this._activeGroup?.activePanel) {
this._onDidActivePanelChange.fire( this._onDidActivePanelChange.fire(
this._activeGroup?.model.activePanel this._activeGroup?.activePanel
); );
} }
} }
@ -704,19 +707,19 @@ export class DockviewComponent
view.initialize(); view.initialize();
if (typeof this.options.tabHeight === 'number') { if (typeof this.options.tabHeight === 'number') {
view.model.tabHeight = this.options.tabHeight; view.model.header.height = this.options.tabHeight;
} }
return view; return view;
} }
private createPanel(options: AddPanelOptions): IGroupPanel { private createPanel(options: AddPanelOptions, group: GroupviewPanel): IGroupPanel {
const view = new DefaultGroupPanelView({ const view = new DefaultGroupPanelView({
content: this.createContentComponent(options.id, options.component), content: this.createContentComponent(options.id, options.component),
tab: this.createTabComponent(options.id, options.tabComponent), tab: this.createTabComponent(options.id, options.tabComponent),
}); });
const panel = new DockviewGroupPanel(options.id, this, this._api); const panel = new DockviewGroupPanel(options.id, this, this._api, group);
panel.init({ panel.init({
view, view,
title: options.title || options.id, title: options.title || options.id,
@ -754,14 +757,12 @@ export class DockviewComponent
); );
} }
private addPanelToNewGroup( private createGroupAtLocation(
panel: IGroupPanel,
location: number[] = [0] location: number[] = [0]
): void { ): GroupviewPanel {
const group = this.createGroup(); const group = this.createGroup();
this.doAddGroup(group, location); this.doAddGroup(group, location);
return group
group.model.openPanel(panel);
} }
private findGroup(panel: IGroupPanel): GroupviewPanel | undefined { private findGroup(panel: IGroupPanel): GroupviewPanel | undefined {

View File

@ -19,7 +19,7 @@ export class DockviewGroupPanel
private readonly mutableDisposable = new MutableDisposable(); private readonly mutableDisposable = new MutableDisposable();
readonly api: DockviewPanelApiImpl; readonly api: DockviewPanelApiImpl;
private _group: GroupviewPanel | undefined; private _group: GroupviewPanel;
private _params?: Parameters; private _params?: Parameters;
private _view?: IGroupPanelView; private _view?: IGroupPanelView;
@ -39,7 +39,7 @@ export class DockviewGroupPanel
return this._suppressClosable; return this._suppressClosable;
} }
get group(): GroupviewPanel | undefined { get group(): GroupviewPanel {
return this._group; return this._group;
} }
@ -50,11 +50,13 @@ export class DockviewGroupPanel
constructor( constructor(
public readonly id: string, public readonly id: string,
accessor: DockviewComponent, accessor: DockviewComponent,
private readonly containerApi: DockviewApi private readonly containerApi: DockviewApi,
group: GroupviewPanel
) { ) {
super(); super();
this._suppressClosable = false; this._suppressClosable = false;
this._title = ''; this._title = '';
this._group = group;
this.api = new DockviewPanelApiImpl(this, this._group); this.api = new DockviewPanelApiImpl(this, this._group);
@ -169,7 +171,7 @@ export class DockviewGroupPanel
// the obtain the correct dimensions of the content panel we must deduct the tab height // the obtain the correct dimensions of the content panel we must deduct the tab height
this.api._onDidPanelDimensionChange.fire({ this.api._onDidPanelDimensionChange.fire({
width, width,
height: height - (this.group?.model.tabHeight || 0), height: height - (this.group.model.header.height || 0),
}); });
this.view?.layout(width, height); this.view?.layout(width, height);

View File

@ -241,9 +241,9 @@ const serializeBranchNode = <T extends IGridView>(
}; };
}; };
export interface ISerializedLeafNode { export interface ISerializedLeafNode<T = any> {
type: 'leaf'; type: 'leaf';
data: any; data: T;
size: number; size: number;
visible?: boolean; visible?: boolean;
} }

View File

@ -24,7 +24,7 @@ export type GroupPanelUpdateEvent = PanelUpdateEvent<{
export interface IGroupPanel extends IDisposable, IPanel { export interface IGroupPanel extends IDisposable, IPanel {
readonly view?: IGroupPanelView; readonly view?: IGroupPanelView;
readonly group?: GroupviewPanel; readonly group: GroupviewPanel;
readonly api: DockviewPanelApi; readonly api: DockviewPanelApi;
readonly title: string; readonly title: string;
readonly suppressClosable: boolean; readonly suppressClosable: boolean;

View File

@ -1,5 +1,5 @@
import { DockviewApi } from '../api/component.api'; import { DockviewApi } from '../api/component.api';
import { getPanelData } from '../dnd/dataTransfer'; import { getPanelData, PanelTransfer } from '../dnd/dataTransfer';
import { Droptarget, Position } from '../dnd/droptarget'; import { Droptarget, Position } from '../dnd/droptarget';
import { IDockviewComponent } from '../dockview/dockviewComponent'; import { IDockviewComponent } from '../dockview/dockviewComponent';
import { isAncestor, toggleClass } from '../dom'; import { isAncestor, toggleClass } from '../dom';
@ -48,37 +48,52 @@ interface GroupMoveEvent {
index?: number; index?: number;
} }
export interface GroupOptions { interface CoreGroupOptions {
locked?: boolean;
hideHeader?: boolean;
}
export interface GroupOptions extends CoreGroupOptions {
readonly panels?: IGroupPanel[]; readonly panels?: IGroupPanel[];
readonly activePanel?: IGroupPanel; readonly activePanel?: IGroupPanel;
readonly id?: string; readonly id?: string;
tabHeight?: number; tabHeight?: number;
} }
export interface GroupPanelViewState extends CoreGroupOptions {
views: string[];
activeView?: string;
id: string;
}
export interface GroupviewChangeEvent { export interface GroupviewChangeEvent {
readonly kind: GroupChangeKind2; readonly kind: GroupChangeKind2;
readonly panel?: IGroupPanel; readonly panel?: IGroupPanel;
} }
export interface GroupPanelViewState {
views: string[];
activeView?: string;
id: string;
}
export interface GroupviewDropEvent { export interface GroupviewDropEvent {
nativeEvent: DragEvent; nativeEvent: DragEvent;
position: Position; position: Position;
getData(): PanelTransfer | undefined;
index?: number; index?: number;
} }
export interface IHeader {
hidden: boolean;
height: number | undefined;
}
export interface IGroupview extends IDisposable, IGridPanelView { export interface IGroupview extends IDisposable, IGridPanelView {
readonly isActive: boolean; readonly isActive: boolean;
readonly size: number; readonly size: number;
readonly panels: IGroupPanel[]; readonly panels: IGroupPanel[];
readonly tabHeight: number | undefined;
readonly activePanel: IGroupPanel | undefined; readonly activePanel: IGroupPanel | undefined;
readonly header: IHeader;
readonly isContentFocused: boolean;
readonly onDidDrop: Event<GroupviewDropEvent>; readonly onDidDrop: Event<GroupviewDropEvent>;
readonly onDidGroupChange: Event<GroupviewChangeEvent>;
readonly onMove: Event<GroupMoveEvent>;
locked: boolean;
// state // state
isPanelActive: (panel: IGroupPanel) => boolean; isPanelActive: (panel: IGroupPanel) => boolean;
indexOf(panel: IGroupPanel): number; indexOf(panel: IGroupPanel): number;
@ -91,16 +106,11 @@ export interface IGroupview extends IDisposable, IGridPanelView {
closeAllPanels(): void; closeAllPanels(): void;
containsPanel(panel: IGroupPanel): boolean; containsPanel(panel: IGroupPanel): boolean;
removePanel: (panelOrId: IGroupPanel | string) => IGroupPanel; removePanel: (panelOrId: IGroupPanel | string) => IGroupPanel;
// events
onDidGroupChange: Event<GroupviewChangeEvent>;
onMove: Event<GroupMoveEvent>;
moveToNext(options?: { panel?: IGroupPanel; suppressRoll?: boolean }): void; moveToNext(options?: { panel?: IGroupPanel; suppressRoll?: boolean }): void;
moveToPrevious(options?: { moveToPrevious(options?: {
panel?: IGroupPanel; panel?: IGroupPanel;
suppressRoll?: boolean; suppressRoll?: boolean;
}): void; }): void;
isContentFocused(): boolean;
updateActions(): void;
canDisplayOverlay(event: DragEvent, target: DockviewDropTargets): boolean; canDisplayOverlay(event: DragEvent, target: DockviewDropTargets): boolean;
} }
@ -111,6 +121,7 @@ export class Groupview extends CompositeDisposable implements IGroupview {
private _activePanel?: IGroupPanel; private _activePanel?: IGroupPanel;
private watermark?: IWatermarkRenderer; private watermark?: IWatermarkRenderer;
private _isGroupActive = false; private _isGroupActive = false;
private _locked = false;
private mostRecentlyUsed: IGroupPanel[] = []; private mostRecentlyUsed: IGroupPanel[] = [];
@ -141,13 +152,12 @@ export class Groupview extends CompositeDisposable implements IGroupview {
return this._activePanel; return this._activePanel;
} }
get tabHeight(): number | undefined { get locked(): boolean {
return this.tabsContainer.height; return this._locked;
} }
set tabHeight(height: number | undefined) { set locked(value: boolean) {
this.tabsContainer.height = height; this._locked = value;
this.layout(this._width, this._height);
} }
get isActive(): boolean { get isActive(): boolean {
@ -188,6 +198,20 @@ export class Groupview extends CompositeDisposable implements IGroupview {
); );
} }
get header(): IHeader {
return this.tabsContainer;
}
get isContentFocused(): boolean {
if (!document.activeElement) {
return false;
}
return isAncestor(
document.activeElement,
this.contentContainer.element
);
}
constructor( constructor(
private readonly container: HTMLElement, private readonly container: HTMLElement,
private accessor: IDockviewComponent, private accessor: IDockviewComponent,
@ -210,9 +234,14 @@ export class Groupview extends CompositeDisposable implements IGroupview {
tabHeight: options.tabHeight, tabHeight: options.tabHeight,
}); });
this.contentContainer = new ContentContainer(); this.contentContainer = new ContentContainer();
this.dropTarget = new Droptarget(this.contentContainer.element, { this.dropTarget = new Droptarget(this.contentContainer.element, {
validOverlays: 'all', validOverlays: 'all',
canDisplayOverlay: (event) => { canDisplayOverlay: (event, quadrant) => {
if (this.locked && !quadrant) {
return false;
}
const data = getPanelData(); const data = getPanelData();
if (data) { if (data) {
@ -231,6 +260,9 @@ export class Groupview extends CompositeDisposable implements IGroupview {
this.contentContainer.element this.contentContainer.element
); );
this.header.hidden = !!options.hideHeader;
this.locked = !!options.locked;
this.addDisposables( this.addDisposables(
this._onMove, this._onMove,
this._onDidGroupChange, this._onDidGroupChange,
@ -266,26 +298,26 @@ export class Groupview extends CompositeDisposable implements IGroupview {
this.updateContainer(); this.updateContainer();
} }
isContentFocused() {
if (!document.activeElement) {
return false;
}
return isAncestor(
document.activeElement,
this.contentContainer.element
);
}
public indexOf(panel: IGroupPanel) { public indexOf(panel: IGroupPanel) {
return this.tabsContainer.indexOf(panel.id); return this.tabsContainer.indexOf(panel.id);
} }
public toJSON(): GroupPanelViewState { public toJSON(): GroupPanelViewState {
return { const result: GroupPanelViewState = {
views: this.tabsContainer.panels, views: this.tabsContainer.panels,
activeView: this._activePanel?.id, activeView: this._activePanel?.id,
id: this.id, id: this.id,
}; };
if (this.locked) {
result.locked = true;
}
if (this.header.hidden) {
result.hideHeader = true;
}
return result;
} }
public moveToNext(options?: { public moveToNext(options?: {
@ -362,7 +394,11 @@ export class Groupview extends CompositeDisposable implements IGroupview {
public openPanel( public openPanel(
panel: IGroupPanel, panel: IGroupPanel,
options: { index?: number; skipFocus?: boolean } = {} options: {
index?: number;
skipFocus?: boolean;
skipSetActive?: boolean;
} = {}
) { ) {
if ( if (
typeof options.index !== 'number' || typeof options.index !== 'number' ||
@ -371,18 +407,22 @@ export class Groupview extends CompositeDisposable implements IGroupview {
options.index = this.panels.length; options.index = this.panels.length;
} }
const skipSetActive = !!options.skipSetActive;
// ensure the group is updated before we fire any events // ensure the group is updated before we fire any events
panel.updateParentGroup(this.parent, true); panel.updateParentGroup(this.parent, true);
if (this._activePanel === panel) { if (!skipSetActive && this._activePanel === panel) {
this.accessor.doSetGroupActive(this.parent); this.accessor.doSetGroupActive(this.parent);
return; return;
} }
this.doAddPanel(panel, options.index); this.doAddPanel(panel, options.index);
this.doSetActivePanel(panel); if (!skipSetActive) {
this.accessor.doSetGroupActive(this.parent, !!options.skipFocus); this.doSetActivePanel(panel);
this.accessor.doSetGroupActive(this.parent, !!options.skipFocus);
}
this.updateContainer(); this.updateContainer();
} }
@ -646,7 +686,12 @@ export class Groupview extends CompositeDisposable implements IGroupview {
index, index,
}); });
} else { } else {
this._onDidDrop.fire({ nativeEvent: event, position, index }); this._onDidDrop.fire({
nativeEvent: event,
position,
index,
getData: () => getPanelData(),
});
} }
} }

View File

@ -1,48 +1,86 @@
import { IFrameworkPart } from '../panel/types'; import { IFrameworkPart } from '../panel/types';
import { IDockviewComponent } from '../dockview/dockviewComponent'; import { IDockviewComponent } from '../dockview/dockviewComponent';
import { GridviewPanelApiImpl } from '../api/gridviewPanelApi'; import {
import { Groupview, GroupOptions } from './groupview'; GridviewPanelApi,
GridviewPanelApiImpl,
} from '../api/gridviewPanelApi';
import { Groupview, GroupOptions, IHeader } from './groupview';
import { GridviewPanel, IGridviewPanel } from '../gridview/gridviewPanel'; import { GridviewPanel, IGridviewPanel } from '../gridview/gridviewPanel';
import { IGroupPanel } from './groupPanel';
export interface IGroupviewPanel extends IGridviewPanel { export interface IGroupviewPanel extends IGridviewPanel {
model: Groupview; model: Groupview;
locked: boolean;
readonly size: number;
readonly panels: IGroupPanel[];
readonly activePanel: IGroupPanel | undefined;
} }
export type IGroupviewPanelPublic = IGroupviewPanel;
export type GroupviewPanelApi = GridviewPanelApi;
class GroupviewApi extends GridviewPanelApiImpl implements GroupviewPanelApi {}
export class GroupviewPanel extends GridviewPanel implements IGroupviewPanel { export class GroupviewPanel extends GridviewPanel implements IGroupviewPanel {
private readonly _model: Groupview; private readonly _model: Groupview;
get panels(): IGroupPanel[] {
return this._model.panels;
}
get activePanel(): IGroupPanel | undefined {
return this._model.activePanel;
}
get size(): number {
return this._model.size;
}
get model(): Groupview { get model(): Groupview {
return this._model; return this._model;
} }
get minimumHeight() { get minimumHeight(): number {
return this._model.minimumHeight; return this._model.minimumHeight;
} }
get maximumHeight() { get maximumHeight(): number {
return this._model.maximumHeight; return this._model.maximumHeight;
} }
get minimumWidth() { get minimumWidth(): number {
return this._model.minimumWidth; return this._model.minimumWidth;
} }
get maximumWidth() { get maximumWidth(): number {
return this._model.maximumWidth; return this._model.maximumWidth;
} }
get locked(): boolean {
return this._model.locked;
}
set locked(value: boolean) {
this._model.locked = value;
}
get header(): IHeader {
return this._model.header;
}
constructor( constructor(
accessor: IDockviewComponent, accessor: IDockviewComponent,
id: string, id: string,
options: GroupOptions options: GroupOptions
) { ) {
super(id, 'groupview_default', new GridviewPanelApiImpl(id)); super(id, 'groupview_default', new GroupviewApi(id));
this._model = new Groupview(this.element, accessor, id, options, this); this._model = new Groupview(this.element, accessor, id, options, this);
} }
initialize() { initialize() {
this.model.initialize(); this._model.initialize();
} }
setActive(isActive: boolean): void { setActive(isActive: boolean): void {

View File

@ -33,6 +33,7 @@ export interface ITabsContainer extends IDisposable {
closePanel: (panel: IGroupPanel) => void; closePanel: (panel: IGroupPanel) => void;
openPanel: (panel: IGroupPanel, index?: number) => void; openPanel: (panel: IGroupPanel, index?: number) => void;
setActionElement(element: HTMLElement | undefined): void; setActionElement(element: HTMLElement | undefined): void;
hidden: boolean;
show(): void; show(): void;
hide(): void; hide(): void;
} }
@ -55,6 +56,7 @@ export class TabsContainer
private actions: HTMLElement | undefined; private actions: HTMLElement | undefined;
private _height: number | undefined; private _height: number | undefined;
private _hidden = false;
private readonly _onDrop = new Emitter<TabDropIndexEvent>(); private readonly _onDrop = new Emitter<TabDropIndexEvent>();
readonly onDrop: Event<TabDropIndexEvent> = this._onDrop.event; readonly onDrop: Event<TabDropIndexEvent> = this._onDrop.event;
@ -85,12 +87,23 @@ export class TabsContainer
} }
} }
show() { get hidden(): boolean {
this.element.style.display = ''; return this._hidden;
} }
hide() { set hidden(value: boolean) {
this.element.style.display = 'none'; this._hidden = value;
this.element.style.display = value ? 'none' : '';
}
show(): void {
if (!this.hidden) {
this.element.style.display = '';
}
}
hide(): void {
this._element.style.display = 'none';
} }
setActionElement(element: HTMLElement | undefined): void { setActionElement(element: HTMLElement | undefined): void {
@ -252,7 +265,7 @@ export class TabsContainer
tabToAdd.onChanged((event) => { tabToAdd.onChanged((event) => {
const alreadyFocused = const alreadyFocused =
panel.id === this.group.model.activePanel?.id && panel.id === this.group.model.activePanel?.id &&
this.group.model.isContentFocused(); this.group.model.isContentFocused;
this.accessor.fireMouseEvent({ ...event, panel, tab: true }); this.accessor.fireMouseEvent({ ...event, panel, tab: true });
const isLeftClick = event.event.button === 0; const isLeftClick = event.event.button === 0;

View File

@ -1,5 +1,4 @@
export * from './dnd/dataTransfer'; export * from './dnd/dataTransfer';
export * from './api/component.api';
export * from './splitview/core/splitview'; export * from './splitview/core/splitview';
export * from './paneview/paneview'; export * from './paneview/paneview';
@ -50,3 +49,10 @@ export {
SplitviewPanelApi, SplitviewPanelApi,
} from './api/splitviewPanelApi'; } from './api/splitviewPanelApi';
export { ExpansionEvent, PaneviewPanelApi } from './api/paneviewPanelApi'; export { ExpansionEvent, PaneviewPanelApi } from './api/paneviewPanelApi';
export {
CommonApi,
SplitviewApi,
PaneviewApi,
GridviewApi,
DockviewApi,
} from './api/component.api';

View File

@ -6,11 +6,15 @@ import { createComponent } from '../panel/componentFactory';
import { DockviewApi } from '../api/component.api'; import { DockviewApi } from '../api/component.api';
import { DefaultTab } from '../dockview/components/tab/defaultTab'; import { DefaultTab } from '../dockview/components/tab/defaultTab';
import { DefaultGroupPanelView } from '../dockview/defaultGroupPanelView'; import { DefaultGroupPanelView } from '../dockview/defaultGroupPanelView';
import { GroupviewPanel } from '../groupview/groupviewPanel';
export class ReactPanelDeserialzier implements IPanelDeserializer { export class ReactPanelDeserialzier implements IPanelDeserializer {
constructor(private readonly layout: DockviewComponent) {} constructor(private readonly layout: DockviewComponent) {}
public fromJSON(panelData: GroupviewPanelState): IGroupPanel { public fromJSON(
panelData: GroupviewPanelState,
group: GroupviewPanel
): IGroupPanel {
const panelId = panelData.id; const panelId = panelData.id;
const params = panelData.params; const params = panelData.params;
const title = panelData.title; const title = panelData.title;
@ -39,7 +43,8 @@ export class ReactPanelDeserialzier implements IPanelDeserializer {
const panel = new DockviewGroupPanel( const panel = new DockviewGroupPanel(
panelId, panelId,
this.layout, this.layout,
new DockviewApi(this.layout) new DockviewApi(this.layout),
group
); );
panel.init({ panel.init({