feat: group controls renderer

This commit is contained in:
mathuo 2022-06-11 18:12:58 +01:00
parent a92fb3f554
commit a7f69fc617
No known key found for this signature in database
GPG Key ID: C6EEDEFD6CA07281
13 changed files with 314 additions and 185 deletions

View File

@ -1,9 +1,5 @@
import { DefaultGroupPanelView } from '../../dockview/defaultGroupPanelView'; import { DefaultGroupPanelView } from '../../dockview/defaultGroupPanelView';
import { import { IContentRenderer, ITabRenderer } from '../../groupview/types';
IActionsRenderer,
IContentRenderer,
ITabRenderer,
} from '../../groupview/types';
describe('defaultGroupPanelView', () => { describe('defaultGroupPanelView', () => {
test('dispose cleanup', () => { test('dispose cleanup', () => {
@ -23,24 +19,14 @@ describe('defaultGroupPanelView', () => {
return partial as IContentRenderer; return partial as IContentRenderer;
}); });
const actionsMock = jest.fn<IActionsRenderer, []>(() => {
const partial: Partial<IContentRenderer> = {
element: document.createElement('div'),
dispose: jest.fn(),
};
return partial as IContentRenderer;
});
const content = new contentMock(); const content = new contentMock();
const tab = new tabMock(); const tab = new tabMock();
const actions = new actionsMock();
const cut = new DefaultGroupPanelView({ content, tab, actions }); const cut = new DefaultGroupPanelView({ content, tab });
cut.dispose(); cut.dispose();
expect(content.dispose).toHaveBeenCalled(); expect(content.dispose).toHaveBeenCalled();
expect(tab.dispose).toHaveBeenCalled(); expect(tab.dispose).toHaveBeenCalled();
expect(actions.dispose).toHaveBeenCalled();
}); });
}); });

View File

@ -152,7 +152,7 @@ class TestGroupPanel implements IDockviewPanel {
public readonly title: string, public readonly title: string,
accessor: DockviewComponent accessor: DockviewComponent
) { ) {
this.api = new DockviewPanelApiImpl(this, this._group); this.api = new DockviewPanelApiImpl(this, this._group!);
this._group = new GroupPanel(accessor, id, {}); this._group = new GroupPanel(accessor, id, {});
this.view = new TestGroupPanelView( this.view = new TestGroupPanelView(
new PanelContentPartTest(id, 'component') new PanelContentPartTest(id, 'component')
@ -162,9 +162,8 @@ class TestGroupPanel implements IDockviewPanel {
get params(): Record<string, any> { get params(): Record<string, any> {
return {}; return {};
} }
get group(): GroupPanel {
get group(): GroupPanel | undefined { return this._group!;
return this._group;
} }
updateParentGroup(group: GroupPanel, isGroupActive: boolean): void { updateParentGroup(group: GroupPanel, isGroupActive: boolean): void {

View File

@ -11,11 +11,7 @@ import {
IWatermarkRenderer, IWatermarkRenderer,
} from '../../groupview/types'; } from '../../groupview/types';
import { PanelUpdateEvent } from '../../panel/types'; import { PanelUpdateEvent } from '../../panel/types';
import { import { GroupOptions, Groupview } from '../../groupview/groupview';
GroupChangeKind2,
GroupOptions,
Groupview,
} from '../../groupview/groupview';
import { DockviewPanelApi } from '../../api/groupPanelApi'; import { DockviewPanelApi } from '../../api/groupPanelApi';
import { import {
DefaultGroupPanelView, DefaultGroupPanelView,
@ -24,6 +20,13 @@ import {
import { GroupPanel } from '../../groupview/groupviewPanel'; import { GroupPanel } from '../../groupview/groupviewPanel';
import { fireEvent } from '@testing-library/dom'; import { fireEvent } from '@testing-library/dom';
import { LocalSelectionTransfer, PanelTransfer } from '../../dnd/dataTransfer'; import { LocalSelectionTransfer, PanelTransfer } from '../../dnd/dataTransfer';
import { CompositeDisposable } from '../../lifecycle';
enum GroupChangeKind2 {
ADD_PANEL,
REMOVE_PANEL,
PANEL_ACTIVE,
}
class Watermark implements IWatermarkRenderer { class Watermark implements IWatermarkRenderer {
public readonly element = document.createElement('div'); public readonly element = document.createElement('div');
@ -193,7 +196,7 @@ export class TestPanel implements IDockviewPanel {
toJSON(): GroupviewPanelState { toJSON(): GroupviewPanelState {
return { return {
id: this.id, id: this.id,
view: this._view.toJSON(), view: this._view?.toJSON(),
title: this._params?.title, title: this._params?.title,
}; };
} }
@ -269,10 +272,29 @@ describe('groupview', () => {
const events: Array<{ const events: Array<{
kind: GroupChangeKind2; kind: GroupChangeKind2;
panel?: IDockviewPanel;
}> = []; }> = [];
const disposable = groupview2.model.onDidGroupChange((e) => {
events.push(e); const disposable = new CompositeDisposable(
groupview2.model.onDidAddPanel((e) => {
events.push({
kind: GroupChangeKind2.ADD_PANEL,
panel: e.panel,
}); });
}),
groupview2.model.onDidRemovePanel((e) => {
events.push({
kind: GroupChangeKind2.REMOVE_PANEL,
panel: e.panel,
});
}),
groupview2.model.onDidActivePanelChange((e) => {
events.push({
kind: GroupChangeKind2.PANEL_ACTIVE,
panel: e.panel,
});
})
);
groupview2.initialize(); groupview2.initialize();
@ -301,10 +323,29 @@ describe('groupview', () => {
test('panel events flow', () => { test('panel events flow', () => {
let events: Array<{ let events: Array<{
kind: GroupChangeKind2; kind: GroupChangeKind2;
panel?: IDockviewPanel;
}> = []; }> = [];
const disposable = groupview.model.onDidGroupChange((e) => {
events.push(e); const disposable = new CompositeDisposable(
groupview.model.onDidAddPanel((e) => {
events.push({
kind: GroupChangeKind2.ADD_PANEL,
panel: e.panel,
}); });
}),
groupview.model.onDidRemovePanel((e) => {
events.push({
kind: GroupChangeKind2.REMOVE_PANEL,
panel: e.panel,
});
}),
groupview.model.onDidActivePanelChange((e) => {
events.push({
kind: GroupChangeKind2.PANEL_ACTIVE,
panel: e.panel,
});
})
);
const panel1 = new TestPanel('panel1', jest.fn() as any); const panel1 = new TestPanel('panel1', jest.fn() as any);
const panel2 = new TestPanel('panel2', jest.fn() as any); const panel2 = new TestPanel('panel2', jest.fn() as any);

View File

@ -1,7 +1,6 @@
import { DefaultTab } from './components/tab/defaultTab'; import { DefaultTab } from './components/tab/defaultTab';
import { import {
GroupPanelPartInitParameters, GroupPanelPartInitParameters,
IActionsRenderer,
IContentRenderer, IContentRenderer,
ITabRenderer, ITabRenderer,
} from '../groupview/types'; } from '../groupview/types';
@ -12,7 +11,6 @@ import { GroupPanelUpdateEvent } from '../groupview/groupPanel';
export interface IGroupPanelView extends IDisposable { export interface IGroupPanelView extends IDisposable {
readonly content: IContentRenderer; readonly content: IContentRenderer;
readonly tab?: ITabRenderer; readonly tab?: ITabRenderer;
readonly actions?: IActionsRenderer;
update(event: GroupPanelUpdateEvent): void; update(event: GroupPanelUpdateEvent): void;
layout(width: number, height: number): void; layout(width: number, height: number): void;
init(params: GroupPanelPartInitParameters): void; init(params: GroupPanelPartInitParameters): void;
@ -23,37 +21,18 @@ export interface IGroupPanelView extends IDisposable {
export class DefaultGroupPanelView implements IGroupPanelView { export class DefaultGroupPanelView implements IGroupPanelView {
private readonly _content: IContentRenderer; private readonly _content: IContentRenderer;
private readonly _tab: ITabRenderer; private readonly _tab: ITabRenderer;
private readonly _actions: IActionsRenderer | undefined;
get content() { get content(): IContentRenderer {
return this._content; return this._content;
} }
get tab() { get tab(): ITabRenderer {
return this._tab; return this._tab;
} }
get actions() { constructor(renderers: { content: IContentRenderer; tab?: ITabRenderer }) {
return this._actions;
}
constructor(renderers: {
content: IContentRenderer;
tab?: ITabRenderer;
actions?: IActionsRenderer;
}) {
this._content = renderers.content; this._content = renderers.content;
this._tab = renderers.tab ?? new DefaultTab(); this._tab = renderers.tab ?? new DefaultTab();
this._actions =
renderers.actions ||
(this.content.actions
? {
element: this.content.actions,
dispose: () => {
//
},
}
: undefined);
} }
init(params: GroupPanelPartInitParameters): void { init(params: GroupPanelPartInitParameters): void {
@ -85,6 +64,5 @@ export class DefaultGroupPanelView implements IGroupPanelView {
dispose(): void { dispose(): void {
this.content.dispose(); this.content.dispose();
this.tab.dispose(); this.tab.dispose();
this.actions?.dispose();
} }
} }

View File

@ -36,7 +36,6 @@ import { LayoutMouseEvent, MouseEventKind } from '../groupview/tab';
import { Orientation } from '../splitview/core/splitview'; import { Orientation } from '../splitview/core/splitview';
import { DefaultTab } from './components/tab/defaultTab'; import { DefaultTab } from './components/tab/defaultTab';
import { import {
GroupChangeKind2,
GroupOptions, GroupOptions,
GroupPanelViewState, GroupPanelViewState,
GroupviewDropEvent, GroupviewDropEvent,
@ -73,6 +72,7 @@ export type DockviewComponentUpdateOptions = Pick<
| 'showDndOverlay' | 'showDndOverlay'
| 'watermarkFrameworkComponent' | 'watermarkFrameworkComponent'
| 'defaultTabComponent' | 'defaultTabComponent'
| 'createGroupControlElement'
>; >;
export interface DockviewDropEvent extends GroupviewDropEvent { export interface DockviewDropEvent extends GroupviewDropEvent {
@ -716,22 +716,14 @@ export class DockviewComponent
view.model.onDidDrop((event) => { view.model.onDidDrop((event) => {
this._onDidDrop.fire({ ...event, api: this._api, group: view }); this._onDidDrop.fire({ ...event, api: this._api, group: view });
}), }),
view.model.onDidGroupChange((event) => { view.model.onDidAddPanel((event) => {
switch (event.kind) {
case GroupChangeKind2.ADD_PANEL:
if (event.panel) {
this._onDidAddPanel.fire(event.panel); this._onDidAddPanel.fire(event.panel);
} }),
break; view.model.onDidRemovePanel((event) => {
case GroupChangeKind2.REMOVE_PANEL:
if (event.panel) {
this._onDidRemovePanel.fire(event.panel); this._onDidRemovePanel.fire(event.panel);
} }),
break; view.model.onDidActivePanelChange((event) => {
case GroupChangeKind2.PANEL_ACTIVE:
this._onDidActivePanelChange.fire(event.panel); this._onDidActivePanelChange.fire(event.panel);
break;
}
}) })
); );

View File

@ -13,6 +13,7 @@ import { ISplitviewStyles, Orientation } from '../splitview/core/splitview';
import { FrameworkFactory } from '../types'; import { FrameworkFactory } from '../types';
import { DockviewDropTargets } from '../groupview/dnd'; import { DockviewDropTargets } from '../groupview/dnd';
import { PanelTransfer } from '../dnd/dataTransfer'; import { PanelTransfer } from '../dnd/dataTransfer';
import { IGroupControlRenderer } from '../react/dockview/groupControlsRenderer';
export interface GroupPanelFrameworkComponentFactory { export interface GroupPanelFrameworkComponentFactory {
content: FrameworkFactory<IContentRenderer>; content: FrameworkFactory<IContentRenderer>;
@ -66,6 +67,7 @@ export interface DockviewComponentOptions extends DockviewRenderFunctions {
styles?: ISplitviewStyles; styles?: ISplitviewStyles;
defaultTabComponent?: string; defaultTabComponent?: string;
showDndOverlay?: (event: DockviewDndOverlayEvent) => boolean; showDndOverlay?: (event: DockviewDndOverlayEvent) => boolean;
createGroupControlElement?: (group: GroupPanel) => IGroupControlRenderer;
} }
export interface PanelOptions { export interface PanelOptions {

View File

@ -14,12 +14,7 @@ import { ITabsContainer, TabsContainer } from './titlebar/tabsContainer';
import { IWatermarkRenderer } from './types'; import { IWatermarkRenderer } from './types';
import { GroupPanel } from './groupviewPanel'; import { GroupPanel } from './groupviewPanel';
import { DockviewDropTargets } from './dnd'; import { DockviewDropTargets } from './dnd';
import { IGroupControlRenderer } from '../react/dockview/groupControlsRenderer';
export enum GroupChangeKind2 {
ADD_PANEL = 'ADD_PANEL',
REMOVE_PANEL = 'REMOVE_PANEL',
PANEL_ACTIVE = 'PANEL_ACTIVE',
}
export interface DndService { export interface DndService {
canDisplayOverlay( canDisplayOverlay(
@ -67,15 +62,14 @@ export interface GroupPanelViewState extends CoreGroupOptions {
} }
export interface GroupviewChangeEvent { export interface GroupviewChangeEvent {
readonly kind: GroupChangeKind2; readonly panel: IDockviewPanel;
readonly panel?: IDockviewPanel;
} }
export interface GroupviewDropEvent { export interface GroupviewDropEvent {
nativeEvent: DragEvent; readonly nativeEvent: DragEvent;
position: Position; readonly position: Position;
readonly index?: number;
getData(): PanelTransfer | undefined; getData(): PanelTransfer | undefined;
index?: number;
} }
export interface IHeader { export interface IHeader {
@ -91,7 +85,9 @@ export interface IGroupview extends IDisposable, IGridPanelView {
readonly header: IHeader; readonly header: IHeader;
readonly isContentFocused: boolean; readonly isContentFocused: boolean;
readonly onDidDrop: Event<GroupviewDropEvent>; readonly onDidDrop: Event<GroupviewDropEvent>;
readonly onDidGroupChange: Event<GroupviewChangeEvent>; readonly onDidAddPanel: Event<GroupviewChangeEvent>;
readonly onDidRemovePanel: Event<GroupviewChangeEvent>;
readonly onDidActivePanelChange: Event<GroupviewChangeEvent>;
readonly onMove: Event<GroupMoveEvent>; readonly onMove: Event<GroupMoveEvent>;
locked: boolean; locked: boolean;
// state // state
@ -121,10 +117,11 @@ export class Groupview extends CompositeDisposable implements IGroupview {
private readonly tabsContainer: ITabsContainer; private readonly tabsContainer: ITabsContainer;
private readonly contentContainer: IContentContainer; private readonly contentContainer: IContentContainer;
private readonly dropTarget: Droptarget; private readonly dropTarget: Droptarget;
private _activePanel?: IDockviewPanel; private _activePanel: IDockviewPanel | undefined;
private watermark?: IWatermarkRenderer; private watermark?: IWatermarkRenderer;
private _isGroupActive = false; private _isGroupActive = false;
private _locked = false; private _locked = false;
private _control: IGroupControlRenderer | undefined;
private mostRecentlyUsed: IDockviewPanel[] = []; private mostRecentlyUsed: IDockviewPanel[] = [];
@ -140,13 +137,22 @@ export class Groupview extends CompositeDisposable implements IGroupview {
private readonly _onMove = new Emitter<GroupMoveEvent>(); private readonly _onMove = new Emitter<GroupMoveEvent>();
readonly onMove: Event<GroupMoveEvent> = this._onMove.event; readonly onMove: Event<GroupMoveEvent> = this._onMove.event;
private readonly _onDidGroupChange = new Emitter<GroupviewChangeEvent>();
readonly onDidGroupChange: Event<GroupviewChangeEvent> =
this._onDidGroupChange.event;
private readonly _onDidDrop = new Emitter<GroupviewDropEvent>(); private readonly _onDidDrop = new Emitter<GroupviewDropEvent>();
readonly onDidDrop: Event<GroupviewDropEvent> = this._onDidDrop.event; readonly onDidDrop: Event<GroupviewDropEvent> = this._onDidDrop.event;
private readonly _onDidAddPanel = new Emitter<GroupviewChangeEvent>();
readonly onDidAddPanel: Event<GroupviewChangeEvent> =
this._onDidAddPanel.event;
private readonly _onDidRemovePanel = new Emitter<GroupviewChangeEvent>();
readonly onDidRemovePanel: Event<GroupviewChangeEvent> =
this._onDidRemovePanel.event;
private readonly _onDidActivePanelChange =
new Emitter<GroupviewChangeEvent>();
readonly onDidActivePanelChange: Event<GroupviewChangeEvent> =
this._onDidActivePanelChange.event;
get element(): HTMLElement { get element(): HTMLElement {
throw new Error('not supported'); throw new Error('not supported');
} }
@ -226,16 +232,10 @@ export class Groupview extends CompositeDisposable implements IGroupview {
this.container.classList.add('groupview'); this.container.classList.add('groupview');
this.addDisposables(
this._onMove,
this._onDidGroupChange,
this._onDidChange,
this._onDidDrop
);
this.tabsContainer = new TabsContainer(this.accessor, this.parent, { this.tabsContainer = new TabsContainer(this.accessor, this.parent, {
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, {
@ -268,7 +268,11 @@ export class Groupview extends CompositeDisposable implements IGroupview {
this.addDisposables( this.addDisposables(
this._onMove, this._onMove,
this._onDidGroupChange, this._onDidChange,
this._onDidDrop,
this._onDidAddPanel,
this._onDidRemovePanel,
this._onDidActivePanelChange,
this.tabsContainer.onDrop((event) => { this.tabsContainer.onDrop((event) => {
this.handleDropEvent(event.event, Position.Center, event.index); this.handleDropEvent(event.event, Position.Center, event.index);
}), }),
@ -284,7 +288,7 @@ export class Groupview extends CompositeDisposable implements IGroupview {
); );
} }
initialize() { initialize(): void {
if (this.options?.panels) { if (this.options?.panels) {
this.options.panels.forEach((panel) => { this.options.panels.forEach((panel) => {
this.doAddPanel(panel); this.doAddPanel(panel);
@ -299,9 +303,21 @@ export class Groupview extends CompositeDisposable implements IGroupview {
// correctly initialized // correctly initialized
this.setActive(this.isActive, true, true); this.setActive(this.isActive, true, true);
this.updateContainer(); this.updateContainer();
if (this.accessor.options.createGroupControlElement) {
this._control = this.accessor.options.createGroupControlElement(
this.parent
);
this.addDisposables(this._control);
this._control.init({
containerApi: new DockviewApi(this.accessor),
api: this.parent.api,
});
this.tabsContainer.setActionElement(this._control.element);
}
} }
public indexOf(panel: IDockviewPanel) { public indexOf(panel: IDockviewPanel): number {
return this.tabsContainer.indexOf(panel.id); return this.tabsContainer.indexOf(panel.id);
} }
@ -326,7 +342,7 @@ export class Groupview extends CompositeDisposable implements IGroupview {
public moveToNext(options?: { public moveToNext(options?: {
panel?: IDockviewPanel; panel?: IDockviewPanel;
suppressRoll?: boolean; suppressRoll?: boolean;
}) { }): void {
if (!options) { if (!options) {
options = {}; options = {};
} }
@ -352,7 +368,7 @@ export class Groupview extends CompositeDisposable implements IGroupview {
public moveToPrevious(options?: { public moveToPrevious(options?: {
panel?: IDockviewPanel; panel?: IDockviewPanel;
suppressRoll?: boolean; suppressRoll?: boolean;
}) { }): void {
if (!options) { if (!options) {
options = {}; options = {};
} }
@ -379,19 +395,19 @@ export class Groupview extends CompositeDisposable implements IGroupview {
this.openPanel(this.panels[normalizedIndex]); this.openPanel(this.panels[normalizedIndex]);
} }
public containsPanel(panel: IDockviewPanel) { public containsPanel(panel: IDockviewPanel): boolean {
return this.panels.includes(panel); return this.panels.includes(panel);
} }
init(_params: PanelInitParameters) { init(_params: PanelInitParameters): void {
//noop //noop
} }
update(_params: PanelUpdateEvent) { update(_params: PanelUpdateEvent): void {
//noop //noop
} }
focus() { focus(): void {
this._activePanel?.focus(); this._activePanel?.focus();
} }
@ -403,7 +419,7 @@ export class Groupview extends CompositeDisposable implements IGroupview {
skipSetPanelActive?: boolean; skipSetPanelActive?: boolean;
skipSetGroupActive?: boolean; skipSetGroupActive?: boolean;
} = {} } = {}
) { ): void {
if ( if (
typeof options.index !== 'number' || typeof options.index !== 'number' ||
options.index > this.panels.length options.index > this.panels.length
@ -452,7 +468,7 @@ export class Groupview extends CompositeDisposable implements IGroupview {
return this._removePanel(panelToRemove); return this._removePanel(panelToRemove);
} }
public closeAllPanels() { public closeAllPanels(): void {
if (this.panels.length > 0) { if (this.panels.length > 0) {
// take a copy since we will be edting the array as we iterate through // take a copy since we will be edting the array as we iterate through
const arrPanelCpy = [...this.panels]; const arrPanelCpy = [...this.panels];
@ -468,25 +484,23 @@ export class Groupview extends CompositeDisposable implements IGroupview {
this.doClose(panel); this.doClose(panel);
} }
private doClose(panel: IDockviewPanel) { private doClose(panel: IDockviewPanel): void {
this.accessor.removePanel(panel); this.accessor.removePanel(panel);
} }
public isPanelActive(panel: IDockviewPanel) { public isPanelActive(panel: IDockviewPanel): boolean {
return this._activePanel === panel; return this._activePanel === panel;
} }
updateActions() { updateActions(element: HTMLElement | undefined): void {
if (this.isActive && this._activePanel?.view?.actions) { this.tabsContainer.setActionElement(element);
this.tabsContainer.setActionElement(
this._activePanel.view.actions.element
);
} else {
this.tabsContainer.setActionElement(undefined);
}
} }
public setActive(isGroupActive: boolean, skipFocus = false, force = false) { public setActive(
isGroupActive: boolean,
skipFocus = false,
force = false
): void {
if (!force && this.isActive === isGroupActive) { if (!force && this.isActive === isGroupActive) {
if (!skipFocus) { if (!skipFocus) {
this._activePanel?.focus(); this._activePanel?.focus();
@ -514,7 +528,7 @@ export class Groupview extends CompositeDisposable implements IGroupview {
} }
} }
public layout(width: number, height: number) { public layout(width: number, height: number): void {
this._width = width; this._width = width;
this._height = height; this._height = height;
@ -525,7 +539,7 @@ export class Groupview extends CompositeDisposable implements IGroupview {
} }
} }
private _removePanel(panel: IDockviewPanel) { private _removePanel(panel: IDockviewPanel): IDockviewPanel {
const isActivePanel = this._activePanel === panel; const isActivePanel = this._activePanel === panel;
this.doRemovePanel(panel); this.doRemovePanel(panel);
@ -543,7 +557,7 @@ export class Groupview extends CompositeDisposable implements IGroupview {
return panel; return panel;
} }
private doRemovePanel(panel: IDockviewPanel) { private doRemovePanel(panel: IDockviewPanel): void {
const index = this.panels.indexOf(panel); const index = this.panels.indexOf(panel);
if (this._activePanel === panel) { if (this._activePanel === panel) {
@ -560,17 +574,14 @@ export class Groupview extends CompositeDisposable implements IGroupview {
); );
} }
this._onDidGroupChange.fire({ this._onDidRemovePanel.fire({ panel });
kind: GroupChangeKind2.REMOVE_PANEL,
panel,
});
} }
private doAddPanel( private doAddPanel(
panel: IDockviewPanel, panel: IDockviewPanel,
index: number = this.panels.length, index: number = this.panels.length,
skipSetActive = false skipSetActive = false
) { ): void {
const existingPanel = this._panels.indexOf(panel); const existingPanel = this._panels.indexOf(panel);
const hasExistingPanel = existingPanel > -1; const hasExistingPanel = existingPanel > -1;
@ -591,13 +602,10 @@ export class Groupview extends CompositeDisposable implements IGroupview {
this.updateMru(panel); this.updateMru(panel);
this.panels.splice(index, 0, panel); this.panels.splice(index, 0, panel);
this._onDidGroupChange.fire({ this._onDidAddPanel.fire({ panel });
kind: GroupChangeKind2.ADD_PANEL,
panel,
});
} }
private doSetActivePanel(panel: IDockviewPanel | undefined) { private doSetActivePanel(panel: IDockviewPanel | undefined): void {
this._activePanel = panel; this._activePanel = panel;
if (panel) { if (panel) {
@ -607,14 +615,11 @@ export class Groupview extends CompositeDisposable implements IGroupview {
this.updateMru(panel); this.updateMru(panel);
this._onDidGroupChange.fire({ this._onDidActivePanelChange.fire({ panel });
kind: GroupChangeKind2.PANEL_ACTIVE,
panel,
});
} }
} }
private updateMru(panel: IDockviewPanel) { private updateMru(panel: IDockviewPanel): void {
if (this.mostRecentlyUsed.includes(panel)) { if (this.mostRecentlyUsed.includes(panel)) {
this.mostRecentlyUsed.splice( this.mostRecentlyUsed.splice(
this.mostRecentlyUsed.indexOf(panel), this.mostRecentlyUsed.indexOf(panel),
@ -624,8 +629,7 @@ export class Groupview extends CompositeDisposable implements IGroupview {
this.mostRecentlyUsed = [panel, ...this.mostRecentlyUsed]; this.mostRecentlyUsed = [panel, ...this.mostRecentlyUsed];
} }
private updateContainer() { private updateContainer(): void {
this.updateActions();
toggleClass(this.container, 'empty', this.isEmpty); toggleClass(this.container, 'empty', this.isEmpty);
this.panels.forEach((panel) => this.panels.forEach((panel) =>
@ -680,7 +684,7 @@ export class Groupview extends CompositeDisposable implements IGroupview {
event: DragEvent, event: DragEvent,
position: Position, position: Position,
index?: number index?: number
) { ): void {
const data = getPanelData(); const data = getPanelData();
if (data) { if (data) {
@ -716,7 +720,7 @@ export class Groupview extends CompositeDisposable implements IGroupview {
} }
} }
public dispose() { public dispose(): void {
super.dispose(); super.dispose();
this.watermark?.dispose(); this.watermark?.dispose();

View File

@ -1,4 +1,3 @@
import { IDisposable } from '../lifecycle';
import { IDockviewComponent } from '../dockview/dockviewComponent'; import { IDockviewComponent } from '../dockview/dockviewComponent';
import { DockviewPanelApi } from '../api/groupPanelApi'; import { DockviewPanelApi } from '../api/groupPanelApi';
import { PanelInitParameters, IPanel } from '../panel/types'; import { PanelInitParameters, IPanel } from '../panel/types';
@ -42,10 +41,6 @@ export interface ITabRenderer extends IPanel {
updateParentGroup(group: GroupPanel, isPanelVisible: boolean): void; updateParentGroup(group: GroupPanel, isPanelVisible: boolean): void;
} }
export interface IActionsRenderer extends IDisposable {
readonly element: HTMLElement;
}
export interface IContentRenderer extends IPanel { export interface IContentRenderer extends IPanel {
readonly element: HTMLElement; readonly element: HTMLElement;
readonly actions?: HTMLElement; readonly actions?: HTMLElement;

View File

@ -12,15 +12,36 @@ import {
TabContextMenuEvent, TabContextMenuEvent,
} from '../../dockview/options'; } from '../../dockview/options';
import { DockviewPanelApi } from '../../api/groupPanelApi'; import { DockviewPanelApi } from '../../api/groupPanelApi';
import { usePortalsLifecycle } from '../react'; import { ReactPortalStore, usePortalsLifecycle } from '../react';
import { DockviewApi } from '../../api/component.api'; import { DockviewApi } from '../../api/component.api';
import { IWatermarkPanelProps, ReactWatermarkPart } from './reactWatermarkPart'; import { IWatermarkPanelProps, ReactWatermarkPart } from './reactWatermarkPart';
import { PanelCollection, PanelParameters } from '../types'; import { PanelCollection, PanelParameters } from '../types';
import { watchElementResize } from '../../dom'; import { watchElementResize } from '../../dom';
import { IContentRenderer, ITabRenderer } from '../../groupview/types'; import { IContentRenderer, ITabRenderer } from '../../groupview/types';
import {
IDockviewGroupControlProps,
IGroupControlRenderer,
ReactGroupControlsRendererPart,
} from './groupControlsRenderer';
import { GroupPanel } from '../../groupview/groupviewPanel';
export const DEFAULT_TAB_IDENTIFIER = '__default__tab__'; export const DEFAULT_TAB_IDENTIFIER = '__default__tab__';
function createGroupControlElement(
component: React.FunctionComponent<IDockviewGroupControlProps> | undefined,
store: ReactPortalStore
): ((groupPanel: GroupPanel) => IGroupControlRenderer) | undefined {
return component
? (groupPanel: GroupPanel) => {
return new ReactGroupControlsRendererPart(
component,
store,
groupPanel
);
}
: undefined;
}
export interface IGroupPanelBaseProps<T extends {} = Record<string, any>> export interface IGroupPanelBaseProps<T extends {} = Record<string, any>>
extends PanelParameters<T> { extends PanelParameters<T> {
api: DockviewPanelApi; api: DockviewPanelApi;
@ -50,6 +71,7 @@ export interface IDockviewReactProps {
className?: string; className?: string;
disableAutoResizing?: boolean; disableAutoResizing?: boolean;
defaultTabComponent?: React.FunctionComponent<IDockviewPanelHeaderProps>; defaultTabComponent?: React.FunctionComponent<IDockviewPanelHeaderProps>;
groupControlComponent?: React.FunctionComponent<IDockviewGroupControlProps>;
} }
export const DockviewReact = React.forwardRef( export const DockviewReact = React.forwardRef(
@ -138,6 +160,10 @@ export const DockviewReact = React.forwardRef(
? { separatorBorder: 'transparent' } ? { separatorBorder: 'transparent' }
: undefined, : undefined,
showDndOverlay: props.showDndOverlay, showDndOverlay: props.showDndOverlay,
createGroupControlElement: createGroupControlElement(
props.groupControlComponent,
{ addPortal }
),
}); });
domRef.current?.appendChild(dockview.element); domRef.current?.appendChild(dockview.element);
@ -243,6 +269,18 @@ export const DockviewReact = React.forwardRef(
}); });
}, [props.defaultTabComponent]); }, [props.defaultTabComponent]);
React.useEffect(() => {
if (!dockviewRef.current) {
return;
}
dockviewRef.current.updateOptions({
createGroupControlElement: createGroupControlElement(
props.groupControlComponent,
{ addPortal }
),
});
}, [props.groupControlComponent]);
return ( return (
<div <div
className={props.className} className={props.className}

View File

@ -0,0 +1,117 @@
import * as React from 'react';
import { ReactPart, ReactPortalStore } from '../react';
import { PanelUpdateEvent } from '../../panel/types';
import { GroupPanel, GroupviewPanelApi } from '../../groupview/groupviewPanel';
import { DockviewApi } from '../../api/component.api';
import {
CompositeDisposable,
IDisposable,
MutableDisposable,
} from '../../lifecycle';
import { IDockviewPanel } from '../../groupview/groupPanel';
export interface IDockviewGroupControlProps {
api: GroupviewPanelApi;
containerApi: DockviewApi;
panels: IDockviewPanel[];
activePanel: IDockviewPanel | undefined;
isGroupActive: boolean;
}
export interface IGroupControlRenderer extends IDisposable {
readonly element: HTMLElement;
init(params: { containerApi: DockviewApi; api: GroupviewPanelApi }): void;
}
export class ReactGroupControlsRendererPart {
private mutableDisposable = new MutableDisposable();
private _element: HTMLElement;
private _part?: ReactPart<IDockviewGroupControlProps>;
get element(): HTMLElement {
return this._element;
}
get part(): ReactPart<IDockviewGroupControlProps> | undefined {
return this._part;
}
get group(): GroupPanel {
return this._group;
}
constructor(
private readonly component: React.FunctionComponent<IDockviewGroupControlProps>,
private readonly reactPortalStore: ReactPortalStore,
private readonly _group: GroupPanel
) {
this._element = document.createElement('div');
this._element.className = 'dockview-react-part';
}
focus() {
// TODO
}
public init(parameters: {
containerApi: DockviewApi;
api: GroupviewPanelApi;
}): void {
this.mutableDisposable.value = new CompositeDisposable(
this._group.model.onDidAddPanel(() => {
this.updatePanels();
}),
this._group.model.onDidRemovePanel(() => {
this.updatePanels();
}),
this._group.model.onDidActivePanelChange(() => {
this.updateActivePanel();
}),
parameters.api.onDidActiveChange(() => {
this.updateGroupActive();
})
);
this._part = new ReactPart(
this.element,
this.reactPortalStore,
this.component,
{
api: parameters.api,
containerApi: parameters.containerApi,
panels: this._group.model.panels,
activePanel: this._group.model.activePanel,
isGroupActive: this._group.api.isActive,
}
);
}
public update(event: PanelUpdateEvent) {
this._part?.update(event.params);
}
public dispose() {
this.mutableDisposable.dispose();
this._part?.dispose();
}
private updatePanels() {
this.update({ params: { panels: this._group.model.panels } });
}
private updateActivePanel() {
this.update({
params: {
activePanel: this._group.model.activePanel,
},
});
}
private updateGroupActive() {
this.update({
params: {
isGroupActive: this._group.api.isActive,
},
});
}
}

View File

@ -2,34 +2,17 @@ import * as React from 'react';
import { import {
IContentRenderer, IContentRenderer,
GroupPanelContentPartInitParameters, GroupPanelContentPartInitParameters,
ITabRenderer,
} from '../../groupview/types'; } from '../../groupview/types';
import { ReactPart, ReactPortalStore } from '../react'; import { ReactPart, ReactPortalStore } from '../react';
import { IDockviewPanelProps } from '../dockview/dockview'; import { IDockviewPanelProps } from '../dockview/dockview';
import { PanelUpdateEvent } from '../../panel/types'; import { PanelUpdateEvent } from '../../panel/types';
import { DockviewPanelApi } from '../../api/groupPanelApi';
import { DockviewApi } from '../../api/component.api';
import { GroupPanel } from '../../groupview/groupviewPanel'; import { GroupPanel } from '../../groupview/groupviewPanel';
import { Emitter, Event } from '../../events'; import { Emitter, Event } from '../../events';
export interface IGroupPanelActionbarProps {
api: DockviewPanelApi;
containerApi: DockviewApi;
}
export interface ReactContentPartContext {
api: DockviewPanelApi;
containerApi: DockviewApi;
actionsPortalElement: HTMLElement;
tabPortalElement: ITabRenderer;
}
export class ReactPanelContentPart implements IContentRenderer { export class ReactPanelContentPart implements IContentRenderer {
private _element: HTMLElement; private _element: HTMLElement;
private part?: ReactPart<IDockviewPanelProps>; private part?: ReactPart<IDockviewPanelProps>;
// //
private _actionsElement: HTMLElement;
private actionsPart?: ReactPart<any>;
private _group: GroupPanel | undefined; private _group: GroupPanel | undefined;
private readonly _onDidFocus = new Emitter<void>(); private readonly _onDidFocus = new Emitter<void>();
@ -42,10 +25,6 @@ export class ReactPanelContentPart implements IContentRenderer {
return this._element; return this._element;
} }
get actions(): HTMLElement {
return this._actionsElement;
}
constructor( constructor(
public readonly id: string, public readonly id: string,
private readonly component: React.FunctionComponent<IDockviewPanelProps>, private readonly component: React.FunctionComponent<IDockviewPanelProps>,
@ -53,9 +32,6 @@ export class ReactPanelContentPart implements IContentRenderer {
) { ) {
this._element = document.createElement('div'); this._element = document.createElement('div');
this._element.className = 'dockview-react-part'; this._element.className = 'dockview-react-part';
this._actionsElement = document.createElement('div');
this._actionsElement.className = 'dockview-react-part';
} }
focus() { focus() {
@ -63,13 +39,6 @@ export class ReactPanelContentPart implements IContentRenderer {
} }
public init(parameters: GroupPanelContentPartInitParameters): void { public init(parameters: GroupPanelContentPartInitParameters): void {
const context: ReactContentPartContext = {
api: parameters.api,
containerApi: parameters.containerApi,
actionsPortalElement: this._actionsElement,
tabPortalElement: parameters.tab,
};
this.part = new ReactPart( this.part = new ReactPart(
this.element, this.element,
this.reactPortalStore, this.reactPortalStore,
@ -78,8 +47,7 @@ export class ReactPanelContentPart implements IContentRenderer {
params: parameters.params, params: parameters.params,
api: parameters.api, api: parameters.api,
containerApi: parameters.containerApi, containerApi: parameters.containerApi,
}, }
context
); );
} }
@ -108,6 +76,5 @@ export class ReactPanelContentPart implements IContentRenderer {
this._onDidFocus.dispose(); this._onDidFocus.dispose();
this._onDidBlur.dispose(); this._onDidBlur.dispose();
this.part?.dispose(); this.part?.dispose();
this.actionsPart?.dispose();
} }
} }

View File

@ -2,8 +2,7 @@ export * from './dockview/dockview';
export * from './dockview/defaultTab'; export * from './dockview/defaultTab';
export * from './splitview/splitview'; export * from './splitview/splitview';
export * from './gridview/gridview'; export * from './gridview/gridview';
export * from './dockview/reactContentPart'; export { IDockviewGroupControlProps } from './dockview/groupControlsRenderer';
export * from './dockview/reactHeaderPart';
export { IWatermarkPanelProps } from './dockview/reactWatermarkPart'; export { IWatermarkPanelProps } from './dockview/reactWatermarkPart';
export * from './paneview/paneview'; export * from './paneview/paneview';
export * from './types'; export * from './types';

View File

@ -65,6 +65,7 @@ export const ReactPartContext = React.createContext<{}>({});
export class ReactPart<P extends object, C extends object = {}> export class ReactPart<P extends object, C extends object = {}>
implements IFrameworkPart implements IFrameworkPart
{ {
private _initialProps: Record<string, any> = {};
private componentInstance?: IPanelWrapperRef; private componentInstance?: IPanelWrapperRef;
private ref?: { portal: React.ReactPortal; disposable: IDisposable }; private ref?: { portal: React.ReactPortal; disposable: IDisposable };
private disposed = false; private disposed = false;
@ -84,7 +85,12 @@ export class ReactPart<P extends object, C extends object = {}>
throw new Error('invalid operation: resource is already disposed'); throw new Error('invalid operation: resource is already disposed');
} }
this.componentInstance?.update(props); if (!this.componentInstance) {
// if the component is yet to be mounted store the props
this._initialProps = { ...this._initialProps, ...props };
} else {
this.componentInstance.update(props);
}
} }
private createPortal() { private createPortal() {
@ -111,6 +117,11 @@ export class ReactPart<P extends object, C extends object = {}>
componentProps: this.parameters as unknown as {}, componentProps: this.parameters as unknown as {},
ref: (element: IPanelWrapperRef) => { ref: (element: IPanelWrapperRef) => {
this.componentInstance = element; this.componentInstance = element;
if (Object.keys(this._initialProps).length > 0) {
this.componentInstance.update(this._initialProps);
this._initialProps = {}; // don't keep a reference to the users object once no longer required
}
}, },
} }
); );