diff --git a/packages/dockview-angular/src/lib/dockview/dockview-angular.component.ts b/packages/dockview-angular/src/lib/dockview/dockview-angular.component.ts index 81e1c8525c..c2de4eaec7 100644 --- a/packages/dockview-angular/src/lib/dockview/dockview-angular.component.ts +++ b/packages/dockview-angular/src/lib/dockview/dockview-angular.component.ts @@ -28,16 +28,7 @@ import { } from 'dockview-core'; import { AngularFrameworkComponentFactory } from '../utils/component-factory'; import { AngularLifecycleManager } from '../utils/lifecycle-utils'; - -export interface DockviewAngularOptions extends DockviewOptions { - components: Record>; - tabComponents?: Record>; - watermarkComponent?: Type; - defaultTabComponent?: Type; - leftHeaderActionsComponent?: Type; - rightHeaderActionsComponent?: Type; - prefixHeaderActionsComponent?: Type; -} +import { DockviewAngularOptions, IAngularTabOverflowConfig } from './types'; @Component({ selector: 'dv-dockview', @@ -68,6 +59,7 @@ export class DockviewAngularComponent implements OnInit, OnDestroy, OnChanges { @Input() leftHeaderActionsComponent?: Type; @Input() rightHeaderActionsComponent?: Type; @Input() prefixHeaderActionsComponent?: Type; + @Input() tabOverflowComponent?: Type | IAngularTabOverflowConfig; // Core dockview options as inputs @Input() className?: string; @@ -182,7 +174,8 @@ export class DockviewAngularComponent implements OnInit, OnDestroy, OnChanges { this.tabComponents, this.watermarkComponent, headerActionsComponents, - this.defaultTabComponent + this.defaultTabComponent, + this.tabOverflowComponent ); return { @@ -211,6 +204,11 @@ export class DockviewAngularComponent implements OnInit, OnDestroy, OnChanges { ? (group) => { return componentFactory.createHeaderActionsComponent('prefix')!; } + : undefined, + createTabOverflowComponent: this.tabOverflowComponent + ? (group) => { + return componentFactory.createTabOverflowComponent()!; + } : undefined }; } diff --git a/packages/dockview-angular/src/lib/dockview/types.ts b/packages/dockview-angular/src/lib/dockview/types.ts index b2a9adf274..eae031c71e 100644 --- a/packages/dockview-angular/src/lib/dockview/types.ts +++ b/packages/dockview-angular/src/lib/dockview/types.ts @@ -8,7 +8,8 @@ import { IDockviewPanelProps, IDockviewPanelHeaderProps, IWatermarkPanelProps, - IDockviewHeaderActionsProps + IDockviewHeaderActionsProps, + TabOverflowEvent } from 'dockview-core'; export interface IDockviewAngularPanelProps extends IDockviewPanelProps { @@ -27,6 +28,19 @@ export interface IDockviewAngularHeaderActionsProps extends IDockviewHeaderActio // Angular-specific header actions properties can be added here } +export interface IDockviewAngularTabOverflowProps { + event: TabOverflowEvent; +} + +export interface IDockviewAngularTabOverflowTriggerProps { + event: TabOverflowEvent; +} + +export interface IAngularTabOverflowConfig { + content?: Type; + trigger?: Type; +} + export interface DockviewAngularOptions extends DockviewOptions { components: Record>; tabComponents?: Record>; @@ -35,6 +49,7 @@ export interface DockviewAngularOptions extends DockviewOptions { leftHeaderActionsComponent?: Type; rightHeaderActionsComponent?: Type; prefixHeaderActionsComponent?: Type; + tabOverflowComponent?: Type | IAngularTabOverflowConfig; } // Alias for backward compatibility diff --git a/packages/dockview-angular/src/lib/utils/angular-renderer.ts b/packages/dockview-angular/src/lib/utils/angular-renderer.ts index f1ee06075f..c79e77cacf 100644 --- a/packages/dockview-angular/src/lib/utils/angular-renderer.ts +++ b/packages/dockview-angular/src/lib/utils/angular-renderer.ts @@ -12,8 +12,11 @@ import { import { IContentRenderer, IFrameworkPart, + ITabOverflowRenderer, + ITabOverflowTriggerRenderer, DockviewIDisposable, - Parameters + Parameters, + TabOverflowEvent } from 'dockview-core'; export interface AngularRendererOptions { @@ -94,4 +97,136 @@ export class AngularRenderer implements IContentRenderer, IFrameworkPart { } this._element = null; } +} + +export class AngularTabOverflowRenderer implements ITabOverflowRenderer { + private componentRef: ComponentRef | null = null; + private _element: HTMLElement; + + constructor( + private options: AngularRendererOptions + ) { + this._element = document.createElement('div'); + this._element.className = 'dv-angular-tab-overflow-part'; + this._element.style.height = '100%'; + } + + get element(): HTMLElement { + return this._element; + } + + update(event: TabOverflowEvent): void { + if (!this.componentRef) { + this.render(event); + } else { + this.updateComponent(event); + } + } + + private render(event: TabOverflowEvent): void { + try { + this.componentRef = createComponent(this.options.component, { + environmentInjector: this.options.environmentInjector || this.options.injector as EnvironmentInjector, + elementInjector: this.options.injector + }); + + // Set the event data + if ('event' in this.componentRef.instance) { + this.componentRef.instance.event = event; + } + + // Get the component's DOM element and append to our element + const hostView = this.componentRef.hostView as EmbeddedViewRef; + const componentElement = hostView.rootNodes[0] as HTMLElement; + this._element.appendChild(componentElement); + + // Trigger change detection + this.componentRef.changeDetectorRef.detectChanges(); + + } catch (error) { + console.error('Error creating Angular tab overflow component:', error); + throw error; + } + } + + private updateComponent(event: TabOverflowEvent): void { + if (this.componentRef && 'event' in this.componentRef.instance) { + this.componentRef.instance.event = event; + this.componentRef.changeDetectorRef.detectChanges(); + } + } + + dispose(): void { + if (this.componentRef) { + this.componentRef.destroy(); + this.componentRef = null; + } + this._element.innerHTML = ''; + } +} + +export class AngularTabOverflowTriggerRenderer implements ITabOverflowTriggerRenderer { + private componentRef: ComponentRef | null = null; + private _element: HTMLElement; + + constructor( + private options: AngularRendererOptions + ) { + this._element = document.createElement('div'); + this._element.className = 'dv-angular-tab-overflow-trigger-part'; + this._element.style.height = '100%'; + } + + get element(): HTMLElement { + return this._element; + } + + update(event: TabOverflowEvent): void { + if (!this.componentRef) { + this.render(event); + } else { + this.updateComponent(event); + } + } + + private render(event: TabOverflowEvent): void { + try { + this.componentRef = createComponent(this.options.component, { + environmentInjector: this.options.environmentInjector || this.options.injector as EnvironmentInjector, + elementInjector: this.options.injector + }); + + // Set the event data + if ('event' in this.componentRef.instance) { + this.componentRef.instance.event = event; + } + + // Get the component's DOM element and append to our element + const hostView = this.componentRef.hostView as EmbeddedViewRef; + const componentElement = hostView.rootNodes[0] as HTMLElement; + this._element.appendChild(componentElement); + + // Trigger change detection + this.componentRef.changeDetectorRef.detectChanges(); + + } catch (error) { + console.error('Error creating Angular tab overflow trigger component:', error); + throw error; + } + } + + private updateComponent(event: TabOverflowEvent): void { + if (this.componentRef && 'event' in this.componentRef.instance) { + this.componentRef.instance.event = event; + this.componentRef.changeDetectorRef.detectChanges(); + } + } + + dispose(): void { + if (this.componentRef) { + this.componentRef.destroy(); + this.componentRef = null; + } + this._element.innerHTML = ''; + } } \ No newline at end of file diff --git a/packages/dockview-angular/src/lib/utils/component-factory.ts b/packages/dockview-angular/src/lib/utils/component-factory.ts index 6934e0389e..6620d5afb4 100644 --- a/packages/dockview-angular/src/lib/utils/component-factory.ts +++ b/packages/dockview-angular/src/lib/utils/component-factory.ts @@ -6,6 +6,8 @@ import { ITabRenderer, IWatermarkRenderer, IHeaderActionsRenderer, + ITabOverflowRenderer, + ITabOverflowTriggerRenderer, TabPartInitParameters, WatermarkRendererInitParameters, GroupPanelPartInitParameters, @@ -14,10 +16,11 @@ import { SplitviewPanel, IPanePart } from 'dockview-core'; -import { AngularRenderer, AngularRendererOptions } from './angular-renderer'; +import { AngularRenderer, AngularRendererOptions, AngularTabOverflowRenderer, AngularTabOverflowTriggerRenderer } from './angular-renderer'; import { AngularGridviewPanel } from '../gridview/angular-gridview-panel'; import { AngularSplitviewPanel } from '../splitview/angular-splitview-panel'; import { AngularPanePart } from '../paneview/angular-pane-part'; +import { IAngularTabOverflowConfig } from '../dockview/types'; export class AngularFrameworkComponentFactory { constructor( @@ -27,7 +30,8 @@ export class AngularFrameworkComponentFactory { private tabComponents?: Record>, private watermarkComponent?: Type, private headerActionsComponents?: Record>, - private defaultTabComponent?: Type + private defaultTabComponent?: Type, + private tabOverflowComponent?: Type | IAngularTabOverflowConfig ) {} // For DockviewComponent @@ -150,4 +154,42 @@ export class AngularFrameworkComponentFactory { renderer.init({}); return renderer; } + + createTabOverflowComponent(): ITabOverflowRenderer | any | undefined { + if (!this.tabOverflowComponent) { + return undefined; + } + + // Check if it's a config object or just a Type (component class) + if ('content' in this.tabOverflowComponent || 'trigger' in this.tabOverflowComponent) { + // New: config object with content and/or trigger + const config = this.tabOverflowComponent as IAngularTabOverflowConfig; + const result: any = {}; + + if (config.content) { + result.content = new AngularTabOverflowRenderer({ + component: config.content, + injector: this.injector, + environmentInjector: this.environmentInjector + }); + } + + if (config.trigger) { + result.trigger = new AngularTabOverflowTriggerRenderer({ + component: config.trigger, + injector: this.injector, + environmentInjector: this.environmentInjector + }); + } + + return result; + } else { + // Legacy: single component for content only + return new AngularTabOverflowRenderer({ + component: this.tabOverflowComponent as Type, + injector: this.injector, + environmentInjector: this.environmentInjector + }); + } + } } \ No newline at end of file diff --git a/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts b/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts index 9d578bc98c..9423eff0ab 100644 --- a/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts +++ b/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts @@ -18,6 +18,13 @@ import { createDropdownElementHandle, DropdownElement, } from './tabOverflowControl'; +import { + ITabOverflowRenderer, + ITabOverflowTriggerRenderer, + ITabOverflowConfig, + OverflowTabData, + TabOverflowEvent, +} from '../../options'; export interface TabDropIndexEvent { readonly event: DragEvent; @@ -76,6 +83,8 @@ export class TabsContainer private _hidden = false; private dropdownPart: DropdownElement | null = null; + private customOverflowRenderer: ITabOverflowRenderer | null = null; + private customTriggerRenderer: ITabOverflowTriggerRenderer | null = null; private _overflowTabs: string[] = []; private readonly _dropdownDisposable = new MutableDisposable(); @@ -140,18 +149,44 @@ export class TabsContainer this.preActionsContainer = document.createElement('div'); this.preActionsContainer.className = 'dv-pre-actions-container'; + // Determine if we should show built-in overflow control + const showBuiltInOverflow = !accessor.options.disableTabsOverflowList && + (!accessor.options.createTabOverflowComponent || + (accessor.options.createTabOverflowComponent && + !this.wouldHaveCustomTrigger(accessor.options.createTabOverflowComponent(group)))); + this.tabs = new Tabs(group, accessor, { - showTabsOverflowControl: !accessor.options.disableTabsOverflowList, + showTabsOverflowControl: showBuiltInOverflow, }); this.voidContainer = new VoidContainer(this.accessor, this.group); + // Initialize custom overflow renderer(s) if provided + if (accessor.options.createTabOverflowComponent) { + const overflowComponent = accessor.options.createTabOverflowComponent(group); + + // Check if it's a config object or just a renderer + if ('content' in overflowComponent || 'trigger' in overflowComponent) { + const config = overflowComponent as ITabOverflowConfig; + this.customOverflowRenderer = config.content || null; + this.customTriggerRenderer = config.trigger || null; + } else { + // Backward compatibility: treat as content renderer only + this.customOverflowRenderer = overflowComponent as ITabOverflowRenderer; + } + } + this._element.appendChild(this.preActionsContainer); this._element.appendChild(this.tabs.element); this._element.appendChild(this.leftActionsContainer); this._element.appendChild(this.voidContainer.element); this._element.appendChild(this.rightActionsContainer); + // Add custom overflow renderer element if present + if (this.customOverflowRenderer) { + this.rightActionsContainer.appendChild(this.customOverflowRenderer.element); + } + this.addDisposables( this.tabs.onDrop((e) => this._onDrop.fire(e)), this.tabs.onWillShowOverlay((e) => this._onWillShowOverlay.fire(e)), @@ -220,7 +255,9 @@ export class TabsContainer }); } } - ) + ), + this.customOverflowRenderer ?? Disposable.NONE, + this.customTriggerRenderer ?? Disposable.NONE ); } @@ -314,6 +351,53 @@ export class TabsContainer const tabs = options.reset ? [] : options.tabs; this._overflowTabs = tabs; + // If we have custom overflow renderer(s), use them instead + if (this.customOverflowRenderer || this.customTriggerRenderer) { + const overflowData: OverflowTabData[] = this._overflowTabs.map((tabId) => { + const tab = this.tabs.tabs.find((t) => t.panel.id === tabId); + if (!tab) { + throw new Error(`Tab not found: ${tabId}`); + } + return { + id: tab.panel.id, + title: tab.panel.title ?? '', + isActive: tab.panel.api.isActive, + panel: tab.panel, + }; + }); + + const event: TabOverflowEvent = { + tabs: overflowData, + isVisible: tabs.length > 0, + triggerElement: this.rightActionsContainer, + }; + + // Update custom content renderer if provided + if (this.customOverflowRenderer) { + this.customOverflowRenderer.update(event); + } + + // Update custom trigger renderer if provided + if (this.customTriggerRenderer) { + // Remove any existing built-in trigger element + if (this.dropdownPart) { + this._dropdownDisposable.dispose(); + this.dropdownPart = null; + } + + this.customTriggerRenderer.update(event); + + // Add custom trigger to the right actions container + if (tabs.length > 0 && !this.rightActionsContainer.contains(this.customTriggerRenderer.element)) { + this.rightActionsContainer.prepend(this.customTriggerRenderer.element); + } else if (tabs.length === 0 && this.rightActionsContainer.contains(this.customTriggerRenderer.element)) { + this.rightActionsContainer.removeChild(this.customTriggerRenderer.element); + } + } + return; + } + + // Default behavior for built-in overflow dropdown if (this._overflowTabs.length > 0 && this.dropdownPart) { this.dropdownPart.update({ tabs: tabs.length }); return; @@ -411,4 +495,13 @@ export class TabsContainer this.tabs.updateDragAndDropState(); this.voidContainer.updateDragAndDropState(); } + + private wouldHaveCustomTrigger(overflowComponent: ITabOverflowRenderer | ITabOverflowConfig): boolean { + // Check if it's a config object with a trigger property + if ('content' in overflowComponent || 'trigger' in overflowComponent) { + const config = overflowComponent as ITabOverflowConfig; + return !!config.trigger; + } + return false; + } } diff --git a/packages/dockview-core/src/dockview/options.ts b/packages/dockview-core/src/dockview/options.ts index 708c71407b..1b213c29e8 100644 --- a/packages/dockview-core/src/dockview/options.ts +++ b/packages/dockview-core/src/dockview/options.ts @@ -28,6 +28,34 @@ export interface TabContextMenuEvent { panel: IDockviewPanel; } +export interface OverflowTabData { + id: string; + title: string; + isActive: boolean; + panel: IDockviewPanel; +} + +export interface TabOverflowEvent { + tabs: OverflowTabData[]; + isVisible: boolean; + triggerElement: HTMLElement; +} + +export interface ITabOverflowRenderer extends IDisposable { + readonly element: HTMLElement; + update(event: TabOverflowEvent): void; +} + +export interface ITabOverflowTriggerRenderer extends IDisposable { + readonly element: HTMLElement; + update(event: TabOverflowEvent): void; +} + +export interface ITabOverflowConfig { + content?: ITabOverflowRenderer; + trigger?: ITabOverflowTriggerRenderer; +} + export interface ViewFactoryData { content: string; tab?: string; @@ -154,6 +182,9 @@ export interface DockviewFrameworkOptions { ) => ITabRenderer | undefined; createComponent: (options: CreateComponentOptions) => IContentRenderer; createWatermarkComponent?: () => IWatermarkRenderer; + createTabOverflowComponent?: ( + group: DockviewGroupPanel + ) => ITabOverflowRenderer | ITabOverflowConfig; } export type DockviewComponentOptions = DockviewOptions & diff --git a/packages/dockview-vue/src/dockview/dockview.vue b/packages/dockview-vue/src/dockview/dockview.vue index 35508d49a6..69a2881f4e 100644 --- a/packages/dockview-vue/src/dockview/dockview.vue +++ b/packages/dockview-vue/src/dockview/dockview.vue @@ -19,10 +19,12 @@ import { import { VueHeaderActionsRenderer, VueRenderer, + VueTabOverflowRenderer, + VueTabOverflowTriggerRenderer, VueWatermarkRenderer, findComponent, } from '../utils'; -import type { IDockviewVueProps, VueEvents } from './types'; +import type { IDockviewVueProps, VueEvents, IVueTabOverflowConfig } from './types'; function extractCoreOptions(props: IDockviewVueProps): DockviewOptions { const coreOptions = (PROPERTY_KEYS_DOCKVIEW as (keyof DockviewOptions)[]).reduce( @@ -140,6 +142,53 @@ onMounted(() => { ); } : undefined, + createTabOverflowComponent: props.tabOverflowComponent + ? (group) => { + // Check if it's a config object or just a string component name + if (typeof props.tabOverflowComponent === 'string') { + // Legacy: single component for content only + const component = findComponent( + inst, + props.tabOverflowComponent + ); + return new VueTabOverflowRenderer( + component!, + inst, + group + ); + } else { + // New: config object with content and/or trigger + const config = props.tabOverflowComponent as IVueTabOverflowConfig; + const result: any = {}; + + if (config.content) { + const contentComponent = findComponent( + inst, + config.content + ); + result.content = new VueTabOverflowRenderer( + contentComponent!, + inst, + group + ); + } + + if (config.trigger) { + const triggerComponent = findComponent( + inst, + config.trigger + ); + result.trigger = new VueTabOverflowTriggerRenderer( + triggerComponent!, + inst, + group + ); + } + + return result; + } + } + : undefined, }; const api = createDockview(el.value, { diff --git a/packages/dockview-vue/src/dockview/types.ts b/packages/dockview-vue/src/dockview/types.ts index ca2e09778d..b84d643289 100644 --- a/packages/dockview-vue/src/dockview/types.ts +++ b/packages/dockview-vue/src/dockview/types.ts @@ -1,11 +1,17 @@ import { type DockviewOptions, type DockviewReadyEvent } from 'dockview-core'; +export interface IVueTabOverflowConfig { + content?: string; + trigger?: string; +} + export interface VueProps { watermarkComponent?: string; defaultTabComponent?: string; rightHeaderActionsComponent?: string; leftHeaderActionsComponent?: string; prefixHeaderActionsComponent?: string; + tabOverflowComponent?: string | IVueTabOverflowConfig; } export type VueEvents = { diff --git a/packages/dockview-vue/src/utils.ts b/packages/dockview-vue/src/utils.ts index 17c9570561..64d59f319e 100644 --- a/packages/dockview-vue/src/utils.ts +++ b/packages/dockview-vue/src/utils.ts @@ -6,11 +6,14 @@ import type { IDockviewPanelHeaderProps, IGroupHeaderProps, IHeaderActionsRenderer, + ITabOverflowRenderer, + ITabOverflowTriggerRenderer, ITabRenderer, IWatermarkPanelProps, IWatermarkRenderer, PanelUpdateEvent, Parameters, + TabOverflowEvent, TabPartInitParameters, WatermarkRendererInitParameters, } from 'dockview-core'; @@ -232,6 +235,82 @@ export class VueHeaderActionsRenderer } } +export class VueTabOverflowRenderer + extends AbstractVueRenderer + implements ITabOverflowRenderer +{ + private _renderDisposable: + | { update: (props: any) => void; dispose: () => void } + | undefined; + + get element(): HTMLElement { + return this._element; + } + + constructor( + component: VueComponent, + parent: ComponentInternalInstance, + group: DockviewGroupPanel + ) { + super(component, parent); + } + + update(event: TabOverflowEvent): void { + if (!this._renderDisposable) { + this._renderDisposable = mountVueComponent( + this.component, + this.parent, + { event }, + this.element + ); + } else { + this._renderDisposable.update({ event }); + } + } + + dispose(): void { + this._renderDisposable?.dispose(); + } +} + +export class VueTabOverflowTriggerRenderer + extends AbstractVueRenderer + implements ITabOverflowTriggerRenderer +{ + private _renderDisposable: + | { update: (props: any) => void; dispose: () => void } + | undefined; + + get element(): HTMLElement { + return this._element; + } + + constructor( + component: VueComponent, + parent: ComponentInternalInstance, + group: DockviewGroupPanel + ) { + super(component, parent); + } + + update(event: TabOverflowEvent): void { + if (!this._renderDisposable) { + this._renderDisposable = mountVueComponent( + this.component, + this.parent, + { event }, + this.element + ); + } else { + this._renderDisposable.update({ event }); + } + } + + dispose(): void { + this._renderDisposable?.dispose(); + } +} + export class VuePart = any> { private _renderDisposable: | { update: (props: any) => void; dispose: () => void } diff --git a/packages/dockview/src/dockview/dockview.tsx b/packages/dockview/src/dockview/dockview.tsx index 1d224c7c62..e2102900c0 100644 --- a/packages/dockview/src/dockview/dockview.tsx +++ b/packages/dockview/src/dockview/dockview.tsx @@ -21,6 +21,8 @@ import { ReactPanelHeaderPart } from './reactHeaderPart'; import { ReactPortalStore, usePortalsLifecycle } from '../react'; import { ReactWatermarkPart } from './reactWatermarkPart'; import { ReactHeaderActionsRendererPart } from './headerActionsRenderer'; +import { ReactTabOverflowPart, ReactTabOverflowTriggerPart } from './reactTabOverflowPart'; +import { ITabOverflowProps, ITabOverflowTriggerProps, IReactTabOverflowConfig } from '../types'; function createGroupControlElement( component: React.FunctionComponent | undefined, @@ -50,6 +52,7 @@ export interface IDockviewReactProps extends DockviewOptions { rightHeaderActionsComponent?: React.FunctionComponent; leftHeaderActionsComponent?: React.FunctionComponent; prefixHeaderActionsComponent?: React.FunctionComponent; + tabOverflowComponent?: React.FunctionComponent | IReactTabOverflowConfig; // onReady: (event: DockviewReadyEvent) => void; onDidDrop?: (event: DockviewDidDropEvent) => void; @@ -155,6 +158,41 @@ export const DockviewReact = React.forwardRef( ); } : undefined, + createTabOverflowComponent: props.tabOverflowComponent + ? (group: DockviewGroupPanel) => { + // Check if it's a config object or just a function component + if (typeof props.tabOverflowComponent === 'function') { + // Legacy: single component for content only + return new ReactTabOverflowPart( + props.tabOverflowComponent, + { addPortal }, + group + ); + } else { + // New: config object with content and/or trigger + const config = props.tabOverflowComponent as IReactTabOverflowConfig; + const result: any = {}; + + if (config.content) { + result.content = new ReactTabOverflowPart( + config.content, + { addPortal }, + group + ); + } + + if (config.trigger) { + result.trigger = new ReactTabOverflowTriggerPart( + config.trigger, + { addPortal }, + group + ); + } + + return result; + } + } + : undefined, defaultTabComponent: props.defaultTabComponent ? DEFAULT_REACT_TAB : undefined, @@ -318,6 +356,49 @@ export const DockviewReact = React.forwardRef( }); }, [props.prefixHeaderActionsComponent]); + React.useEffect(() => { + if (!dockviewRef.current) { + return; + } + dockviewRef.current.updateOptions({ + createTabOverflowComponent: props.tabOverflowComponent + ? (group: DockviewGroupPanel) => { + // Check if it's a config object or just a function component + if (typeof props.tabOverflowComponent === 'function') { + // Legacy: single component for content only + return new ReactTabOverflowPart( + props.tabOverflowComponent, + { addPortal }, + group + ); + } else { + // New: config object with content and/or trigger + const config = props.tabOverflowComponent as IReactTabOverflowConfig; + const result: any = {}; + + if (config.content) { + result.content = new ReactTabOverflowPart( + config.content, + { addPortal }, + group + ); + } + + if (config.trigger) { + result.trigger = new ReactTabOverflowTriggerPart( + config.trigger, + { addPortal }, + group + ); + } + + return result; + } + } + : undefined, + }); + }, [props.tabOverflowComponent]); + return (
{portals} diff --git a/packages/dockview/src/dockview/reactTabOverflowPart.ts b/packages/dockview/src/dockview/reactTabOverflowPart.ts new file mode 100644 index 0000000000..e58ca5e685 --- /dev/null +++ b/packages/dockview/src/dockview/reactTabOverflowPart.ts @@ -0,0 +1,85 @@ +import React from 'react'; +import { + ITabOverflowRenderer, + ITabOverflowTriggerRenderer, + TabOverflowEvent, + DockviewGroupPanel, +} from 'dockview-core'; +import { ReactPart, ReactPortalStore } from '../react'; +import { ITabOverflowProps, ITabOverflowTriggerProps } from '../types'; + +export class ReactTabOverflowPart implements ITabOverflowRenderer { + private _element: HTMLElement; + private part?: ReactPart; + + constructor( + private readonly component: React.FunctionComponent, + private readonly reactPortalStore: ReactPortalStore, + private readonly group: DockviewGroupPanel + ) { + this._element = document.createElement('div'); + this._element.style.height = '100%'; + this._element.className = 'dv-react-tab-overflow-part'; + } + + get element(): HTMLElement { + return this._element; + } + + update(event: TabOverflowEvent): void { + if (!this.part) { + // Initialize the React part with initial event data + this.part = new ReactPart( + this.element, + this.reactPortalStore, + this.component, + { event } + ); + } else { + // Update the existing React part with new event data + this.part.update({ event }); + } + } + + dispose(): void { + this.part?.dispose(); + } +} + +export class ReactTabOverflowTriggerPart implements ITabOverflowTriggerRenderer { + private _element: HTMLElement; + private part?: ReactPart; + + constructor( + private readonly component: React.FunctionComponent, + private readonly reactPortalStore: ReactPortalStore, + private readonly group: DockviewGroupPanel + ) { + this._element = document.createElement('div'); + this._element.style.height = '100%'; + this._element.className = 'dv-react-tab-overflow-trigger-part'; + } + + get element(): HTMLElement { + return this._element; + } + + update(event: TabOverflowEvent): void { + if (!this.part) { + // Initialize the React part with initial event data + this.part = new ReactPart( + this.element, + this.reactPortalStore, + this.component, + { event } + ); + } else { + // Update the existing React part with new event data + this.part.update({ event }); + } + } + + dispose(): void { + this.part?.dispose(); + } +} \ No newline at end of file diff --git a/packages/dockview/src/types.ts b/packages/dockview/src/types.ts index a593bab270..ca9ec34138 100644 --- a/packages/dockview/src/types.ts +++ b/packages/dockview/src/types.ts @@ -1,5 +1,19 @@ -import { Parameters } from 'dockview-core'; +import React from 'react'; +import { Parameters, TabOverflowEvent } from 'dockview-core'; export interface PanelParameters { params: T; } + +export interface ITabOverflowProps { + event: TabOverflowEvent; +} + +export interface ITabOverflowTriggerProps { + event: TabOverflowEvent; +} + +export interface IReactTabOverflowConfig { + content?: React.FunctionComponent; + trigger?: React.FunctionComponent; +} diff --git a/packages/docs/docs/core/panels/customTabOverflow.mdx b/packages/docs/docs/core/panels/customTabOverflow.mdx new file mode 100644 index 0000000000..a7a16cbfc5 --- /dev/null +++ b/packages/docs/docs/core/panels/customTabOverflow.mdx @@ -0,0 +1,712 @@ +--- +title: Custom Tab Overflow +--- + +import { DocRef } from '@site/src/components/ui/reference/docRef'; + +This feature allows you to provide your own custom components for rendering tab overflow menus instead of using the built-in dropdown. You can customize both the trigger element (the button that appears in the tab header) and the overflow content (the menu that displays the hidden tabs). + +## Overview + +When there are too many tabs to display in the available space, Dockview normally shows a built-in overflow dropdown. With this feature, you can replace that with your own custom components for better styling consistency and enhanced accessibility. + +You can customize: +- **Content only** (legacy mode): Replace just the overflow menu content +- **Both trigger and content**: Replace both the trigger button and the overflow menu +- **Trigger only**: Replace just the trigger button while keeping the built-in overflow menu + +## Options + + + + + + + + + + + + + + + + + +## Basic Usage (Content Only) + + + +```tsx +import React from 'react'; +import { DockviewReact, ITabOverflowProps } from 'dockview'; + +const CustomTabOverflow: React.FC = ({ event }) => { + const [isOpen, setIsOpen] = React.useState(false); + + React.useEffect(() => { + if (!event.isVisible) { + setIsOpen(false); + } + }, [event.isVisible]); + + if (!event.isVisible) { + return null; + } + + return ( +
+ + + {isOpen && ( +
+ {event.tabs.map((tab) => ( +
{ + tab.panel.api.setActive(); + setIsOpen(false); + }} + style={{ + padding: '8px 12px', + cursor: 'pointer', + borderBottom: '1px solid #eee', + backgroundColor: tab.isActive ? '#e6f3ff' : 'transparent', + }} + > + {tab.title} + {tab.isActive && ( + + (active) + + )} +
+ ))} +
+ )} +
+ ); +}; + + +``` + +
+ + + +```vue + + + +``` + + + + + +```typescript +// custom-tab-overflow.component.ts +import { Component, Input } from '@angular/core'; +import { TabOverflowEvent } from 'dockview-core'; + +@Component({ + selector: 'app-custom-tab-overflow', + template: ` +
+ + +
+
+ {{ tab.title }} + + (active) + +
+
+
+ ` +}) +export class CustomTabOverflowComponent { + @Input() event!: TabOverflowEvent; + isOpen = false; + + activateTab(tab: any) { + tab.panel.api.setActive(); + this.isOpen = false; + } +} + +// app.component.ts +@Component({ + selector: 'app-root', + template: ` + + + ` +}) +export class AppComponent { + components = { /* ... your panel components */ }; + tabOverflowComponent = CustomTabOverflowComponent; +} +``` + +
+ + + +```javascript +import { createDockview } from 'dockview-core'; + +class CustomTabOverflowRenderer { + constructor() { + this._element = document.createElement('div'); + this._element.style.position = 'relative'; + this.isOpen = false; + } + + get element() { + return this._element; + } + + update(event) { + if (!event.isVisible) { + this._element.style.display = 'none'; + this.isOpen = false; + return; + } + + this._element.style.display = 'block'; + + // Create button + const button = document.createElement('button'); + button.textContent = `+${event.tabs.length} more`; + button.style.cssText = ` + padding: 4px 8px; + border: 1px solid #ccc; + border-radius: 4px; + background: white; + cursor: pointer; + `; + + // Create dropdown + const dropdown = document.createElement('div'); + dropdown.style.cssText = ` + position: absolute; + top: 100%; + right: 0; + background: white; + border: 1px solid #ccc; + border-radius: 4px; + box-shadow: 0 2px 8px rgba(0,0,0,0.15); + z-index: 1000; + min-width: 200px; + display: ${this.isOpen ? 'block' : 'none'}; + `; + + // Add tab items + event.tabs.forEach(tab => { + const tabElement = document.createElement('div'); + tabElement.style.cssText = ` + padding: 8px 12px; + cursor: pointer; + border-bottom: 1px solid #eee; + background-color: ${tab.isActive ? '#e6f3ff' : 'transparent'}; + `; + + tabElement.innerHTML = ` + ${tab.title} + ${tab.isActive ? '(active)' : ''} + `; + + tabElement.addEventListener('click', () => { + tab.panel.api.setActive(); + this.isOpen = false; + this.update(event); + }); + + dropdown.appendChild(tabElement); + }); + + // Toggle dropdown + button.addEventListener('click', () => { + this.isOpen = !this.isOpen; + dropdown.style.display = this.isOpen ? 'block' : 'none'; + }); + + // Rebuild element + this._element.innerHTML = ''; + this._element.appendChild(button); + this._element.appendChild(dropdown); + } + + dispose() { + // cleanup + } +} + +const api = createDockview(element, { + createTabOverflowComponent: () => new CustomTabOverflowRenderer(), + // ... other options +}); +``` + + + +## Enhanced Usage (Trigger and Content) + + + +You can customize both the trigger element and the overflow content using a configuration object: + +```tsx +import React from 'react'; +import { DockviewReact, ITabOverflowProps, ITabOverflowTriggerProps } from 'dockview'; + +// Custom trigger component (appears in tab header) +const CustomTrigger: React.FC = ({ event }) => { + if (!event.isVisible) return null; + + return ( + + ); +}; + +// Custom content component (the overflow menu) +const CustomContent: React.FC = ({ event }) => { + return ( +
+

Hidden Tabs

+ {event.tabs.map((tab) => ( +
tab.panel.api.setActive()} + style={{ + padding: '8px', + margin: '4px 0', + borderRadius: '4px', + cursor: 'pointer', + background: tab.isActive ? '#007bff' : '#ffffff', + color: tab.isActive ? 'white' : 'black' + }} + > + {tab.title} +
+ ))} +
+ ); +}; + + +``` + +You can also customize just the trigger while keeping the built-in overflow menu: + +```tsx + +``` + +
+ + + +```vue + + + +``` + + + + + +```typescript +// custom-trigger.component.ts +@Component({ + selector: 'app-custom-trigger', + template: ` + + ` +}) +export class CustomTriggerComponent { + @Input() event!: TabOverflowEvent; +} + +// custom-content.component.ts +@Component({ + selector: 'app-custom-content', + template: ` +
+

Hidden Tabs

+
+ {{ tab.title }} +
+
+ ` +}) +export class CustomContentComponent { + @Input() event!: TabOverflowEvent; +} + +// app.component.ts +@Component({ + selector: 'app-root', + template: ` + + + ` +}) +export class AppComponent { + components = { /* ... your panel components */ }; + customTrigger = CustomTriggerComponent; + customContent = CustomContentComponent; +} +``` + +
+ + + +```javascript +import { createDockview } from 'dockview-core'; + +class CustomTriggerRenderer { + constructor() { + this._element = document.createElement('button'); + this._element.style.cssText = ` + background: #ff6b6b; + color: white; + border: none; + border-radius: 50%; + width: 24px; + height: 24px; + font-size: 12px; + cursor: pointer; + `; + } + + get element() { + return this._element; + } + + update(event) { + if (!event.isVisible) { + this._element.style.display = 'none'; + return; + } + this._element.style.display = 'block'; + this._element.textContent = event.tabs.length; + } + + dispose() {} +} + +class CustomContentRenderer { + constructor() { + this._element = document.createElement('div'); + this._element.style.cssText = ` + background: #f8f9fa; + border: 2px solid #dee2e6; + border-radius: 8px; + padding: 8px; + min-width: 250px; + `; + } + + get element() { + return this._element; + } + + update(event) { + this._element.innerHTML = ''; + + const title = document.createElement('h4'); + title.textContent = 'Hidden Tabs'; + title.style.cssText = 'margin: 0 0 8px 0; font-size: 14px;'; + this._element.appendChild(title); + + event.tabs.forEach(tab => { + const tabElement = document.createElement('div'); + tabElement.textContent = tab.title; + tabElement.style.cssText = ` + padding: 8px; + margin: 4px 0; + border-radius: 4px; + cursor: pointer; + background: ${tab.isActive ? '#007bff' : '#ffffff'}; + color: ${tab.isActive ? 'white' : 'black'}; + `; + + tabElement.addEventListener('click', () => { + tab.panel.api.setActive(); + }); + + this._element.appendChild(tabElement); + }); + } + + dispose() {} +} + +const api = createDockview(element, { + createTabOverflowComponent: () => ({ + trigger: new CustomTriggerRenderer(), + content: new CustomContentRenderer() + }), + // ... other options +}); +``` + + + +## API Reference + +### TabOverflowEvent + +The `TabOverflowEvent` provides the following data: + +- `tabs: OverflowTabData[]` - Array of tabs that are currently overflowing +- `isVisible: boolean` - Whether overflow tabs are currently present +- `triggerElement: HTMLElement` - The container element for positioning + +### OverflowTabData + +Each overflow tab provides: + +- `id: string` - Unique panel identifier +- `title: string` - Panel title +- `isActive: boolean` - Whether this tab is currently active +- `panel: IDockviewPanel` - Full panel API for interactions + +## Benefits + +- **Custom Styling**: Match your application's design system +- **Enhanced Accessibility**: Add ARIA labels, keyboard navigation, etc. +- **Custom Behavior**: Implement features like tab search, grouping, or custom actions +- **Positioning Control**: Place the overflow UI wherever makes sense in your layout + +## Live Example + + + +## Notes + +- The custom overflow component is only rendered when `event.isVisible` is true +- You can activate tabs by calling `tab.panel.api.setActive()` +- The component receives updates whenever the overflow state changes +- Positioning is handled by your component - use `event.triggerElement` as a reference point if needed \ No newline at end of file diff --git a/packages/docs/examples/custom-tab-overflow-example.tsx b/packages/docs/examples/custom-tab-overflow-example.tsx new file mode 100644 index 0000000000..6ddc107c20 --- /dev/null +++ b/packages/docs/examples/custom-tab-overflow-example.tsx @@ -0,0 +1,114 @@ +import React from 'react'; +import { DockviewReact, ITabOverflowProps } from 'dockview'; +import 'dockview/dist/styles/dockview.css'; + +// Custom overflow component that renders a dropdown with tab titles +const CustomTabOverflow: React.FC = ({ event }) => { + const [isOpen, setIsOpen] = React.useState(false); + + // Show/hide the overflow UI based on whether there are overflow tabs + React.useEffect(() => { + if (!event.isVisible) { + setIsOpen(false); + } + }, [event.isVisible]); + + if (!event.isVisible) { + return null; // No overflow tabs, don't render anything + } + + return ( +
+ + + {isOpen && ( +
+ {event.tabs.map((tab) => ( +
{ + tab.panel.api.setActive(); + setIsOpen(false); + }} + style={{ + padding: '8px 12px', + cursor: 'pointer', + borderBottom: '1px solid #eee', + backgroundColor: tab.isActive ? '#e6f3ff' : 'transparent', + }} + > + {tab.title} + {tab.isActive && ( + + (active) + + )} +
+ ))} +
+ )} +
+ ); +}; + +// Simple panel component +const SimplePanel: React.FC = () => ( +
+

Panel Content

+

This is a sample panel. Try resizing the window to see the custom overflow behavior.

+
+); + +const CustomTabOverflowExample: React.FC = () => { + const [api, setApi] = React.useState(); + + const onReady = (event: any) => { + setApi(event.api); + + // Add multiple panels to trigger overflow + for (let i = 1; i <= 8; i++) { + event.api.addPanel({ + id: `panel${i}`, + title: `Panel ${i}`, + component: 'simple', + }); + } + }; + + return ( +
+ +
+ ); +}; + +export default CustomTabOverflowExample; \ No newline at end of file diff --git a/packages/docs/templates/dockview/custom-tab-overflow/angular/index.html b/packages/docs/templates/dockview/custom-tab-overflow/angular/index.html new file mode 100644 index 0000000000..8207415f3c --- /dev/null +++ b/packages/docs/templates/dockview/custom-tab-overflow/angular/index.html @@ -0,0 +1,139 @@ + + + + Dockview | custom tab overflow angular + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/docs/templates/dockview/custom-tab-overflow/angular/src/index.ts b/packages/docs/templates/dockview/custom-tab-overflow/angular/src/index.ts new file mode 100644 index 0000000000..4faa8c1574 --- /dev/null +++ b/packages/docs/templates/dockview/custom-tab-overflow/angular/src/index.ts @@ -0,0 +1,142 @@ +import 'zone.js'; +import '@angular/compiler'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; +import { Component, Type, NgModule, Input } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { DockviewAngularModule, DockviewReadyEvent } from 'dockview-angular'; +import { TabOverflowEvent } from 'dockview-core'; +import 'dockview-core/dist/styles/dockview.css'; + +// Default panel component +@Component({ + selector: 'default-panel', + template: ` +
+

{{ title }}

+

This is a sample panel. Try resizing the window to see the custom overflow behavior.

+
+ ` +}) +export class DefaultPanelComponent { + @Input() api: any; + @Input() params: any; + + get title() { + return this.params?.title || this.api?.title || this.api?.id || 'Panel'; + } +} + +// Custom trigger component (appears in the tab header) +@Component({ + selector: 'custom-trigger', + template: ` + + ` +}) +export class CustomTriggerComponent { + @Input() event!: TabOverflowEvent; +} + +// Custom content component (the overflow menu) +@Component({ + selector: 'custom-content', + template: ` +
+
+ 📋 Hidden Tabs ({{ event.tabs.length }}) +
+ +
+
+ + {{ tab.title }} + + + ✓ Active + +
+
+
+ ` +}) +export class CustomContentComponent { + @Input() event!: TabOverflowEvent; + + activateTab(tab: any) { + tab.panel.api.setActive(); + } +} + +// Main app component +@Component({ + selector: 'app-root', + template: ` + + + ` +}) +export class AppComponent { + components: Record>; + tabOverflowConfig = { + trigger: CustomTriggerComponent, + content: CustomContentComponent + }; + + constructor() { + this.components = { + default: DefaultPanelComponent, + }; + } + + onReady(event: DockviewReadyEvent) { + // Add multiple panels to trigger overflow + for (let i = 1; i <= 8; i++) { + event.api.addPanel({ + id: `panel_${i}`, + component: 'default', + title: `Panel ${i}`, + }); + } + } +} + +// App module +@NgModule({ + declarations: [AppComponent, DefaultPanelComponent, CustomTriggerComponent, CustomContentComponent], + imports: [BrowserModule, DockviewAngularModule], + providers: [], + bootstrap: [AppComponent] +}) +export class AppModule { } + +// Bootstrap the application +platformBrowserDynamic().bootstrapModule(AppModule).catch(err => console.error(err)); \ No newline at end of file diff --git a/packages/docs/templates/dockview/custom-tab-overflow/react/package.json b/packages/docs/templates/dockview/custom-tab-overflow/react/package.json new file mode 100644 index 0000000000..62da198535 --- /dev/null +++ b/packages/docs/templates/dockview/custom-tab-overflow/react/package.json @@ -0,0 +1,32 @@ +{ + "name": "dockview.custom-tab-overflow", + "description": "", + "keywords": [ + "dockview" + ], + "version": "1.0.0", + "main": "src/index.tsx", + "dependencies": { + "dockview": "*", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@types/react": "^18.0.28", + "@types/react-dom": "^18.0.11", + "typescript": "^4.9.5", + "react-scripts": "*" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test --env=jsdom", + "eject": "react-scripts eject" + }, + "browserslist": [ + ">0.2%", + "not dead", + "not ie <= 11", + "not op_mini all" + ] +} \ No newline at end of file diff --git a/packages/docs/templates/dockview/custom-tab-overflow/react/src/app.tsx b/packages/docs/templates/dockview/custom-tab-overflow/react/src/app.tsx new file mode 100644 index 0000000000..0dac113b39 --- /dev/null +++ b/packages/docs/templates/dockview/custom-tab-overflow/react/src/app.tsx @@ -0,0 +1,141 @@ +import React from 'react'; +import { DockviewReact, DockviewReadyEvent, IDockviewPanelProps, ITabOverflowProps, ITabOverflowTriggerProps } from 'dockview'; +import 'dockview/dist/styles/dockview.css'; + +// Simple panel component +const Panel: React.FC = (props) => { + const [title, setTitle] = React.useState(''); + + React.useEffect(() => { + const updateTitle = () => { + setTitle(props.api.title || props.api.id); + }; + + updateTitle(); + const disposable = props.api.onDidTitleChange(updateTitle); + + return () => { + disposable.dispose(); + }; + }, [props.api]); + + return ( +
+

{title}

+

This is a sample panel. Try resizing the window to see the custom overflow behavior.

+
+ ); +}; + +// Custom trigger component (appears in the tab header) +const CustomTrigger: React.FC = ({ event }) => { + if (!event.isVisible) return null; + + return ( + + ); +}; + +// Custom content component (the overflow menu) +const CustomContent: React.FC = ({ event }) => { + return ( +
+
+ 📋 Hidden Tabs ({event.tabs.length}) +
+ +
+ {event.tabs.map((tab, index) => ( +
tab.panel.api.setActive()} + style={{ + padding: '12px', + margin: '6px 0', + borderRadius: '8px', + cursor: 'pointer', + background: tab.isActive + ? 'rgba(255, 255, 255, 0.2)' + : 'rgba(255, 255, 255, 0.1)', + border: tab.isActive + ? '2px solid rgba(255, 255, 255, 0.6)' + : '2px solid transparent', + transition: 'all 0.2s ease', + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between' + }} + > + + {tab.title} + + {tab.isActive && ( + + ✓ Active + + )} +
+ ))} +
+
+ ); +}; + +export const App: React.FC = () => { + const onReady = (event: DockviewReadyEvent) => { + // Add multiple panels to trigger overflow + for (let i = 1; i <= 8; i++) { + event.api.addPanel({ + id: `panel_${i}`, + component: 'panel', + title: `Panel ${i}`, + }); + } + }; + + return ( + + ); +}; \ No newline at end of file diff --git a/packages/docs/templates/dockview/custom-tab-overflow/react/src/index.tsx b/packages/docs/templates/dockview/custom-tab-overflow/react/src/index.tsx new file mode 100644 index 0000000000..e82d44ba18 --- /dev/null +++ b/packages/docs/templates/dockview/custom-tab-overflow/react/src/index.tsx @@ -0,0 +1,6 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import { App } from './app'; + +const root = ReactDOM.createRoot(document.getElementById('root')!); +root.render(); \ No newline at end of file diff --git a/packages/docs/templates/dockview/custom-tab-overflow/react/tsconfig.json b/packages/docs/templates/dockview/custom-tab-overflow/react/tsconfig.json new file mode 100644 index 0000000000..bcdd588a4f --- /dev/null +++ b/packages/docs/templates/dockview/custom-tab-overflow/react/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "outDir": "build/dist", + "module": "esnext", + "target": "es5", + "lib": ["es6", "dom"], + "sourceMap": true, + "allowJs": true, + "jsx": "react-jsx", + "moduleResolution": "node", + "rootDir": "src", + "forceConsistentCasingInFileNames": true, + "noImplicitReturns": true, + "noImplicitThis": true, + "noImplicitAny": true, + "strictNullChecks": true + } +} \ No newline at end of file diff --git a/packages/docs/templates/dockview/custom-tab-overflow/typescript/index.html b/packages/docs/templates/dockview/custom-tab-overflow/typescript/index.html new file mode 100644 index 0000000000..890089d52a --- /dev/null +++ b/packages/docs/templates/dockview/custom-tab-overflow/typescript/index.html @@ -0,0 +1,129 @@ + + + + Dockview | custom tab overflow typescript + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/docs/templates/dockview/custom-tab-overflow/typescript/src/index.ts b/packages/docs/templates/dockview/custom-tab-overflow/typescript/src/index.ts new file mode 100644 index 0000000000..f34e4525e0 --- /dev/null +++ b/packages/docs/templates/dockview/custom-tab-overflow/typescript/src/index.ts @@ -0,0 +1,196 @@ +import 'dockview-core/dist/styles/dockview.css'; +import { + createDockview, + GroupPanelPartInitParameters, + IContentRenderer, + ITabOverflowRenderer, + ITabOverflowTriggerRenderer, + TabOverflowEvent, + themeAbyss, +} from 'dockview-core'; + +// Simple panel component +class Panel implements IContentRenderer { + private readonly _element: HTMLElement; + + get element(): HTMLElement { + return this._element; + } + + constructor() { + this._element = document.createElement('div'); + this._element.style.padding = '16px'; + this._element.style.height = '100%'; + } + + init(parameters: GroupPanelPartInitParameters): void { + const title = parameters.api.title || parameters.api.id; + this._element.innerHTML = ` +

${title}

+

This is a sample panel. Try resizing the window to see the custom overflow behavior.

+ `; + } + + dispose(): void { + // cleanup + } +} + +// Custom trigger renderer (appears in the tab header) +class CustomTriggerRenderer implements ITabOverflowTriggerRenderer { + private readonly _element: HTMLElement; + + get element(): HTMLElement { + return this._element; + } + + constructor() { + this._element = document.createElement('button'); + this._element.style.cssText = ` + background: linear-gradient(45deg, #ff6b6b, #feca57); + color: white; + border: none; + border-radius: 50%; + width: 28px; + height: 28px; + font-size: 12px; + font-weight: bold; + cursor: pointer; + box-shadow: 0 2px 4px rgba(0,0,0,0.2); + display: flex; + align-items: center; + justify-content: center; + `; + } + + update(event: TabOverflowEvent): void { + if (!event.isVisible) { + this._element.style.display = 'none'; + return; + } + this._element.style.display = 'flex'; + this._element.textContent = event.tabs.length.toString(); + } + + dispose(): void { + // cleanup + } +} + +// Custom content renderer (the overflow menu) +class CustomContentRenderer implements ITabOverflowRenderer { + private readonly _element: HTMLElement; + + get element(): HTMLElement { + return this._element; + } + + constructor() { + this._element = document.createElement('div'); + this._element.style.cssText = ` + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border-radius: 12px; + padding: 16px; + min-width: 280px; + color: white; + box-shadow: 0 10px 25px rgba(0,0,0,0.2); + `; + } + + update(event: TabOverflowEvent): void { + this._element.innerHTML = ''; + + // Title + const title = document.createElement('div'); + title.textContent = `📋 Hidden Tabs (${event.tabs.length})`; + title.style.cssText = ` + font-size: 16px; + font-weight: bold; + margin-bottom: 12px; + text-align: center; + `; + this._element.appendChild(title); + + // Scrollable container + const scrollContainer = document.createElement('div'); + scrollContainer.style.cssText = ` + max-height: 300px; + overflow-y: auto; + `; + + // Add tab items + event.tabs.forEach((tab) => { + const tabElement = document.createElement('div'); + tabElement.style.cssText = ` + padding: 12px; + margin: 6px 0; + border-radius: 8px; + cursor: pointer; + background: ${tab.isActive ? 'rgba(255, 255, 255, 0.2)' : 'rgba(255, 255, 255, 0.1)'}; + border: ${tab.isActive ? '2px solid rgba(255, 255, 255, 0.6)' : '2px solid transparent'}; + transition: all 0.2s ease; + display: flex; + align-items: center; + justify-content: space-between; + `; + + const titleSpan = document.createElement('span'); + titleSpan.textContent = tab.title; + titleSpan.style.fontWeight = tab.isActive ? 'bold' : 'normal'; + + tabElement.appendChild(titleSpan); + + if (tab.isActive) { + const activeSpan = document.createElement('span'); + activeSpan.textContent = '✓ Active'; + activeSpan.style.cssText = ` + font-size: 12px; + background: rgba(255,255,255,0.3); + padding: 2px 6px; + border-radius: 4px; + `; + tabElement.appendChild(activeSpan); + } + + tabElement.addEventListener('click', () => { + tab.panel.api.setActive(); + }); + + scrollContainer.appendChild(tabElement); + }); + + this._element.appendChild(scrollContainer); + } + + dispose(): void { + // cleanup + } +} + +// Create dockview instance +const api = createDockview(document.getElementById('app')!, { + theme: themeAbyss, + createComponent: (options) => { + switch (options.name) { + case 'default': + return new Panel(); + default: + throw new Error(`Unknown component: ${options.name}`); + } + }, + createTabOverflowComponent: () => { + return { + trigger: new CustomTriggerRenderer(), + content: new CustomContentRenderer() + }; + }, +}); + +// Add multiple panels to trigger overflow +for (let i = 1; i <= 8; i++) { + api.addPanel({ + id: `panel_${i}`, + component: 'default', + title: `Panel ${i}`, + }); +} \ No newline at end of file diff --git a/packages/docs/templates/dockview/custom-tab-overflow/vue/index.html b/packages/docs/templates/dockview/custom-tab-overflow/vue/index.html new file mode 100644 index 0000000000..f93e644cd7 --- /dev/null +++ b/packages/docs/templates/dockview/custom-tab-overflow/vue/index.html @@ -0,0 +1,135 @@ + + + + Dockview | custom tab overflow vue + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/docs/templates/dockview/custom-tab-overflow/vue/src/index.ts b/packages/docs/templates/dockview/custom-tab-overflow/vue/src/index.ts new file mode 100644 index 0000000000..e575e02e6d --- /dev/null +++ b/packages/docs/templates/dockview/custom-tab-overflow/vue/src/index.ts @@ -0,0 +1,147 @@ +import 'dockview-vue/dist/styles/dockview.css'; +import { PropType, createApp, defineComponent } from 'vue'; +import { + DockviewVue, + DockviewReadyEvent, + IDockviewPanelProps, +} from 'dockview-vue'; + +// Simple panel component +const Panel = defineComponent({ + name: 'Panel', + props: { + params: { + type: Object as PropType, + required: true, + }, + }, + data() { + return { + title: '', + }; + }, + mounted() { + const disposable = this.params.api.onDidTitleChange(() => { + this.title = this.params.api.title; + }); + this.title = this.params.api.title; + + return () => { + disposable.dispose(); + }; + }, + template: ` +
+

{{ title }}

+

This is a sample panel. Try resizing the window to see the custom overflow behavior.

+
`, +}); + +// Custom trigger component (appears in the tab header) +const CustomTrigger = defineComponent({ + name: 'CustomTrigger', + props: { + event: { + type: Object, + required: true, + }, + }, + template: ` + `, +}); + +// Custom content component (the overflow menu) +const CustomContent = defineComponent({ + name: 'CustomContent', + props: { + event: { + type: Object, + required: true, + }, + }, + methods: { + activateTab(tab: any) { + tab.panel.api.setActive(); + }, + }, + template: ` +
+
+ 📋 Hidden Tabs ({{ event.tabs.length }}) +
+ +
+
+ + {{ tab.title }} + + + ✓ Active + +
+
+
`, +}); + +const App = defineComponent({ + name: 'App', + components: { + 'dockview-vue': DockviewVue, + panel: Panel, + 'custom-trigger': CustomTrigger, + 'custom-content': CustomContent, + }, + methods: { + onReady(event: DockviewReadyEvent) { + // Add multiple panels to trigger overflow + for (let i = 1; i <= 8; i++) { + event.api.addPanel({ + id: `panel_${i}`, + component: 'panel', + title: `Panel ${i}`, + }); + } + }, + }, + template: ` + + `, +}); + +const app = createApp(App); +app.config.errorHandler = (err) => { + console.log(err); +}; +app.mount(document.getElementById('app')!); \ No newline at end of file