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/
test-report.xml
*.code-workspace
yarn-error.log

View File

@ -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",

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(() => {
element = document.createElement('div');
jest.spyOn(element, 'clientHeight', 'get').mockImplementation(
() => 100
);
jest.spyOn(element, 'clientWidth', 'get').mockImplementation(() => 200);
});
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: {
views: ['panel2', 'panel3'],
id: 'group-2',
activeView: 'panel2',
activeView: 'panel3',
},
size: 500,
},

View File

@ -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 {

View File

@ -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,
});
});
});

View File

@ -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,

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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 {

View File

@ -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);

View File

@ -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;
}

View File

@ -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;

View File

@ -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(),
});
}
}

View File

@ -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 {

View File

@ -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;

View File

@ -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';

View File

@ -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({