feat: persistance and rendering

This commit is contained in:
mathuo 2023-02-28 22:43:21 +08:00
parent 37b0a062a4
commit f26a1cd404
No known key found for this signature in database
GPG Key ID: C6EEDEFD6CA07281
17 changed files with 280 additions and 164 deletions

View File

@ -1,8 +1,9 @@
import { DefaultGroupPanelView } from '../../dockview/defaultGroupPanelView';
import { GroupPanel } from '../../groupview/groupviewPanel';
import { IContentRenderer, ITabRenderer } from '../../groupview/types';
describe('defaultGroupPanelView', () => {
test('dispose cleanup', () => {
test('that dispose is called on content and tab renderers when present', () => {
const contentMock = jest.fn<IContentRenderer, []>(() => {
const partial: Partial<IContentRenderer> = {
element: document.createElement('div'),
@ -22,11 +23,109 @@ describe('defaultGroupPanelView', () => {
const content = new contentMock();
const tab = new tabMock();
const cut = new DefaultGroupPanelView({ content, tab });
const cut = new DefaultGroupPanelView({
content,
tab,
contentComponent: 'contentComponent',
});
cut.dispose();
expect(content.dispose).toHaveBeenCalled();
expect(tab.dispose).toHaveBeenCalled();
});
test('that update is called on content and tab renderers when present', () => {
const contentMock = jest.fn<IContentRenderer, []>(() => {
const partial: Partial<IContentRenderer> = {
element: document.createElement('div'),
update: jest.fn(),
};
return partial as IContentRenderer;
});
const tabMock = jest.fn<ITabRenderer, []>(() => {
const partial: Partial<IContentRenderer> = {
element: document.createElement('div'),
update: jest.fn(),
};
return partial as IContentRenderer;
});
const content = new contentMock();
const tab = new tabMock();
const cut = new DefaultGroupPanelView({
content,
tab,
contentComponent: 'contentComponent',
});
cut.update({
params: {},
});
expect(content.update).toHaveBeenCalled();
expect(tab.update).toHaveBeenCalled();
});
test('test1', () => {
const contentMock = jest.fn<IContentRenderer, []>(() => {
const partial: Partial<IContentRenderer> = {
element: document.createElement('div'),
onGroupChange: jest.fn(),
onPanelVisibleChange: jest.fn(),
};
return partial as IContentRenderer;
});
const tabMock = jest.fn<ITabRenderer, []>(() => {
const partial: Partial<IContentRenderer> = {
element: document.createElement('div'),
onGroupChange: jest.fn(),
onPanelVisibleChange: jest.fn(),
};
return partial as IContentRenderer;
});
const content = new contentMock();
const tab = new tabMock();
let cut = new DefaultGroupPanelView({
content,
tab,
contentComponent: 'contentComponent',
});
const group1 = jest.fn() as any;
const group2 = jest.fn() as any;
cut.updateParentGroup(group1 as GroupPanel, false);
expect(content.onGroupChange).toHaveBeenNthCalledWith(1, group1);
expect(tab.onGroupChange).toHaveBeenNthCalledWith(1, group1);
expect(content.onPanelVisibleChange).toHaveBeenNthCalledWith(1, false);
expect(tab.onPanelVisibleChange).toHaveBeenNthCalledWith(1, false);
expect(content.onGroupChange).toHaveBeenCalledTimes(1);
expect(tab.onGroupChange).toHaveBeenCalledTimes(1);
expect(content.onPanelVisibleChange).toHaveBeenCalledTimes(1);
expect(tab.onPanelVisibleChange).toHaveBeenCalledTimes(1);
cut.updateParentGroup(group1 as GroupPanel, true);
expect(content.onPanelVisibleChange).toHaveBeenNthCalledWith(2, true);
expect(tab.onPanelVisibleChange).toHaveBeenNthCalledWith(2, true);
expect(content.onGroupChange).toHaveBeenCalledTimes(1);
expect(tab.onGroupChange).toHaveBeenCalledTimes(1);
expect(content.onPanelVisibleChange).toHaveBeenCalledTimes(2);
expect(tab.onPanelVisibleChange).toHaveBeenCalledTimes(2);
cut.updateParentGroup(group2 as GroupPanel, true);
expect(content.onGroupChange).toHaveBeenNthCalledWith(2, group2);
expect(tab.onGroupChange).toHaveBeenNthCalledWith(2, group2);
expect(content.onGroupChange).toHaveBeenCalledTimes(2);
expect(tab.onGroupChange).toHaveBeenCalledTimes(2);
expect(content.onPanelVisibleChange).toHaveBeenCalledTimes(2);
expect(tab.onPanelVisibleChange).toHaveBeenCalledTimes(2);
});
});

View File

@ -8,8 +8,6 @@ import { PanelUpdateEvent } from '../../../panel/types';
import { GroupPanel } from '../../../groupview/groupviewPanel';
import { createCloseButton } from '../../../svg';
export const DEFAULT_TAB_IDENTIFIER = '__default__tab__';
export class DefaultTab extends CompositeDisposable implements ITabRenderer {
private _element: HTMLElement;
@ -26,10 +24,6 @@ export class DefaultTab extends CompositeDisposable implements ITabRenderer {
return this._element;
}
get id() {
return DEFAULT_TAB_IDENTIFIER;
}
constructor() {
super();
@ -69,18 +63,13 @@ export class DefaultTab extends CompositeDisposable implements ITabRenderer {
this.render();
}
public toJSON() {
return { id: this.id };
}
focus() {
//noop
}
public init(params: GroupPanelPartInitParameters) {
this.params = params;
this._content.textContent =
typeof params.title === 'string' ? params.title : this.id;
this._content.textContent = params.title;
addDisposableListener(this.action, 'click', (ev) => {
ev.preventDefault(); //
@ -107,10 +96,7 @@ export class DefaultTab extends CompositeDisposable implements ITabRenderer {
private render() {
if (this._content.textContent !== this.params.title) {
this._content.textContent =
typeof this.params.title === 'string'
? this.params.title
: this.id;
this._content.textContent = this.params.title;
}
}
}

View File

@ -7,21 +7,32 @@ import {
} from '../groupview/types';
import { GroupPanel } from '../groupview/groupviewPanel';
import { IDisposable } from '../lifecycle';
import { createComponent } from '../panel/componentFactory';
import { IDockviewComponent } from './dockviewComponent';
export interface SerializedGroupPanelView {
tab?: { id: string };
content: { id: string };
}
export interface IGroupPanelView extends IDisposable {
readonly contentComponent: string;
readonly tabComponent?: string;
readonly content: IContentRenderer;
readonly tab?: ITabRenderer;
update(event: GroupPanelUpdateEvent): void;
layout(width: number, height: number): void;
init(params: GroupPanelPartInitParameters): void;
updateParentGroup(group: GroupPanel, isPanelVisible: boolean): void;
toJSON(): {};
}
export class DefaultGroupPanelView implements IGroupPanelView {
private readonly _content: IContentRenderer;
private readonly _tab: ITabRenderer;
private _group: GroupPanel | null = null;
private _isPanelVisible: boolean | null = null;
get content(): IContentRenderer {
return this._content;
}
@ -30,9 +41,15 @@ export class DefaultGroupPanelView implements IGroupPanelView {
return this._tab;
}
constructor(renderers: { content: IContentRenderer; tab?: ITabRenderer }) {
this._content = renderers.content;
this._tab = renderers.tab ?? new DefaultTab();
constructor(
private readonly accessor: IDockviewComponent,
private readonly id: string,
readonly contentComponent: string,
readonly tabComponent?: string
) {
this._content = this.createContentComponent(this.id, contentComponent);
this._tab =
this.createTabComponent(this.id, tabComponent) ?? new DefaultTab();
}
init(params: GroupPanelPartInitParameters): void {
@ -41,35 +58,65 @@ export class DefaultGroupPanelView implements IGroupPanelView {
}
updateParentGroup(group: GroupPanel, isPanelVisible: boolean): void {
this._content.updateParentGroup(group, isPanelVisible);
this._tab?.updateParentGroup(group, isPanelVisible);
if (group !== this._group) {
this._group = group;
if (this._content.onGroupChange) {
this._content.onGroupChange(group);
}
if (this._tab.onGroupChange) {
this._tab.onGroupChange(group);
}
}
if (isPanelVisible !== this._isPanelVisible) {
this._isPanelVisible = isPanelVisible;
if (this._content.onPanelVisibleChange) {
this._content.onPanelVisibleChange(isPanelVisible);
}
if (this._tab.onPanelVisibleChange) {
this._tab.onPanelVisibleChange(isPanelVisible);
}
}
}
layout(width: number, height: number): void {
this.content.layout(width, height);
this.content.layout?.(width, height);
}
update(event: GroupPanelUpdateEvent): void {
this.content.update(event);
this.tab.update(event);
}
toJSON(): {} {
let tab =
this.tab instanceof DefaultTab ? undefined : this.tab.toJSON();
if (tab && Object.keys(tab).length === 0) {
tab = undefined;
}
return {
content: this.content.toJSON(),
tab,
};
this.content.update?.(event);
this.tab.update?.(event);
}
dispose(): void {
this.content.dispose();
this.tab.dispose();
this.content.dispose?.();
this.tab.dispose?.();
}
private createContentComponent(
id: string,
componentName: string
): IContentRenderer {
return createComponent(
id,
componentName,
this.accessor.options.components || {},
this.accessor.options.frameworkComponents,
this.accessor.options.frameworkComponentFactory?.content
);
}
private createTabComponent(
id: string,
componentName?: string
): ITabRenderer {
return createComponent(
id,
componentName,
this.accessor.options.tabComponents || {},
this.accessor.options.frameworkTabComponents,
this.accessor.options.frameworkComponentFactory?.tab,
() => new DefaultTab()
);
}
}

View File

@ -1,7 +1,7 @@
import { GroupviewPanelState, ITabRenderer } from '../groupview/types';
import { GroupPanel } from '../groupview/groupviewPanel';
import { DockviewPanel, IDockviewPanel } from './dockviewPanel';
import { DockviewComponent } from './dockviewComponent';
import { IDockviewComponent } from './dockviewComponent';
import { createComponent } from '../panel/componentFactory';
import { DefaultTab } from './components/tab/defaultTab';
import { DefaultGroupPanelView } from './defaultGroupPanelView';
@ -12,7 +12,7 @@ export interface IPanelDeserializer {
}
export class DefaultDockviewDeserialzier implements IPanelDeserializer {
constructor(private readonly layout: DockviewComponent) {}
constructor(private readonly layout: IDockviewComponent) {}
public fromJSON(
panelData: GroupviewPanelState,
@ -21,14 +21,21 @@ export class DefaultDockviewDeserialzier implements IPanelDeserializer {
const panelId = panelData.id;
const params = panelData.params;
const title = panelData.title;
const viewData = panelData.view;
const viewData = panelData.view!;
let tab: ITabRenderer;
if (viewData.tab?.id) {
const contentComponent = viewData
? viewData.content.id
: panelData.contentComponent || 'unknown';
const tabComponent = viewData
? viewData.tab?.id
: panelData.tabComponent;
if (tabComponent) {
tab = createComponent(
viewData.tab.id,
viewData.tab.id,
panelId,
tabComponent,
this.layout.options.tabComponents,
this.layout.options.frameworkTabComponents,
this.layout.options.frameworkComponentFactory?.tab,
@ -36,7 +43,7 @@ export class DefaultDockviewDeserialzier implements IPanelDeserializer {
);
} else if (this.layout.options.defaultTabComponent) {
tab = createComponent(
this.layout.options.defaultTabComponent,
panelId,
this.layout.options.defaultTabComponent,
this.layout.options.tabComponents,
this.layout.options.frameworkTabComponents,
@ -47,16 +54,12 @@ export class DefaultDockviewDeserialzier implements IPanelDeserializer {
tab = new DefaultTab();
}
const view = new DefaultGroupPanelView({
content: createComponent(
viewData.content.id,
viewData.content.id,
this.layout.options.components,
this.layout.options.frameworkComponents,
this.layout.options.frameworkComponentFactory?.content
),
tab,
});
const view = new DefaultGroupPanelView(
this.layout,
panelId,
contentComponent,
tabComponent
);
const panel = new DockviewPanel(
panelId,
@ -67,7 +70,7 @@ export class DefaultDockviewDeserialzier implements IPanelDeserializer {
panel.init({
view,
title,
title: title || panelId,
params: params || {},
});

View File

@ -950,13 +950,16 @@ export class DockviewComponent
options: AddPanelOptions,
group: GroupPanel
): IDockviewPanel {
const view = new DefaultGroupPanelView({
content: this.createContentComponent(options.id, options.component),
tab: this.createTabComponent(
options.id,
options.tabComponent || this.options.defaultTabComponent
),
});
const contentComponent = options.component;
const tabComponent =
options.tabComponent || this.options.defaultTabComponent;
const view = new DefaultGroupPanelView(
this,
options.id,
contentComponent,
tabComponent
);
const panel = new DockviewPanel(options.id, this, this._api, group);
panel.init({
@ -968,33 +971,6 @@ export class DockviewComponent
return panel;
}
private createContentComponent(
id: string,
componentName: string
): IContentRenderer {
return createComponent(
id,
componentName,
this.options.components || {},
this.options.frameworkComponents,
this.options.frameworkComponentFactory?.content
);
}
private createTabComponent(
id: string,
componentName?: string
): ITabRenderer {
return createComponent(
id,
componentName,
this.options.tabComponents || {},
this.options.frameworkTabComponents,
this.options.frameworkComponentFactory?.tab,
() => new DefaultTab()
);
}
private createGroupAtLocation(location: number[] = [0]): GroupPanel {
const group = this.createGroup();
this.doAddGroup(group, location);

View File

@ -12,7 +12,7 @@ import { GroupPanel } from '../groupview/groupviewPanel';
import { CompositeDisposable, IDisposable } from '../lifecycle';
import { IPanel, Parameters } from '../panel/types';
import { IGroupPanelView } from './defaultGroupPanelView';
import { DockviewComponent } from './dockviewComponent';
import { IDockviewComponent } from './dockviewComponent';
export interface IDockviewPanel extends IDisposable, IPanel {
readonly view?: IGroupPanelView;
@ -56,7 +56,7 @@ export class DockviewPanel
constructor(
public readonly id: string,
accessor: DockviewComponent,
accessor: IDockviewComponent,
private readonly containerApi: DockviewApi,
group: GroupPanel
) {
@ -82,9 +82,7 @@ export class DockviewPanel
this._params = params.params;
this._view = params.view;
if (typeof params.title === 'string') {
this.setTitle(params.title);
}
this.setTitle(params.title);
this.view?.init({
...params,
@ -100,7 +98,8 @@ export class DockviewPanel
public toJSON(): GroupviewPanelState {
return <GroupviewPanelState>{
id: this.id,
view: this.view!.toJSON(),
contentComponent: this.view?.contentComponent,
tabComponent: this.view?.tabComponent,
params:
Object.keys(this._params || {}).length > 0
? this._params
@ -133,11 +132,9 @@ export class DockviewPanel
...event.params.params,
};
if (typeof params.title === 'string') {
if (params.title !== this.title) {
this._title = params.title;
this.api._onDidTitleChange.fire({ title: this.title });
}
if (params.title !== this.title) {
this._title = params.title;
this.api._onDidTitleChange.fire({ title: this.title });
}
this.view?.update({

View File

@ -235,14 +235,14 @@ export abstract class BaseGrid<T extends IGridPanelView>
if (this._activeGroup) {
this._activeGroup.setActive(false);
if (!skipFocus) {
this._activeGroup.focus();
this._activeGroup.focus?.();
}
}
if (group) {
group.setActive(true);
if (!skipFocus) {
group.focus();
group.focus?.();
}
}

View File

@ -40,15 +40,15 @@ export abstract class BasePanelView<T extends PanelApiImpl>
*/
protected abstract getComponent(): IFrameworkPart;
get element() {
get element(): HTMLElement {
return this._element;
}
get width() {
get width(): number {
return this._width;
}
get height() {
get height(): number {
return this._height;
}
@ -83,11 +83,11 @@ export abstract class BasePanelView<T extends PanelApiImpl>
);
}
focus() {
focus(): void {
this.api._onFocusEvent.fire();
}
layout(width: number, height: number) {
layout(width: number, height: number): void {
this._width = width;
this._height = height;
this.api._onDidDimensionChange.fire({ width, height });
@ -104,7 +104,7 @@ export abstract class BasePanelView<T extends PanelApiImpl>
this.part = this.getComponent();
}
update(event: PanelUpdateEvent) {
update(event: PanelUpdateEvent): void {
this._params = {
...this._params,
params: {
@ -125,7 +125,7 @@ export abstract class BasePanelView<T extends PanelApiImpl>
};
}
dispose() {
dispose(): void {
super.dispose();
this.api.dispose();

View File

@ -77,7 +77,7 @@ export interface IHeader {
height: number | undefined;
}
export interface IGroupview extends IDisposable, IGridPanelView {
export interface IGroupview extends IGridPanelView {
readonly isActive: boolean;
readonly size: number;
readonly panels: IDockviewPanel[];
@ -427,7 +427,7 @@ export class Groupview extends CompositeDisposable implements IGroupview {
}
focus(): void {
this._activePanel?.focus();
this._activePanel?.focus?.();
}
public openPanel(
@ -525,7 +525,7 @@ export class Groupview extends CompositeDisposable implements IGroupview {
): void {
if (!force && this.isActive === isGroupActive) {
if (!skipFocus) {
this._activePanel?.focus();
this._activePanel?.focus?.();
}
return;
}
@ -545,7 +545,7 @@ export class Groupview extends CompositeDisposable implements IGroupview {
if (isGroupActive) {
if (!skipFocus) {
this._activePanel?.focus();
this._activePanel?.focus?.();
}
}
}

View File

@ -74,9 +74,8 @@ export class ContentContainer
const disposable = new CompositeDisposable();
if (this.panel.view) {
const _onDidFocus: Event<void> =
this.panel.view.content.onDidFocus!;
const _onDidBlur: Event<void> = this.panel.view.content.onDidBlur!;
const _onDidFocus = this.panel.view.content.onDidFocus;
const _onDidBlur = this.panel.view.content.onDidBlur;
const { onDidFocus, onDidBlur } = trackFocus(this._element);

View File

@ -9,7 +9,11 @@ import {
import { DockviewApi } from '../api/component.api';
import { GroupPanel } from './groupviewPanel';
import { Event } from '../events';
import { IGroupPanelView } from '../dockview/defaultGroupPanelView';
import {
IGroupPanelView,
SerializedGroupPanelView,
} from '../dockview/defaultGroupPanelView';
import { Optional } from '../types';
export interface IRenderable {
id: string;
@ -19,7 +23,7 @@ export interface IRenderable {
}
export interface HeaderPartInitParameters {
title?: string;
title: string;
}
export interface GroupPanelPartInitParameters
@ -40,20 +44,28 @@ export interface IWatermarkRenderer extends IPanel {
updateParentGroup(group: GroupPanel, visible: boolean): void;
}
export interface ITabRenderer extends IPanel {
export interface ITabRenderer
extends Optional<
Omit<IPanel, 'id'>,
'dispose' | 'update' | 'layout' | 'toJSON'
> {
readonly element: HTMLElement;
init(parameters: GroupPanelPartInitParameters): void;
updateParentGroup(group: GroupPanel, isPanelVisible: boolean): void;
onGroupChange?(group: GroupPanel): void;
onPanelVisibleChange?(isPanelVisible: boolean): void;
}
export interface IContentRenderer extends IPanel {
export interface IContentRenderer
extends Optional<
Omit<IPanel, 'id'>,
'dispose' | 'update' | 'layout' | 'toJSON'
> {
readonly element: HTMLElement;
readonly actions?: HTMLElement;
readonly onDidFocus?: Event<void>;
readonly onDidBlur?: Event<void>;
updateParentGroup(group: GroupPanel, isPanelVisible: boolean): void;
init(parameters: GroupPanelContentPartInitParameters): void;
layout(width: number, height: number): void;
onGroupChange?(group: GroupPanel): void;
onPanelVisibleChange?(isPanelVisible: boolean): void;
}
// watermark component
@ -88,7 +100,9 @@ export type GroupPanelUpdateEvent = PanelUpdateEvent<{
export interface GroupviewPanelState {
id: string;
view?: any;
contentComponent?: string;
tabComponent?: string;
title?: string;
params?: { [key: string]: any };
view?: SerializedGroupPanelView; // depreciated
}

View File

@ -22,7 +22,7 @@ export interface IPanel extends IDisposable {
layout(width: number, height: number): void;
update(event: PanelUpdateEvent<Parameters>): void;
toJSON(): object;
focus(): void;
focus?(): void;
}
export interface IFrameworkPart extends IDisposable {

View File

@ -11,3 +11,5 @@ export type FunctionOrValue<T> = (() => T) | T;
export function isBooleanValue(value: any): value is boolean {
return typeof value === 'boolean';
}
export type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>;

View File

@ -11,7 +11,6 @@ import {
ITabRenderer,
watchElementResize,
GroupPanel,
DEFAULT_TAB_IDENTIFIER,
DefaultDockviewDeserialzier,
} from 'dockview-core';
import { ReactPanelContentPart } from './reactContentPart';
@ -72,6 +71,8 @@ export interface IDockviewReactProps {
singleTabMode?: 'fullwidth' | 'default';
}
const DEFAULT_REACT_TAB = 'props.defaultTabComponent';
export const DockviewReact = React.forwardRef(
(props: IDockviewReactProps, ref: React.ForwardedRef<HTMLDivElement>) => {
const domRef = React.useRef<HTMLDivElement>(null);
@ -144,16 +145,22 @@ export const DockviewReact = React.forwardRef(
const element = document.createElement('div');
const frameworkTabComponents = props.tabComponents || {};
if (props.defaultTabComponent) {
frameworkTabComponents[DEFAULT_REACT_TAB] =
props.defaultTabComponent;
}
const dockview = new DockviewComponent(element, {
frameworkComponentFactory: factory,
frameworkComponents: props.components,
frameworkTabComponents: {
...(props.tabComponents || {}),
[DEFAULT_TAB_IDENTIFIER]: props.defaultTabComponent,
},
frameworkTabComponents,
tabHeight: props.tabHeight,
watermarkFrameworkComponent: props.watermarkComponent,
defaultTabComponent: DEFAULT_TAB_IDENTIFIER,
defaultTabComponent: props.defaultTabComponent
? DEFAULT_REACT_TAB
: undefined,
styles: props.hideBorders
? { separatorBorder: 'transparent' }
: undefined,
@ -241,12 +248,19 @@ export const DockviewReact = React.forwardRef(
if (!dockviewRef.current) {
return;
}
const frameworkTabComponents = props.tabComponents || {};
if (props.defaultTabComponent) {
frameworkTabComponents[DEFAULT_REACT_TAB] =
props.defaultTabComponent;
}
dockviewRef.current.updateOptions({
defaultTabComponent: DEFAULT_TAB_IDENTIFIER,
frameworkTabComponents: {
...(props.tabComponents || {}),
[DEFAULT_TAB_IDENTIFIER]: props.defaultTabComponent,
},
defaultTabComponent: props.defaultTabComponent
? DEFAULT_REACT_TAB
: undefined,
frameworkTabComponents,
});
}, [props.defaultTabComponent]);

View File

@ -52,12 +52,6 @@ export class ReactPanelContentPart implements IContentRenderer {
);
}
public toJSON() {
return {
id: this.id,
};
}
public update(event: PanelUpdateEvent) {
this.part?.update(event.params);
}

View File

@ -2,7 +2,6 @@ import * as React from 'react';
import { ReactPart, ReactPortalStore } from '../react';
import { IGroupPanelBaseProps } from './dockview';
import {
DEFAULT_TAB_IDENTIFIER,
PanelUpdateEvent,
GroupPanel,
ITabRenderer,
@ -47,16 +46,6 @@ export class ReactPanelHeaderPart implements ITabRenderer {
this.part?.update(event.params);
}
public toJSON() {
if (this.id === DEFAULT_TAB_IDENTIFIER) {
return {};
}
return {
id: this.id,
};
}
public layout(_width: number, _height: number) {
// noop - retrieval from api
}

View File

@ -32,10 +32,6 @@ export class WebviewContentRenderer implements IContentRenderer {
this.parameters = parameters;
}
public toJSON() {
return {};
}
public update(params: PanelUpdateEvent) {
if (this.parameters) {
this.parameters.params = params.params;