mirror of
https://github.com/mathuo/dockview
synced 2025-08-27 12:36:35 +00:00
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:
commit
d7d6dc2635
1
.gitignore
vendored
1
.gitignore
vendored
@ -11,3 +11,4 @@ storybook-static/
|
||||
.rollup.cache/
|
||||
test-report.xml
|
||||
*.code-workspace
|
||||
yarn-error.log
|
||||
|
@ -25,7 +25,8 @@
|
||||
"prepack": "npm run rebuild && npm run test",
|
||||
"rebuild": "npm run clean && npm run build",
|
||||
"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": [
|
||||
"dist",
|
||||
|
63
packages/dockview/scripts/publishExperimental.js
Normal file
63
packages/dockview/scripts/publishExperimental.js
Normal 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);
|
@ -20,6 +20,11 @@ describe('droptarget', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
element = document.createElement('div');
|
||||
|
||||
jest.spyOn(element, 'clientHeight', 'get').mockImplementation(
|
||||
() => 100
|
||||
);
|
||||
jest.spyOn(element, 'clientWidth', 'get').mockImplementation(() => 200);
|
||||
});
|
||||
|
||||
test('non-directional', () => {
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
@ -619,7 +619,7 @@ describe('dockviewComponent', () => {
|
||||
data: {
|
||||
views: ['panel2', 'panel3'],
|
||||
id: 'group-2',
|
||||
activeView: 'panel2',
|
||||
activeView: 'panel3',
|
||||
},
|
||||
size: 500,
|
||||
},
|
||||
|
@ -1,19 +1,26 @@
|
||||
import { DockviewComponent } from '../..';
|
||||
import { DockviewComponent } from '../../dockview/dockviewComponent';
|
||||
import { DockviewApi } from '../../api/component.api';
|
||||
import { IGroupPanelView } from '../../dockview/defaultGroupPanelView';
|
||||
import { DockviewGroupPanel } from '../../dockview/dockviewGroupPanel';
|
||||
import { GroupviewPanel } from '../../groupview/groupviewPanel';
|
||||
|
||||
describe('dockviewGroupPanel', () => {
|
||||
test('update title', () => {
|
||||
const dockviewApiMock = jest.fn<DockviewApi, []>(() => {
|
||||
return {} as any;
|
||||
return {
|
||||
onDidActiveChange: jest.fn(),
|
||||
} as any;
|
||||
});
|
||||
const accessorMock = jest.fn<DockviewComponent, []>(() => {
|
||||
return {} as any;
|
||||
});
|
||||
const groupMock = jest.fn<GroupviewPanel, []>(() => {
|
||||
return {} as any;
|
||||
});
|
||||
const api = new dockviewApiMock();
|
||||
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;
|
||||
|
||||
@ -41,10 +48,14 @@ describe('dockviewGroupPanel', () => {
|
||||
const accessorMock = jest.fn<DockviewComponent, []>(() => {
|
||||
return {} as any;
|
||||
});
|
||||
const groupMock = jest.fn<GroupviewPanel, []>(() => {
|
||||
return {} as any;
|
||||
});
|
||||
const api = new dockviewApiMock();
|
||||
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;
|
||||
|
||||
@ -77,10 +88,14 @@ describe('dockviewGroupPanel', () => {
|
||||
const accessorMock = jest.fn<DockviewComponent, []>(() => {
|
||||
return {} as any;
|
||||
});
|
||||
const groupMock = jest.fn<GroupviewPanel, []>(() => {
|
||||
return {} as any;
|
||||
});
|
||||
const api = new dockviewApiMock();
|
||||
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, []>(() => {
|
||||
return {
|
||||
|
@ -15,7 +15,11 @@ import {
|
||||
} from '../../groupview/types';
|
||||
import { PanelUpdateEvent } from '../../panel/types';
|
||||
import { GroupviewPanel } from '../../groupview/groupviewPanel';
|
||||
import { GroupChangeKind2, GroupOptions } from '../../groupview/groupview';
|
||||
import {
|
||||
GroupChangeKind2,
|
||||
GroupOptions,
|
||||
Groupview,
|
||||
} from '../../groupview/groupview';
|
||||
import { DockviewPanelApi } from '../../api/groupPanelApi';
|
||||
import {
|
||||
DefaultGroupPanelView,
|
||||
@ -463,4 +467,59 @@ describe('groupview', () => {
|
||||
const panel = cut.addPanel({ id: 'id', component: 'component' });
|
||||
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,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -17,7 +17,7 @@ export interface SuppressClosableEvent {
|
||||
* because it belongs to a groupview
|
||||
*/
|
||||
export interface DockviewPanelApi extends Omit<GridviewPanelApi, 'setVisible'> {
|
||||
readonly group: GroupviewPanel | undefined;
|
||||
readonly group: GroupviewPanel;
|
||||
readonly isGroupActive: boolean;
|
||||
readonly title: string;
|
||||
readonly suppressClosable: boolean;
|
||||
@ -29,7 +29,7 @@ export class DockviewPanelApiImpl
|
||||
extends GridviewPanelApiImpl
|
||||
implements DockviewPanelApi
|
||||
{
|
||||
private _group: GroupviewPanel | undefined;
|
||||
private _group: GroupviewPanel;
|
||||
|
||||
readonly _onDidTitleChange = new Emitter<TitleEvent>();
|
||||
readonly onDidTitleChange = this._onDidTitleChange.event;
|
||||
@ -60,7 +60,7 @@ export class DockviewPanelApiImpl
|
||||
return !!this.group?.isActive;
|
||||
}
|
||||
|
||||
set group(value: GroupviewPanel | undefined) {
|
||||
set group(value: GroupviewPanel) {
|
||||
const isOldGroupActive = this.isGroupActive;
|
||||
|
||||
this._group = value;
|
||||
@ -78,13 +78,13 @@ export class DockviewPanelApiImpl
|
||||
}
|
||||
}
|
||||
|
||||
get group(): GroupviewPanel | undefined {
|
||||
get group(): GroupviewPanel {
|
||||
return this._group;
|
||||
}
|
||||
|
||||
constructor(private panel: IGroupPanel, group: GroupviewPanel | undefined) {
|
||||
constructor(private panel: IGroupPanel, group: GroupviewPanel) {
|
||||
super(panel.id);
|
||||
this.group = group;
|
||||
this._group = group;
|
||||
|
||||
this.addDisposables(
|
||||
this.disposable,
|
||||
|
@ -26,7 +26,9 @@ function isBooleanValue(
|
||||
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 {
|
||||
private target: HTMLElement | undefined;
|
||||
@ -62,11 +64,29 @@ export class Droptarget extends CompositeDisposable {
|
||||
new DragAndDropObserver(this.element, {
|
||||
onDragEnter: () => undefined,
|
||||
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 (!this.options.canDisplayOverlay) {
|
||||
return;
|
||||
}
|
||||
} else if (!this.options.canDisplayOverlay(e)) {
|
||||
} else if (!this.options.canDisplayOverlay(e, quadrant)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -90,24 +110,6 @@ export class Droptarget extends CompositeDisposable {
|
||||
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 isSmallY = height < 100;
|
||||
|
||||
|
@ -4,36 +4,52 @@ import {
|
||||
IViewDeserializer,
|
||||
} from '../gridview/gridview';
|
||||
import { GroupviewPanelState, IGroupPanel } from '../groupview/groupPanel';
|
||||
import { GroupPanelViewState } from '../groupview/groupview';
|
||||
import { GroupviewPanel } from '../groupview/groupviewPanel';
|
||||
import { DockviewComponent } from './dockviewComponent';
|
||||
|
||||
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 {
|
||||
constructor(
|
||||
private readonly layout: DockviewComponent,
|
||||
private panelDeserializer: {
|
||||
createPanel: (id: string) => IGroupPanel;
|
||||
}
|
||||
private panelDeserializer: PanelDeserializerOptions
|
||||
) {}
|
||||
|
||||
public fromJSON(node: ISerializedLeafNode): IGridView {
|
||||
const children = node.data.views;
|
||||
const active = node.data.activeView;
|
||||
public fromJSON(node: ISerializedLeafNode<GroupPanelViewState>): IGridView {
|
||||
const data = node.data;
|
||||
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) {
|
||||
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({
|
||||
panels,
|
||||
activePanel: panels.find((p) => p.id === active),
|
||||
id: node.data.id,
|
||||
});
|
||||
if (!group.activePanel && group.panels.length > 0) {
|
||||
group.model.openPanel(group.panels[group.panels.length - 1]);
|
||||
}
|
||||
|
||||
return group;
|
||||
}
|
||||
}
|
||||
|
@ -63,7 +63,7 @@ export interface SerializedDockview {
|
||||
}
|
||||
|
||||
export type DockviewComponentUpdateOptions = Pick<
|
||||
DockviewComponentOptions,
|
||||
DockviewComponentOptions,
|
||||
| 'orientation'
|
||||
| 'components'
|
||||
| 'frameworkComponents'
|
||||
@ -160,7 +160,7 @@ export class DockviewComponent
|
||||
}
|
||||
|
||||
get panels(): IGroupPanel[] {
|
||||
return this.groups.flatMap((group) => group.model.panels);
|
||||
return this.groups.flatMap((group) => group.panels);
|
||||
}
|
||||
|
||||
get deserializer(): IPanelDeserializer | undefined {
|
||||
@ -182,13 +182,13 @@ export class DockviewComponent
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return activeGroup.model.activePanel;
|
||||
return activeGroup.activePanel;
|
||||
}
|
||||
|
||||
set tabHeight(height: number | undefined) {
|
||||
this.options.tabHeight = height;
|
||||
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 {
|
||||
if (!panel.group) {
|
||||
throw new Error(`Panel ${panel.id} has no associated group`);
|
||||
}
|
||||
this.doSetGroupActive(panel.group);
|
||||
panel.group.model.openPanel(panel);
|
||||
}
|
||||
@ -280,9 +277,9 @@ export class DockviewComponent
|
||||
|
||||
if (options.includePanel && options.group) {
|
||||
if (
|
||||
options.group.model.activePanel !==
|
||||
options.group.model.panels[
|
||||
options.group.model.panels.length - 1
|
||||
options.group.activePanel !==
|
||||
options.group.panels[
|
||||
options.group.panels.length - 1
|
||||
]
|
||||
) {
|
||||
options.group.model.moveToNext({ suppressRoll: true });
|
||||
@ -291,7 +288,7 @@ export class DockviewComponent
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@ -305,8 +302,8 @@ export class DockviewComponent
|
||||
|
||||
if (options.includePanel && options.group) {
|
||||
if (
|
||||
options.group.model.activePanel !==
|
||||
options.group.model.panels[0]
|
||||
options.group.activePanel !==
|
||||
options.group.panels[0]
|
||||
) {
|
||||
options.group.model.moveToPrevious({ suppressRoll: true });
|
||||
return;
|
||||
@ -367,9 +364,9 @@ export class DockviewComponent
|
||||
this.gridview.deserialize(
|
||||
grid,
|
||||
new DefaultDeserializer(this, {
|
||||
createPanel: (id) => {
|
||||
createPanel: (id, group) => {
|
||||
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`);
|
||||
}
|
||||
|
||||
const panel = this.createPanel(options);
|
||||
|
||||
let referenceGroup: GroupviewPanel | undefined;
|
||||
|
||||
if (options.position?.referencePanel) {
|
||||
@ -431,9 +426,12 @@ export class DockviewComponent
|
||||
referenceGroup = this.activeGroup;
|
||||
}
|
||||
|
||||
let panel: IGroupPanel
|
||||
|
||||
if (referenceGroup) {
|
||||
const target = toTarget(options.position?.direction || 'within');
|
||||
if (target === Position.Center) {
|
||||
panel = this.createPanel(options, referenceGroup)
|
||||
referenceGroup.model.openPanel(panel);
|
||||
} else {
|
||||
const location = getGridLocation(referenceGroup.element);
|
||||
@ -442,10 +440,14 @@ export class DockviewComponent
|
||||
location,
|
||||
target
|
||||
);
|
||||
this.addPanelToNewGroup(panel, relativeLocation);
|
||||
const group = this.createGroupAtLocation(relativeLocation);
|
||||
panel = this.createPanel(options, group)
|
||||
group.model.openPanel(panel);
|
||||
}
|
||||
} else {
|
||||
this.addPanelToNewGroup(panel);
|
||||
const group = this.createGroupAtLocation();
|
||||
panel = this.createPanel(options, group);
|
||||
group.model.openPanel(panel);
|
||||
}
|
||||
|
||||
return panel;
|
||||
@ -474,7 +476,7 @@ export class DockviewComponent
|
||||
|
||||
if (
|
||||
!retainGroupForWatermark &&
|
||||
group.model.size === 0 &&
|
||||
group.size === 0 &&
|
||||
options.removeEmptyGroup
|
||||
) {
|
||||
this.removeGroup(group);
|
||||
@ -532,7 +534,7 @@ export class DockviewComponent
|
||||
}
|
||||
|
||||
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) {
|
||||
this.removePanel(panel, {
|
||||
@ -577,7 +579,7 @@ export class DockviewComponent
|
||||
target
|
||||
);
|
||||
|
||||
if (sourceGroup && sourceGroup.model.size < 2) {
|
||||
if (sourceGroup && sourceGroup.size < 2) {
|
||||
const [targetParentLocation, to] = tail(targetLocation);
|
||||
const sourceLocation = getGridLocation(sourceGroup.element);
|
||||
const [sourceParentLocation, from] = tail(sourceLocation);
|
||||
@ -622,7 +624,8 @@ export class DockviewComponent
|
||||
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;
|
||||
super.doSetGroupActive(group, skipFocus);
|
||||
|
||||
if (!isGroupAlreadyFocused && this._activeGroup?.model.activePanel) {
|
||||
if (!isGroupAlreadyFocused && this._activeGroup?.activePanel) {
|
||||
this._onDidActivePanelChange.fire(
|
||||
this._activeGroup?.model.activePanel
|
||||
this._activeGroup?.activePanel
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -704,19 +707,19 @@ export class DockviewComponent
|
||||
view.initialize();
|
||||
|
||||
if (typeof this.options.tabHeight === 'number') {
|
||||
view.model.tabHeight = this.options.tabHeight;
|
||||
view.model.header.height = this.options.tabHeight;
|
||||
}
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
private createPanel(options: AddPanelOptions): IGroupPanel {
|
||||
private createPanel(options: AddPanelOptions, group: GroupviewPanel): IGroupPanel {
|
||||
const view = new DefaultGroupPanelView({
|
||||
content: this.createContentComponent(options.id, options.component),
|
||||
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({
|
||||
view,
|
||||
title: options.title || options.id,
|
||||
@ -754,14 +757,12 @@ export class DockviewComponent
|
||||
);
|
||||
}
|
||||
|
||||
private addPanelToNewGroup(
|
||||
panel: IGroupPanel,
|
||||
private createGroupAtLocation(
|
||||
location: number[] = [0]
|
||||
): void {
|
||||
): GroupviewPanel {
|
||||
const group = this.createGroup();
|
||||
this.doAddGroup(group, location);
|
||||
|
||||
group.model.openPanel(panel);
|
||||
return group
|
||||
}
|
||||
|
||||
private findGroup(panel: IGroupPanel): GroupviewPanel | undefined {
|
||||
|
@ -19,7 +19,7 @@ export class DockviewGroupPanel
|
||||
private readonly mutableDisposable = new MutableDisposable();
|
||||
|
||||
readonly api: DockviewPanelApiImpl;
|
||||
private _group: GroupviewPanel | undefined;
|
||||
private _group: GroupviewPanel;
|
||||
private _params?: Parameters;
|
||||
|
||||
private _view?: IGroupPanelView;
|
||||
@ -39,7 +39,7 @@ export class DockviewGroupPanel
|
||||
return this._suppressClosable;
|
||||
}
|
||||
|
||||
get group(): GroupviewPanel | undefined {
|
||||
get group(): GroupviewPanel {
|
||||
return this._group;
|
||||
}
|
||||
|
||||
@ -50,11 +50,13 @@ export class DockviewGroupPanel
|
||||
constructor(
|
||||
public readonly id: string,
|
||||
accessor: DockviewComponent,
|
||||
private readonly containerApi: DockviewApi
|
||||
private readonly containerApi: DockviewApi,
|
||||
group: GroupviewPanel
|
||||
) {
|
||||
super();
|
||||
this._suppressClosable = false;
|
||||
this._title = '';
|
||||
this._group = 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
|
||||
this.api._onDidPanelDimensionChange.fire({
|
||||
width,
|
||||
height: height - (this.group?.model.tabHeight || 0),
|
||||
height: height - (this.group.model.header.height || 0),
|
||||
});
|
||||
|
||||
this.view?.layout(width, height);
|
||||
|
@ -241,9 +241,9 @@ const serializeBranchNode = <T extends IGridView>(
|
||||
};
|
||||
};
|
||||
|
||||
export interface ISerializedLeafNode {
|
||||
export interface ISerializedLeafNode<T = any> {
|
||||
type: 'leaf';
|
||||
data: any;
|
||||
data: T;
|
||||
size: number;
|
||||
visible?: boolean;
|
||||
}
|
||||
|
@ -24,7 +24,7 @@ export type GroupPanelUpdateEvent = PanelUpdateEvent<{
|
||||
|
||||
export interface IGroupPanel extends IDisposable, IPanel {
|
||||
readonly view?: IGroupPanelView;
|
||||
readonly group?: GroupviewPanel;
|
||||
readonly group: GroupviewPanel;
|
||||
readonly api: DockviewPanelApi;
|
||||
readonly title: string;
|
||||
readonly suppressClosable: boolean;
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { DockviewApi } from '../api/component.api';
|
||||
import { getPanelData } from '../dnd/dataTransfer';
|
||||
import { getPanelData, PanelTransfer } from '../dnd/dataTransfer';
|
||||
import { Droptarget, Position } from '../dnd/droptarget';
|
||||
import { IDockviewComponent } from '../dockview/dockviewComponent';
|
||||
import { isAncestor, toggleClass } from '../dom';
|
||||
@ -48,37 +48,52 @@ interface GroupMoveEvent {
|
||||
index?: number;
|
||||
}
|
||||
|
||||
export interface GroupOptions {
|
||||
interface CoreGroupOptions {
|
||||
locked?: boolean;
|
||||
hideHeader?: boolean;
|
||||
}
|
||||
|
||||
export interface GroupOptions extends CoreGroupOptions {
|
||||
readonly panels?: IGroupPanel[];
|
||||
readonly activePanel?: IGroupPanel;
|
||||
readonly id?: string;
|
||||
tabHeight?: number;
|
||||
}
|
||||
|
||||
export interface GroupPanelViewState extends CoreGroupOptions {
|
||||
views: string[];
|
||||
activeView?: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface GroupviewChangeEvent {
|
||||
readonly kind: GroupChangeKind2;
|
||||
readonly panel?: IGroupPanel;
|
||||
}
|
||||
|
||||
export interface GroupPanelViewState {
|
||||
views: string[];
|
||||
activeView?: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface GroupviewDropEvent {
|
||||
nativeEvent: DragEvent;
|
||||
position: Position;
|
||||
getData(): PanelTransfer | undefined;
|
||||
index?: number;
|
||||
}
|
||||
|
||||
export interface IHeader {
|
||||
hidden: boolean;
|
||||
height: number | undefined;
|
||||
}
|
||||
|
||||
export interface IGroupview extends IDisposable, IGridPanelView {
|
||||
readonly isActive: boolean;
|
||||
readonly size: number;
|
||||
readonly panels: IGroupPanel[];
|
||||
readonly tabHeight: number | undefined;
|
||||
readonly activePanel: IGroupPanel | undefined;
|
||||
readonly header: IHeader;
|
||||
readonly isContentFocused: boolean;
|
||||
readonly onDidDrop: Event<GroupviewDropEvent>;
|
||||
readonly onDidGroupChange: Event<GroupviewChangeEvent>;
|
||||
readonly onMove: Event<GroupMoveEvent>;
|
||||
locked: boolean;
|
||||
// state
|
||||
isPanelActive: (panel: IGroupPanel) => boolean;
|
||||
indexOf(panel: IGroupPanel): number;
|
||||
@ -91,16 +106,11 @@ export interface IGroupview extends IDisposable, IGridPanelView {
|
||||
closeAllPanels(): void;
|
||||
containsPanel(panel: IGroupPanel): boolean;
|
||||
removePanel: (panelOrId: IGroupPanel | string) => IGroupPanel;
|
||||
// events
|
||||
onDidGroupChange: Event<GroupviewChangeEvent>;
|
||||
onMove: Event<GroupMoveEvent>;
|
||||
moveToNext(options?: { panel?: IGroupPanel; suppressRoll?: boolean }): void;
|
||||
moveToPrevious(options?: {
|
||||
panel?: IGroupPanel;
|
||||
suppressRoll?: boolean;
|
||||
}): void;
|
||||
isContentFocused(): boolean;
|
||||
updateActions(): void;
|
||||
canDisplayOverlay(event: DragEvent, target: DockviewDropTargets): boolean;
|
||||
}
|
||||
|
||||
@ -111,6 +121,7 @@ export class Groupview extends CompositeDisposable implements IGroupview {
|
||||
private _activePanel?: IGroupPanel;
|
||||
private watermark?: IWatermarkRenderer;
|
||||
private _isGroupActive = false;
|
||||
private _locked = false;
|
||||
|
||||
private mostRecentlyUsed: IGroupPanel[] = [];
|
||||
|
||||
@ -141,13 +152,12 @@ export class Groupview extends CompositeDisposable implements IGroupview {
|
||||
return this._activePanel;
|
||||
}
|
||||
|
||||
get tabHeight(): number | undefined {
|
||||
return this.tabsContainer.height;
|
||||
get locked(): boolean {
|
||||
return this._locked;
|
||||
}
|
||||
|
||||
set tabHeight(height: number | undefined) {
|
||||
this.tabsContainer.height = height;
|
||||
this.layout(this._width, this._height);
|
||||
set locked(value: boolean) {
|
||||
this._locked = value;
|
||||
}
|
||||
|
||||
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(
|
||||
private readonly container: HTMLElement,
|
||||
private accessor: IDockviewComponent,
|
||||
@ -210,9 +234,14 @@ export class Groupview extends CompositeDisposable implements IGroupview {
|
||||
tabHeight: options.tabHeight,
|
||||
});
|
||||
this.contentContainer = new ContentContainer();
|
||||
|
||||
this.dropTarget = new Droptarget(this.contentContainer.element, {
|
||||
validOverlays: 'all',
|
||||
canDisplayOverlay: (event) => {
|
||||
canDisplayOverlay: (event, quadrant) => {
|
||||
if (this.locked && !quadrant) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const data = getPanelData();
|
||||
|
||||
if (data) {
|
||||
@ -231,6 +260,9 @@ export class Groupview extends CompositeDisposable implements IGroupview {
|
||||
this.contentContainer.element
|
||||
);
|
||||
|
||||
this.header.hidden = !!options.hideHeader;
|
||||
this.locked = !!options.locked;
|
||||
|
||||
this.addDisposables(
|
||||
this._onMove,
|
||||
this._onDidGroupChange,
|
||||
@ -266,26 +298,26 @@ export class Groupview extends CompositeDisposable implements IGroupview {
|
||||
this.updateContainer();
|
||||
}
|
||||
|
||||
isContentFocused() {
|
||||
if (!document.activeElement) {
|
||||
return false;
|
||||
}
|
||||
return isAncestor(
|
||||
document.activeElement,
|
||||
this.contentContainer.element
|
||||
);
|
||||
}
|
||||
|
||||
public indexOf(panel: IGroupPanel) {
|
||||
return this.tabsContainer.indexOf(panel.id);
|
||||
}
|
||||
|
||||
public toJSON(): GroupPanelViewState {
|
||||
return {
|
||||
const result: GroupPanelViewState = {
|
||||
views: this.tabsContainer.panels,
|
||||
activeView: this._activePanel?.id,
|
||||
id: this.id,
|
||||
};
|
||||
|
||||
if (this.locked) {
|
||||
result.locked = true;
|
||||
}
|
||||
|
||||
if (this.header.hidden) {
|
||||
result.hideHeader = true;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public moveToNext(options?: {
|
||||
@ -362,7 +394,11 @@ export class Groupview extends CompositeDisposable implements IGroupview {
|
||||
|
||||
public openPanel(
|
||||
panel: IGroupPanel,
|
||||
options: { index?: number; skipFocus?: boolean } = {}
|
||||
options: {
|
||||
index?: number;
|
||||
skipFocus?: boolean;
|
||||
skipSetActive?: boolean;
|
||||
} = {}
|
||||
) {
|
||||
if (
|
||||
typeof options.index !== 'number' ||
|
||||
@ -371,18 +407,22 @@ export class Groupview extends CompositeDisposable implements IGroupview {
|
||||
options.index = this.panels.length;
|
||||
}
|
||||
|
||||
const skipSetActive = !!options.skipSetActive;
|
||||
|
||||
// ensure the group is updated before we fire any events
|
||||
panel.updateParentGroup(this.parent, true);
|
||||
|
||||
if (this._activePanel === panel) {
|
||||
if (!skipSetActive && this._activePanel === panel) {
|
||||
this.accessor.doSetGroupActive(this.parent);
|
||||
return;
|
||||
}
|
||||
|
||||
this.doAddPanel(panel, options.index);
|
||||
|
||||
this.doSetActivePanel(panel);
|
||||
this.accessor.doSetGroupActive(this.parent, !!options.skipFocus);
|
||||
if (!skipSetActive) {
|
||||
this.doSetActivePanel(panel);
|
||||
this.accessor.doSetGroupActive(this.parent, !!options.skipFocus);
|
||||
}
|
||||
|
||||
this.updateContainer();
|
||||
}
|
||||
@ -646,7 +686,12 @@ export class Groupview extends CompositeDisposable implements IGroupview {
|
||||
index,
|
||||
});
|
||||
} else {
|
||||
this._onDidDrop.fire({ nativeEvent: event, position, index });
|
||||
this._onDidDrop.fire({
|
||||
nativeEvent: event,
|
||||
position,
|
||||
index,
|
||||
getData: () => getPanelData(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,48 +1,86 @@
|
||||
import { IFrameworkPart } from '../panel/types';
|
||||
import { IDockviewComponent } from '../dockview/dockviewComponent';
|
||||
import { GridviewPanelApiImpl } from '../api/gridviewPanelApi';
|
||||
import { Groupview, GroupOptions } from './groupview';
|
||||
import {
|
||||
GridviewPanelApi,
|
||||
GridviewPanelApiImpl,
|
||||
} from '../api/gridviewPanelApi';
|
||||
import { Groupview, GroupOptions, IHeader } from './groupview';
|
||||
import { GridviewPanel, IGridviewPanel } from '../gridview/gridviewPanel';
|
||||
import { IGroupPanel } from './groupPanel';
|
||||
|
||||
export interface IGroupviewPanel extends IGridviewPanel {
|
||||
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 {
|
||||
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 {
|
||||
return this._model;
|
||||
}
|
||||
|
||||
get minimumHeight() {
|
||||
get minimumHeight(): number {
|
||||
return this._model.minimumHeight;
|
||||
}
|
||||
|
||||
get maximumHeight() {
|
||||
get maximumHeight(): number {
|
||||
return this._model.maximumHeight;
|
||||
}
|
||||
|
||||
get minimumWidth() {
|
||||
get minimumWidth(): number {
|
||||
return this._model.minimumWidth;
|
||||
}
|
||||
|
||||
get maximumWidth() {
|
||||
get maximumWidth(): number {
|
||||
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(
|
||||
accessor: IDockviewComponent,
|
||||
id: string,
|
||||
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);
|
||||
}
|
||||
|
||||
initialize() {
|
||||
this.model.initialize();
|
||||
this._model.initialize();
|
||||
}
|
||||
|
||||
setActive(isActive: boolean): void {
|
||||
|
@ -33,6 +33,7 @@ export interface ITabsContainer extends IDisposable {
|
||||
closePanel: (panel: IGroupPanel) => void;
|
||||
openPanel: (panel: IGroupPanel, index?: number) => void;
|
||||
setActionElement(element: HTMLElement | undefined): void;
|
||||
hidden: boolean;
|
||||
show(): void;
|
||||
hide(): void;
|
||||
}
|
||||
@ -55,6 +56,7 @@ export class TabsContainer
|
||||
private actions: HTMLElement | undefined;
|
||||
|
||||
private _height: number | undefined;
|
||||
private _hidden = false;
|
||||
|
||||
private readonly _onDrop = new Emitter<TabDropIndexEvent>();
|
||||
readonly onDrop: Event<TabDropIndexEvent> = this._onDrop.event;
|
||||
@ -85,12 +87,23 @@ export class TabsContainer
|
||||
}
|
||||
}
|
||||
|
||||
show() {
|
||||
this.element.style.display = '';
|
||||
get hidden(): boolean {
|
||||
return this._hidden;
|
||||
}
|
||||
|
||||
hide() {
|
||||
this.element.style.display = 'none';
|
||||
set hidden(value: boolean) {
|
||||
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 {
|
||||
@ -252,7 +265,7 @@ export class TabsContainer
|
||||
tabToAdd.onChanged((event) => {
|
||||
const alreadyFocused =
|
||||
panel.id === this.group.model.activePanel?.id &&
|
||||
this.group.model.isContentFocused();
|
||||
this.group.model.isContentFocused;
|
||||
this.accessor.fireMouseEvent({ ...event, panel, tab: true });
|
||||
|
||||
const isLeftClick = event.event.button === 0;
|
||||
|
@ -1,5 +1,4 @@
|
||||
export * from './dnd/dataTransfer';
|
||||
export * from './api/component.api';
|
||||
|
||||
export * from './splitview/core/splitview';
|
||||
export * from './paneview/paneview';
|
||||
@ -50,3 +49,10 @@ export {
|
||||
SplitviewPanelApi,
|
||||
} from './api/splitviewPanelApi';
|
||||
export { ExpansionEvent, PaneviewPanelApi } from './api/paneviewPanelApi';
|
||||
export {
|
||||
CommonApi,
|
||||
SplitviewApi,
|
||||
PaneviewApi,
|
||||
GridviewApi,
|
||||
DockviewApi,
|
||||
} from './api/component.api';
|
||||
|
@ -6,11 +6,15 @@ import { createComponent } from '../panel/componentFactory';
|
||||
import { DockviewApi } from '../api/component.api';
|
||||
import { DefaultTab } from '../dockview/components/tab/defaultTab';
|
||||
import { DefaultGroupPanelView } from '../dockview/defaultGroupPanelView';
|
||||
import { GroupviewPanel } from '../groupview/groupviewPanel';
|
||||
|
||||
export class ReactPanelDeserialzier implements IPanelDeserializer {
|
||||
constructor(private readonly layout: DockviewComponent) {}
|
||||
|
||||
public fromJSON(panelData: GroupviewPanelState): IGroupPanel {
|
||||
public fromJSON(
|
||||
panelData: GroupviewPanelState,
|
||||
group: GroupviewPanel
|
||||
): IGroupPanel {
|
||||
const panelId = panelData.id;
|
||||
const params = panelData.params;
|
||||
const title = panelData.title;
|
||||
@ -39,7 +43,8 @@ export class ReactPanelDeserialzier implements IPanelDeserializer {
|
||||
const panel = new DockviewGroupPanel(
|
||||
panelId,
|
||||
this.layout,
|
||||
new DockviewApi(this.layout)
|
||||
new DockviewApi(this.layout),
|
||||
group
|
||||
);
|
||||
|
||||
panel.init({
|
||||
|
Loading…
x
Reference in New Issue
Block a user