From 3b5649832e7943786894caf8874348b51282b0bc Mon Sep 17 00:00:00 2001 From: mslxl Date: Sun, 1 Jun 2025 04:22:41 +0800 Subject: [PATCH 1/5] feat: add header position support to dockview group panel Resolve #894 --- .../dockview/components/titlebar/tabs.scss | 24 ++++++- .../src/dockview/components/titlebar/tabs.ts | 21 +++++- .../components/titlebar/tabsContainer.scss | 10 +++ .../components/titlebar/tabsContainer.ts | 22 +++++- .../src/dockview/dockviewGroupPanel.scss | 17 ++++- .../src/dockview/dockviewGroupPanel.ts | 9 +++ .../src/dockview/dockviewGroupPanelModel.ts | 67 ++++++++++++++++++- .../dockview-core/src/dockview/framework.ts | 2 + .../dockview-core/src/dockview/options.ts | 3 + packages/dockview-core/src/lifecycle.ts | 11 +-- .../src/dockview/headerActionsRenderer.ts | 1 + .../dockview/demo-dockview/src/controls.tsx | 6 ++ .../demo-dockview/src/groupActions.tsx | 17 +++++ 13 files changed, 198 insertions(+), 12 deletions(-) diff --git a/packages/dockview-core/src/dockview/components/titlebar/tabs.scss b/packages/dockview-core/src/dockview/components/titlebar/tabs.scss index 7672d97c79..7a4b55670e 100644 --- a/packages/dockview-core/src/dockview/components/titlebar/tabs.scss +++ b/packages/dockview-core/src/dockview/components/titlebar/tabs.scss @@ -4,7 +4,8 @@ overflow: auto; scrollbar-width: thin; // firefox - &.dv-horizontal { + &.dv-horizontal, + &.dv-vertical { .dv-tab { &:not(:first-child)::before { content: ' '; @@ -14,12 +15,20 @@ z-index: 5; pointer-events: none; background-color: var(--dv-tab-divider-color); - width: 1px; - height: 100%; } } } + &.dv-horizontal .dv-tab:not(:first-child)::before { + width: 1px; + height: 100%; + } + + &.dv-vertical .dv-tab:not(:first-child)::before { + width: 100%; + height: 1px; + } + &::-webkit-scrollbar { height: 3px; } @@ -33,6 +42,15 @@ &::-webkit-scrollbar-thumb { background: var(--dv-tabs-container-scrollbar-color); } + + &.dv-tabs-container-vertical { + flex-direction: column; + + .dv-tab { + writing-mode: vertical-rl; + transform: rotate(180deg); + } + } } .dv-scrollable { diff --git a/packages/dockview-core/src/dockview/components/titlebar/tabs.ts b/packages/dockview-core/src/dockview/components/titlebar/tabs.ts index 53486ed7cc..95598b659b 100644 --- a/packages/dockview-core/src/dockview/components/titlebar/tabs.ts +++ b/packages/dockview-core/src/dockview/components/titlebar/tabs.ts @@ -1,7 +1,9 @@ import { getPanelData } from '../../../dnd/dataTransfer'; import { + addClasses, isChildEntirelyVisibleWithinParent, OverflowObserver, + removeClasses, } from '../../../dom'; import { addDisposableListener, Emitter, Event } from '../../../events'; import { @@ -15,6 +17,7 @@ import { DockviewComponent } from '../../dockviewComponent'; import { DockviewGroupPanel } from '../../dockviewGroupPanel'; import { WillShowOverlayLocationEvent } from '../../dockviewGroupPanelModel'; import { DockviewPanel, IDockviewPanel } from '../../dockviewPanel'; +import { IHeaderDirection } from '../../options'; import { Tab } from '../tab/tab'; import { TabDragEvent, TabDropIndexEvent } from './tabsContainer'; @@ -26,6 +29,7 @@ export class Tabs extends CompositeDisposable { private _tabs: IValueDisposable[] = []; private selectedIndex = -1; private _showTabsOverflowControl = false; + private _direction: IHeaderDirection = 'horizontal'; private readonly _onTabDragStart = new Emitter(); readonly onTabDragStart: Event = this._onTabDragStart.event; @@ -87,6 +91,21 @@ export class Tabs extends CompositeDisposable { return this._tabs.map((_) => _.value); } + get direction(): IHeaderDirection { + return this._direction; + } + + set direction(value: IHeaderDirection) { + this._direction = value; + removeClasses(this._tabsList, 'dv-horizontal', 'dv-vertical'); + if(value === 'vertical') { + addClasses(this._tabsList, 'dv-tabs-container-vertical', 'dv-vertical'); + } else { + removeClasses(this._tabsList, 'dv-tabs-container-vertical'); + addClasses(this._tabsList, 'dv-horizontal'); + } + } + constructor( private readonly group: DockviewGroupPanel, private readonly accessor: DockviewComponent, @@ -97,7 +116,7 @@ export class Tabs extends CompositeDisposable { super(); this._tabsList = document.createElement('div'); - this._tabsList.className = 'dv-tabs-container dv-horizontal'; + this._tabsList.className = 'dv-tabs-container'; this.showTabsOverflowControl = options.showTabsOverflowControl; diff --git a/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.scss b/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.scss index 9b35945fa2..e0a6807ab0 100644 --- a/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.scss +++ b/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.scss @@ -33,5 +33,15 @@ .dv-right-actions-container { display: flex; + + &.dv-right-actions-container-vertical { + flex-direction: column; + } + } + + &.dv-groupview-header-vertical { + flex-direction: column; + height: auto; + width: var(--dv-tabs-and-actions-container-height); } } diff --git a/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts b/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts index d2117c744f..e3c957f084 100644 --- a/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts +++ b/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts @@ -8,7 +8,7 @@ import { addDisposableListener, Emitter, Event } from '../../../events'; import { Tab } from '../tab/tab'; import { DockviewGroupPanel } from '../../dockviewGroupPanel'; import { VoidContainer } from './voidContainer'; -import { findRelativeZIndexParent, toggleClass } from '../../../dom'; +import { addClasses, findRelativeZIndexParent, removeClasses, toggleClass } from '../../../dom'; import { IDockviewPanel } from '../../dockviewPanel'; import { DockviewComponent } from '../../dockviewComponent'; import { WillShowOverlayLocationEvent } from '../../dockviewGroupPanelModel'; @@ -18,6 +18,7 @@ import { createDropdownElementHandle, DropdownElement, } from './tabOverflowControl'; +import { IHeaderDirection } from '../../options'; export interface TabDropIndexEvent { readonly event: DragEvent; @@ -43,6 +44,7 @@ export interface ITabsContainer extends IDisposable { readonly onGroupDragStart: Event; readonly onWillShowOverlay: Event; hidden: boolean; + direction: IHeaderDirection; delete(id: string): void; indexOf(id: string): number; setActive(isGroupActive: boolean): void; @@ -73,6 +75,7 @@ export class TabsContainer private preActions: HTMLElement | undefined; private _hidden = false; + private _direction: IHeaderDirection = 'horizontal'; private dropdownPart: DropdownElement | null = null; private _overflowTabs: string[] = []; @@ -111,6 +114,23 @@ export class TabsContainer this.element.style.display = value ? 'none' : ''; } + get direction(): IHeaderDirection { + return this._direction; + } + + set direction(value: IHeaderDirection) { + this._direction = value; + if(value === 'vertical') { + addClasses(this._element, 'dv-groupview-header-vertical'); + addClasses(this.rightActionsContainer, 'dv-right-actions-container-vertical'); + this.tabs.direction = value; + } else { + removeClasses(this._element, 'dv-groupview-header-vertical'); + removeClasses(this.rightActionsContainer, 'dv-right-actions-container-vertical'); + this.tabs.direction = value; + } + } + get element(): HTMLElement { return this._element; } diff --git a/packages/dockview-core/src/dockview/dockviewGroupPanel.scss b/packages/dockview-core/src/dockview/dockviewGroupPanel.scss index d452917f2a..cc588b97c3 100644 --- a/packages/dockview-core/src/dockview/dockviewGroupPanel.scss +++ b/packages/dockview-core/src/dockview/dockviewGroupPanel.scss @@ -1,6 +1,5 @@ .dv-groupview { display: flex; - flex-direction: column; height: 100%; background-color: var(--dv-group-view-background-color); overflow: hidden; @@ -14,4 +13,20 @@ min-height: 0; outline: none; } + + &.dv-groupview-header-top { + flex-direction: column; + } + + &.dv-groupview-header-bottom { + flex-direction: column-reverse; + } + + &.dv-groupview-header-left { + flex-direction: row; + } + + &.dv-groupview-header-right { + flex-direction: row-reverse; + } } diff --git a/packages/dockview-core/src/dockview/dockviewGroupPanel.ts b/packages/dockview-core/src/dockview/dockviewGroupPanel.ts index fd5e70fe36..0a19dd33a1 100644 --- a/packages/dockview-core/src/dockview/dockviewGroupPanel.ts +++ b/packages/dockview-core/src/dockview/dockviewGroupPanel.ts @@ -13,6 +13,7 @@ import { DockviewGroupPanelApi, DockviewGroupPanelApiImpl, } from '../api/dockviewGroupPanelApi'; +import { IHeaderPosition } from './options'; const MINIMUM_DOCKVIEW_GROUP_PANEL_WIDTH = 100; const MINIMUM_DOCKVIEW_GROUP_PANEL_HEIGHT = 100; @@ -21,6 +22,7 @@ export interface IDockviewGroupPanel extends IGridviewPanel { model: IDockviewGroupPanelModel; locked: DockviewGroupPanelLocked; + headerPosition: IHeaderPosition; readonly size: number; readonly panels: IDockviewPanel[]; readonly activePanel: IDockviewPanel | undefined; @@ -94,6 +96,13 @@ export class DockviewGroupPanel return this._model.header; } + get headerPosition(): IHeaderPosition { + return this._model.headerPosition; + } + set headerPosition(value: IHeaderPosition) { + this._model.headerPosition = value; + } + constructor( accessor: DockviewComponent, id: string, diff --git a/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts b/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts index c56babf911..6c8032101b 100644 --- a/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts +++ b/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts @@ -2,7 +2,7 @@ import { DockviewApi } from '../api/component.api'; import { getPanelData, PanelTransfer } from '../dnd/dataTransfer'; import { Position, WillShowOverlayEvent } from '../dnd/droptarget'; import { DockviewComponent } from './dockviewComponent'; -import { isAncestor, toggleClass } from '../dom'; +import { addClasses, isAncestor, removeClasses, toggleClass } from '../dom'; import { addDisposableListener, DockviewEvent, @@ -35,6 +35,8 @@ import { DockviewDndOverlayEvent, DockviewUnhandledDragOverEvent, IHeaderActionsRenderer, + IHeaderDirection, + IHeaderPosition, } from './options'; import { OverlayRenderContainer } from '../overlay/overlayRenderContainer'; import { TitleEvent } from '../api/dockviewPanelApi'; @@ -51,6 +53,7 @@ interface GroupMoveEvent { interface CoreGroupOptions { locked?: DockviewGroupPanelLocked; hideHeader?: boolean; + headerPosition?: 'top' | 'bottom' | 'left' | 'right'; skipSetActive?: boolean; constraints?: Partial; initialWidth?: number; @@ -136,6 +139,7 @@ export class DockviewWillDropEvent extends DockviewDidDropEvent { export interface IHeader { hidden: boolean; + direction: IHeaderDirection; } export type DockviewGroupPanelLocked = boolean | 'no-drop-target'; @@ -160,6 +164,7 @@ export interface IDockviewGroupPanelModel extends IPanel { readonly onDidActivePanelChange: Event; readonly onMove: Event; locked: DockviewGroupPanelLocked; + headerPosition: IHeaderPosition; setActive(isActive: boolean): void; initialize(): void; // state @@ -261,7 +266,10 @@ export class DockviewGroupPanelModel private _rightHeaderActions: IHeaderActionsRenderer | undefined; private _leftHeaderActions: IHeaderActionsRenderer | undefined; private _prefixHeaderActions: IHeaderActionsRenderer | undefined; - + private _rightHeaderActionsDisposable: IDisposable | undefined; + private _leftHeaderActionsDisposable: IDisposable | undefined; + private _prefixHeaderActionsDisposable: IDisposable | undefined; + private _headerPosition: IHeaderPosition | undefined; private _location: DockviewGroupLocation = { type: 'grid' }; private mostRecentlyUsed: IDockviewPanel[] = []; @@ -387,6 +395,37 @@ export class DockviewGroupPanelModel ); } + get headerPosition(): IHeaderPosition { + return this._headerPosition ?? 'top'; + } + + set headerPosition(value: IHeaderPosition) { + this._headerPosition = value; + removeClasses( + this.container, + 'dv-groupview-header-top', + 'dv-groupview-header-bottom', + 'dv-groupview-header-left', + 'dv-groupview-header-right' + ); + addClasses(this.container, `dv-groupview-header-${value}`); + + const direction = value === 'top' || value === 'bottom' ? 'horizontal' : 'vertical'; + this.tabsContainer.direction = direction; + this.header.direction = direction; + + + // resize the active panel to fit the new header direction + // if not, the panel will overflow the tabs container + if (this._activePanel?.layout) { + this._activePanel.layout(this._width, this._height); + } + + if(this._leftHeaderActions || this._rightHeaderActions || this._prefixHeaderActions) { + this.updateHeaderActions(); + } + } + get location(): DockviewGroupLocation { return this._location; } @@ -455,6 +494,7 @@ export class DockviewGroupPanelModel this.header.hidden = !!options.hideHeader; this.locked = options.locked ?? false; + this.headerPosition = options.headerPosition ?? 'top'; this.addDisposables( this._onTabDragStart, @@ -565,11 +605,21 @@ export class DockviewGroupPanelModel this.setActive(this.isActive, true); this.updateContainer(); + this.updateHeaderActions(); + } + + updateHeaderActions(): void { if (this.accessor.options.createRightHeaderActionComponent) { + if(this._rightHeaderActionsDisposable) { + this._rightHeaderActionsDisposable.dispose() + this.removeDisposable(this._rightHeaderActionsDisposable); + } + this._rightHeaderActions = this.accessor.options.createRightHeaderActionComponent( this.groupPanel ); + this._rightHeaderActionsDisposable = this._rightHeaderActions; this.addDisposables(this._rightHeaderActions); this._rightHeaderActions.init({ containerApi: this._api, @@ -582,10 +632,16 @@ export class DockviewGroupPanelModel } if (this.accessor.options.createLeftHeaderActionComponent) { + if(this._leftHeaderActionsDisposable) { + this._leftHeaderActionsDisposable.dispose() + this.removeDisposable(this._leftHeaderActionsDisposable); + } + this._leftHeaderActions = this.accessor.options.createLeftHeaderActionComponent( this.groupPanel ); + this._leftHeaderActionsDisposable = this._leftHeaderActions; this.addDisposables(this._leftHeaderActions); this._leftHeaderActions.init({ containerApi: this._api, @@ -598,10 +654,16 @@ export class DockviewGroupPanelModel } if (this.accessor.options.createPrefixHeaderActionComponent) { + if(this._prefixHeaderActionsDisposable) { + this._prefixHeaderActionsDisposable.dispose() + this.removeDisposable(this._prefixHeaderActionsDisposable); + } + this._prefixHeaderActions = this.accessor.options.createPrefixHeaderActionComponent( this.groupPanel ); + this._prefixHeaderActionsDisposable = this._prefixHeaderActions; this.addDisposables(this._prefixHeaderActions); this._prefixHeaderActions.init({ containerApi: this._api, @@ -612,6 +674,7 @@ export class DockviewGroupPanelModel this._prefixHeaderActions.element ); } + } rerender(panel: IDockviewPanel): void { diff --git a/packages/dockview-core/src/dockview/framework.ts b/packages/dockview-core/src/dockview/framework.ts index 1ed239b464..f03672d345 100644 --- a/packages/dockview-core/src/dockview/framework.ts +++ b/packages/dockview-core/src/dockview/framework.ts @@ -4,6 +4,7 @@ import { DockviewPanelApi } from '../api/dockviewPanelApi'; import { PanelParameters } from '../framwork'; import { DockviewGroupPanel, IDockviewGroupPanel } from './dockviewGroupPanel'; import { IDockviewPanel } from './dockviewPanel'; +import { IHeaderPosition } from './options'; export interface IGroupPanelBaseProps extends PanelParameters { @@ -27,6 +28,7 @@ export interface IDockviewHeaderActionsProps { activePanel: IDockviewPanel | undefined; isGroupActive: boolean; group: DockviewGroupPanel; + headerPosition: IHeaderPosition; } export interface IGroupHeaderProps { diff --git a/packages/dockview-core/src/dockview/options.ts b/packages/dockview-core/src/dockview/options.ts index 39e66d2c36..e0a26067a3 100644 --- a/packages/dockview-core/src/dockview/options.ts +++ b/packages/dockview-core/src/dockview/options.ts @@ -35,6 +35,9 @@ export interface ViewFactoryData { tab?: string; } +export type IHeaderPosition = 'top' | 'bottom' | 'left' | 'right'; +export type IHeaderDirection = 'horizontal' | 'vertical'; + export interface DockviewOptions { /** * Disable the auto-resizing which is controlled through a `ResizeObserver`. diff --git a/packages/dockview-core/src/lifecycle.ts b/packages/dockview-core/src/lifecycle.ts index f9b69bfdea..702c30d5c4 100644 --- a/packages/dockview-core/src/lifecycle.ts +++ b/packages/dockview-core/src/lifecycle.ts @@ -24,7 +24,7 @@ export namespace Disposable { } export class CompositeDisposable { - private _disposables: IDisposable[]; + private _disposables: Set; private _isDisposed = false; get isDisposed(): boolean { @@ -32,11 +32,14 @@ export class CompositeDisposable { } constructor(...args: IDisposable[]) { - this._disposables = args; + this._disposables = new Set(args); } public addDisposables(...args: IDisposable[]): void { - args.forEach((arg) => this._disposables.push(arg)); + args.forEach((arg) => this._disposables.add(arg)); + } + public removeDisposable(disposable: IDisposable): void { + this._disposables.delete(disposable); } public dispose(): void { @@ -46,7 +49,7 @@ export class CompositeDisposable { this._isDisposed = true; this._disposables.forEach((arg) => arg.dispose()); - this._disposables = []; + this._disposables.clear(); } } diff --git a/packages/dockview/src/dockview/headerActionsRenderer.ts b/packages/dockview/src/dockview/headerActionsRenderer.ts index fcf4f273db..0b23b74af6 100644 --- a/packages/dockview/src/dockview/headerActionsRenderer.ts +++ b/packages/dockview/src/dockview/headerActionsRenderer.ts @@ -65,6 +65,7 @@ export class ReactHeaderActionsRendererPart implements IHeaderActionsRenderer { activePanel: this._group.model.activePanel, isGroupActive: this._group.api.isActive, group: this._group, + headerPosition: this._group.headerPosition, } ); } diff --git a/packages/docs/sandboxes/react/dockview/demo-dockview/src/controls.tsx b/packages/docs/sandboxes/react/dockview/demo-dockview/src/controls.tsx index c9fd5e19f1..854bdcf3f9 100644 --- a/packages/docs/sandboxes/react/dockview/demo-dockview/src/controls.tsx +++ b/packages/docs/sandboxes/react/dockview/demo-dockview/src/controls.tsx @@ -73,12 +73,16 @@ export const RightControls = (props: IDockviewHeaderActionsProps) => { } }; + const vertical = props.group.headerPosition === 'left' || props.group.headerPosition === 'right'; + return (
{ style={{ display: 'flex', alignItems: 'center', + justifyContent: 'center', padding: '0px 8px', height: '100%', color: 'var(--dv-activegroup-visiblepanel-tab-color)', @@ -137,6 +142,7 @@ export const PrefixHeaderControls = (props: IDockviewHeaderActionsProps) => { style={{ display: 'flex', alignItems: 'center', + justifyContent: 'center', padding: '0px 8px', height: '100%', color: 'var(--dv-activegroup-visiblepanel-tab-color)', diff --git a/packages/docs/sandboxes/react/dockview/demo-dockview/src/groupActions.tsx b/packages/docs/sandboxes/react/dockview/demo-dockview/src/groupActions.tsx index 0af2a66cc4..3b9ef0931f 100644 --- a/packages/docs/sandboxes/react/dockview/demo-dockview/src/groupActions.tsx +++ b/packages/docs/sandboxes/react/dockview/demo-dockview/src/groupActions.tsx @@ -103,6 +103,23 @@ const GroupAction = (props: { > ad_group +