diff --git a/packages/dockview-core/src/__tests__/dockview/components/titlebar/tabsContainer.spec.ts b/packages/dockview-core/src/__tests__/dockview/components/titlebar/tabsContainer.spec.ts index 664057572..711d2ee0b 100644 --- a/packages/dockview-core/src/__tests__/dockview/components/titlebar/tabsContainer.spec.ts +++ b/packages/dockview-core/src/__tests__/dockview/components/titlebar/tabsContainer.spec.ts @@ -638,4 +638,214 @@ describe('tabsContainer', () => { expect(preventDefaultSpy).toBeCalledTimes(1); expect(accessor.addFloatingGroup).toBeCalledTimes(1); }); + + test('pre header actions', () => { + const accessorMock = jest.fn(() => { + return (>{ + options: {}, + onDidAddPanel: jest.fn(), + onDidRemovePanel: jest.fn(), + element: document.createElement('div'), + addFloatingGroup: jest.fn(), + getGroupPanel: jest.fn(), + }) as DockviewComponent; + }); + + const groupPanelMock = jest.fn(() => { + return (>{ + api: { isFloating: true } as any, + model: {} as any, + }) as DockviewGroupPanel; + }); + + const accessor = new accessorMock(); + const groupPanel = new groupPanelMock(); + + const cut = new TabsContainer(accessor, groupPanel); + + const panelMock = jest.fn((id: string) => { + const partial: Partial = { + id, + + view: { + tab: { + element: document.createElement('div'), + } as any, + content: { + element: document.createElement('div'), + } as any, + } as any, + }; + return partial as IDockviewPanel; + }); + + const panel = new panelMock('test_id'); + cut.openPanel(panel); + + let result = cut.element.querySelector('.pre-actions-container'); + expect(result).toBeTruthy(); + expect(result!.childNodes.length).toBe(0); + + const actions = document.createElement('div'); + cut.setPrefixActionsElement(actions); + + result = cut.element.querySelector('.pre-actions-container'); + expect(result).toBeTruthy(); + expect(result!.childNodes.length).toBe(1); + expect(result!.childNodes.item(0)).toBe(actions); + + const updatedActions = document.createElement('div'); + cut.setPrefixActionsElement(updatedActions); + + result = cut.element.querySelector('.pre-actions-container'); + expect(result).toBeTruthy(); + expect(result!.childNodes.length).toBe(1); + expect(result!.childNodes.item(0)).toBe(updatedActions); + + cut.setPrefixActionsElement(undefined); + + result = cut.element.querySelector('.pre-actions-container'); + expect(result).toBeTruthy(); + expect(result!.childNodes.length).toBe(0); + }); + + test('left header actions', () => { + const accessorMock = jest.fn(() => { + return (>{ + options: {}, + onDidAddPanel: jest.fn(), + onDidRemovePanel: jest.fn(), + element: document.createElement('div'), + addFloatingGroup: jest.fn(), + getGroupPanel: jest.fn(), + }) as DockviewComponent; + }); + + const groupPanelMock = jest.fn(() => { + return (>{ + api: { isFloating: true } as any, + model: {} as any, + }) as DockviewGroupPanel; + }); + + const accessor = new accessorMock(); + const groupPanel = new groupPanelMock(); + + const cut = new TabsContainer(accessor, groupPanel); + + const panelMock = jest.fn((id: string) => { + const partial: Partial = { + id, + + view: { + tab: { + element: document.createElement('div'), + } as any, + content: { + element: document.createElement('div'), + } as any, + } as any, + }; + return partial as IDockviewPanel; + }); + + const panel = new panelMock('test_id'); + cut.openPanel(panel); + + let result = cut.element.querySelector('.left-actions-container'); + expect(result).toBeTruthy(); + expect(result!.childNodes.length).toBe(0); + + const actions = document.createElement('div'); + cut.setLeftActionsElement(actions); + + result = cut.element.querySelector('.left-actions-container'); + expect(result).toBeTruthy(); + expect(result!.childNodes.length).toBe(1); + expect(result!.childNodes.item(0)).toBe(actions); + + const updatedActions = document.createElement('div'); + cut.setLeftActionsElement(updatedActions); + + result = cut.element.querySelector('.left-actions-container'); + expect(result).toBeTruthy(); + expect(result!.childNodes.length).toBe(1); + expect(result!.childNodes.item(0)).toBe(updatedActions); + + cut.setLeftActionsElement(undefined); + + result = cut.element.querySelector('.left-actions-container'); + expect(result).toBeTruthy(); + expect(result!.childNodes.length).toBe(0); + }); + + test('right header actions', () => { + const accessorMock = jest.fn(() => { + return (>{ + options: {}, + onDidAddPanel: jest.fn(), + onDidRemovePanel: jest.fn(), + element: document.createElement('div'), + addFloatingGroup: jest.fn(), + getGroupPanel: jest.fn(), + }) as DockviewComponent; + }); + + const groupPanelMock = jest.fn(() => { + return (>{ + api: { isFloating: true } as any, + model: {} as any, + }) as DockviewGroupPanel; + }); + + const accessor = new accessorMock(); + const groupPanel = new groupPanelMock(); + + const cut = new TabsContainer(accessor, groupPanel); + + const panelMock = jest.fn((id: string) => { + const partial: Partial = { + id, + + view: { + tab: { + element: document.createElement('div'), + } as any, + content: { + element: document.createElement('div'), + } as any, + } as any, + }; + return partial as IDockviewPanel; + }); + + const panel = new panelMock('test_id'); + cut.openPanel(panel); + + let result = cut.element.querySelector('.right-actions-container'); + expect(result).toBeTruthy(); + expect(result!.childNodes.length).toBe(0); + + const actions = document.createElement('div'); + cut.setRightActionsElement(actions); + + result = cut.element.querySelector('.right-actions-container'); + expect(result).toBeTruthy(); + expect(result!.childNodes.length).toBe(1); + expect(result!.childNodes.item(0)).toBe(actions); + + const updatedActions = document.createElement('div'); + cut.setRightActionsElement(updatedActions); + + result = cut.element.querySelector('.right-actions-container'); + expect(result).toBeTruthy(); + expect(result!.childNodes.length).toBe(1); + expect(result!.childNodes.item(0)).toBe(updatedActions); + + cut.setRightActionsElement(undefined); + + result = cut.element.querySelector('.right-actions-container'); + expect(result).toBeTruthy(); + expect(result!.childNodes.length).toBe(0); + }); }); diff --git a/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts b/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts index 4a3931cf2..77a3f7182 100644 --- a/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts +++ b/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts @@ -43,6 +43,7 @@ export interface ITabsContainer extends IDisposable { openPanel: (panel: IDockviewPanel, index?: number) => void; setRightActionsElement(element: HTMLElement | undefined): void; setLeftActionsElement(element: HTMLElement | undefined): void; + setPrefixActionsElement(element: HTMLElement | undefined): void; show(): void; hide(): void; } @@ -55,12 +56,14 @@ export class TabsContainer private readonly tabContainer: HTMLElement; private readonly rightActionsContainer: HTMLElement; private readonly leftActionsContainer: HTMLElement; + private readonly preActionsContainer: HTMLElement; private readonly voidContainer: VoidContainer; private tabs: IValueDisposable[] = []; private selectedIndex = -1; private rightActions: HTMLElement | undefined; private leftActions: HTMLElement | undefined; + private preActions: HTMLElement | undefined; private _hidden = false; @@ -129,6 +132,20 @@ export class TabsContainer } } + setPrefixActionsElement(element: HTMLElement | undefined): void { + if (this.preActions === element) { + return; + } + if (this.preActions) { + this.preActions.remove(); + this.preActions = undefined; + } + if (element) { + this.preActionsContainer.appendChild(element); + this.preActions = element; + } + } + get element(): HTMLElement { return this._element; } @@ -192,11 +209,15 @@ export class TabsContainer this.leftActionsContainer = document.createElement('div'); this.leftActionsContainer.className = 'left-actions-container'; + this.preActionsContainer = document.createElement('div'); + this.preActionsContainer.className = 'pre-actions-container'; + this.tabContainer = document.createElement('div'); this.tabContainer.className = 'tabs-container'; this.voidContainer = new VoidContainer(this.accessor, this.group); + this._element.appendChild(this.preActionsContainer); this._element.appendChild(this.tabContainer); this._element.appendChild(this.leftActionsContainer); this._element.appendChild(this.voidContainer.element); diff --git a/packages/dockview-core/src/dockview/dockviewComponent.ts b/packages/dockview-core/src/dockview/dockviewComponent.ts index 53d96d250..bb8452f20 100644 --- a/packages/dockview-core/src/dockview/dockviewComponent.ts +++ b/packages/dockview-core/src/dockview/dockviewComponent.ts @@ -92,6 +92,7 @@ export type DockviewComponentUpdateOptions = Pick< | 'defaultTabComponent' | 'createLeftHeaderActionsElement' | 'createRightHeaderActionsElement' + | 'createPrefixHeaderActionsElement' | 'disableFloatingGroups' | 'floatingGroupBounds' >; diff --git a/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts b/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts index d05aa7308..0f3ec17ab 100644 --- a/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts +++ b/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts @@ -12,7 +12,7 @@ import { IContentContainer, } from './components/panel/content'; import { - GroupDragEvent, + GroupDragEvent, ITabsContainer, TabDragEvent, TabsContainer, @@ -144,6 +144,7 @@ export class DockviewGroupPanelModel private _isFloating = false; private _rightHeaderActions: IHeaderActionsRenderer | undefined; private _leftHeaderActions: IHeaderActionsRenderer | undefined; + private _prefixHeaderActions: IHeaderActionsRenderer | undefined; private mostRecentlyUsed: IDockviewPanel[] = []; @@ -398,6 +399,21 @@ export class DockviewGroupPanelModel this._leftHeaderActions.element ); } + + if (this.accessor.options.createPrefixHeaderActionsElement) { + this._prefixHeaderActions = + this.accessor.options.createPrefixHeaderActionsElement( + this.groupPanel + ); + this.addDisposables(this._prefixHeaderActions); + this._prefixHeaderActions.init({ + containerApi: new DockviewApi(this.accessor), + api: this.groupPanel.api, + }); + this.tabsContainer.setPrefixActionsElement( + this._prefixHeaderActions.element + ); + } } public indexOf(panel: IDockviewPanel): number { diff --git a/packages/dockview-core/src/dockview/options.ts b/packages/dockview-core/src/dockview/options.ts index 920737a06..f9d2cc1c5 100644 --- a/packages/dockview-core/src/dockview/options.ts +++ b/packages/dockview-core/src/dockview/options.ts @@ -84,6 +84,9 @@ export interface DockviewComponentOptions extends DockviewRenderFunctions { createLeftHeaderActionsElement?: ( group: DockviewGroupPanel ) => IHeaderActionsRenderer; + createPrefixHeaderActionsElement?: ( + group: DockviewGroupPanel + ) => IHeaderActionsRenderer; singleTabMode?: 'fullwidth' | 'default'; parentElement?: HTMLElement; disableFloatingGroups?: boolean; diff --git a/packages/dockview/src/dockview/dockview.tsx b/packages/dockview/src/dockview/dockview.tsx index 7c6bcc435..2485e6d81 100644 --- a/packages/dockview/src/dockview/dockview.tsx +++ b/packages/dockview/src/dockview/dockview.tsx @@ -67,6 +67,7 @@ export interface IDockviewReactProps { defaultTabComponent?: React.FunctionComponent; rightHeaderActionsComponent?: React.FunctionComponent; leftHeaderActionsComponent?: React.FunctionComponent; + prefixHeaderActionsComponent?: React.FunctionComponent; singleTabMode?: 'fullwidth' | 'default'; disableFloatingGroups?: boolean; floatingGroupBounds?: @@ -166,6 +167,10 @@ export const DockviewReact = React.forwardRef( props.rightHeaderActionsComponent, { addPortal } ), + createPrefixHeaderActionsElement: createGroupControlElement( + props.prefixHeaderActionsComponent, + { addPortal } + ), singleTabMode: props.singleTabMode, disableFloatingGroups: props.disableFloatingGroups, floatingGroupBounds: props.floatingGroupBounds, @@ -301,6 +306,18 @@ export const DockviewReact = React.forwardRef( }); }, [props.leftHeaderActionsComponent]); + React.useEffect(() => { + if (!dockviewRef.current) { + return; + } + dockviewRef.current.updateOptions({ + createPrefixHeaderActionsElement: createGroupControlElement( + props.prefixHeaderActionsComponent, + { addPortal } + ), + }); + }, [props.prefixHeaderActionsComponent]); + return (
void | No | | | -| components | object | No | | | -| tabComponents | object | Yes | | | -| watermarkComponent | object | Yes | | | -| hideBorders | boolean | Yes | false | | -| className | string | Yes | '' | | -| disableAutoResizing | boolean | Yes | false | | -| onDidDrop | Event | Yes | false | | -| showDndOverlay | Event | Yes | false | | -| defaultTabComponent | object | Yes | | | -| leftHeaderActionsComponent | object | Yes | | | -| rightHeaderActionsComponent | object | Yes | | | -| singleTabMode | 'fullwidth' \| 'default' | Yes | 'default' | | +| Property | Type | Optional | Default | Description | +| ---------------------------- | ------------------------------------ | -------- | --------- | ----------- | +| onReady | (event: SplitviewReadyEvent) => void | No | | | +| components | object | No | | | +| tabComponents | object | Yes | | | +| watermarkComponent | object | Yes | | | +| hideBorders | boolean | Yes | false | | +| className | string | Yes | '' | | +| disableAutoResizing | boolean | Yes | false | | +| onDidDrop | Event | Yes | false | | +| showDndOverlay | Event | Yes | false | | +| defaultTabComponent | object | Yes | | | +| leftHeaderActionsComponent | object | Yes | | | +| rightHeaderActionsComponent | object | Yes | | | +| prefixHeaderActionsComponent | object | Yes | | | +| singleTabMode | 'fullwidth' \| 'default' | Yes | 'default' | | ## Dockview API @@ -804,8 +805,8 @@ better understanding of what this means, try and drag the panels in the example ### Group Controls Panel -`DockviewReact` accepts `leftHeaderActionsComponent` and `rightHeaderActionsComponent` which expect a React component with props `IDockviewHeaderActionsProps`. -These controls are rendered of the left and right side of the space to the right of the tabs in the header bar. +`DockviewReact` accepts `leftHeaderActionsComponent`, `rightHeaderActionsComponent` and `prefixHeaderActionsComponent` which expect a React component with props `IDockviewHeaderActionsProps`. +These controls are rendered to left and right side of the space to the right of the tabs in the header bar as well as before the first tab in the case of the prefix header prop. ```tsx const Component: React.FunctionComponent = () => { diff --git a/packages/docs/sandboxes/demo-dockview/src/app.tsx b/packages/docs/sandboxes/demo-dockview/src/app.tsx index 7e84a0747..0e5b2ff96 100644 --- a/packages/docs/sandboxes/demo-dockview/src/app.tsx +++ b/packages/docs/sandboxes/demo-dockview/src/app.tsx @@ -191,6 +191,23 @@ const LeftControls = (props: IDockviewHeaderActionsProps) => { ); }; +const PrefixHeaderControls = (props: IDockviewHeaderActionsProps) => { + return ( +
+ +
+ ); +}; + const DockviewDemo = (props: { theme?: string }) => { const onReady = (event: DockviewReadyEvent) => { event.api.addPanel({ @@ -262,6 +279,7 @@ const DockviewDemo = (props: { theme?: string }) => { defaultTabComponent={headerComponents.default} rightHeaderActionsComponent={RightControls} leftHeaderActionsComponent={LeftControls} + prefixHeaderActionsComponent={PrefixHeaderControls} onReady={onReady} className={props.theme || 'dockview-theme-abyss'} />