From 4af5119638fdfe77e4babbed273a080420521e8c Mon Sep 17 00:00:00 2001 From: mathuo <6710312+mathuo@users.noreply.github.com> Date: Sun, 10 Nov 2024 21:47:51 +0000 Subject: [PATCH] experiments --- .../src/dockview/components/popupService.ts | 82 +++++++++ .../components/titlebar/tabPanel.scss | 43 +++++ .../components/titlebar/tabsContainer.scss | 61 +------ .../components/titlebar/tabsContainer.ts | 98 ++++------ .../dockview/components/titlebar/tabsPanel.ts | 171 ++++++++++++++++++ .../src/dockview/dockviewComponent.scss | 10 +- .../src/dockview/dockviewComponent.ts | 5 + packages/dockview-core/src/theme.scss | 4 +- 8 files changed, 351 insertions(+), 123 deletions(-) create mode 100644 packages/dockview-core/src/dockview/components/popupService.ts create mode 100644 packages/dockview-core/src/dockview/components/titlebar/tabPanel.scss create mode 100644 packages/dockview-core/src/dockview/components/titlebar/tabsPanel.ts diff --git a/packages/dockview-core/src/dockview/components/popupService.ts b/packages/dockview-core/src/dockview/components/popupService.ts new file mode 100644 index 000000000..1fb5a0996 --- /dev/null +++ b/packages/dockview-core/src/dockview/components/popupService.ts @@ -0,0 +1,82 @@ +import { addDisposableWindowListener } from '../../events'; +import { + CompositeDisposable, + Disposable, + MutableDisposable, +} from '../../lifecycle'; + +export class PopupService extends CompositeDisposable { + private readonly _element: HTMLElement; + private _active: HTMLElement | null = null; + private _activeDisposable = new MutableDisposable(); + + constructor(private readonly root: HTMLElement) { + super(); + + this._element = document.createElement('div'); + this._element.className = 'dv-popover-anchor'; + this._element.style.position = 'relative'; + + this.root.prepend(this._element); + + this.addDisposables( + Disposable.from(() => { + this.close(); + }), + this._activeDisposable + ); + } + + openPopover( + element: HTMLElement, + position: { x: number; y: number } + ): void { + this.close(); + + const wrapper = document.createElement('div'); + wrapper.style.position = 'absolute'; + wrapper.style.zIndex = '99'; + wrapper.appendChild(element); + + const anchorBox = this._element.getBoundingClientRect(); + const offsetX = anchorBox.left; + const offsetY = anchorBox.top; + + wrapper.style.top = `${position.y - offsetY}px`; + wrapper.style.left = `${position.x - offsetX}px`; + + this._element.appendChild(wrapper); + + this._active = wrapper; + + this._activeDisposable.value = new CompositeDisposable( + addDisposableWindowListener(window, 'pointerdown', (event) => { + const target = event.target; + + if (!(target instanceof HTMLElement)) { + return; + } + + let el: HTMLElement | null = target; + + while (el && el !== wrapper) { + el = el?.parentElement ?? null; + } + + if (el) { + return; // clicked within popover + } + + this.close(); + }) + ); + } + + close(): void { + if (this._active) { + this._active.remove(); + this._activeDisposable.dispose(); + this._active = null; + } + } +} diff --git a/packages/dockview-core/src/dockview/components/titlebar/tabPanel.scss b/packages/dockview-core/src/dockview/components/titlebar/tabPanel.scss new file mode 100644 index 000000000..a7faa9658 --- /dev/null +++ b/packages/dockview-core/src/dockview/components/titlebar/tabPanel.scss @@ -0,0 +1,43 @@ +.dv-tabs-container { + display: flex; + overflow-x: overlay; + overflow-y: hidden; + height: 100%; + + scrollbar-width: thin; // firefox + + &::-webkit-scrollbar { + height: 3px; + } + + /* Track */ + &::-webkit-scrollbar-track { + background: transparent; + } + + /* Handle */ + &::-webkit-scrollbar-thumb { + background: var(--dv-tabs-container-scrollbar-color); + } + + .dv-tab { + -webkit-user-drag: element; + outline: none; + min-width: 75px; + cursor: pointer; + position: relative; + box-sizing: border-box; + + &:not(:first-child)::before { + content: ' '; + position: absolute; + top: 0; + left: 0; + z-index: 5; + pointer-events: none; + background-color: var(--dv-tab-divider-color); + width: 1px; + height: 100%; + } + } +} diff --git a/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.scss b/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.scss index fef520e03..14815f8bc 100644 --- a/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.scss +++ b/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.scss @@ -7,17 +7,17 @@ font-size: var(--dv-tabs-and-actions-container-font-size); &.dv-single-tab.dv-full-width-single-tab { - .dv-tabs-container { - flex-grow: 1; - - .dv-tab { + .dv-tabs-container { flex-grow: 1; - } - } - .dv-void-container { - flex-grow: 0; - } + .dv-tab { + flex-grow: 1; + } + } + + .dv-void-container { + flex-grow: 0; + } } .dv-void-container { @@ -25,47 +25,4 @@ flex-grow: 1; cursor: grab; } - - .dv-tabs-container { - display: flex; - overflow-x: overlay; - overflow-y: hidden; - - scrollbar-width: thin; // firefox - - &::-webkit-scrollbar { - height: 3px; - } - - /* Track */ - &::-webkit-scrollbar-track { - background: transparent; - } - - /* Handle */ - &::-webkit-scrollbar-thumb { - background: var(--dv-tabs-container-scrollbar-color); - } - - .dv-tab { - -webkit-user-drag: element; - outline: none; - min-width: 75px; - cursor: pointer; - position: relative; - box-sizing: border-box; - - &:not(:first-child)::before { - content: ' '; - position: absolute; - top: 0; - left: 0; - z-index: 5; - pointer-events: none; - background-color: var(--dv-tab-divider-color); - width: 1px; - height: 100%; - } - } - } } diff --git a/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts b/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts index c30d1175e..7e0ebbf98 100644 --- a/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts +++ b/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts @@ -12,6 +12,7 @@ import { DockviewPanel, IDockviewPanel } from '../../dockviewPanel'; import { DockviewComponent } from '../../dockviewComponent'; import { WillShowOverlayLocationEvent } from '../../dockviewGroupPanelModel'; import { getPanelData } from '../../../dnd/dataTransfer'; +import { TabsPanel } from './tabsPanel'; export interface TabDropIndexEvent { readonly event: DragEvent; @@ -56,14 +57,12 @@ export class TabsContainer implements ITabsContainer { private readonly _element: HTMLElement; - private readonly tabContainer: HTMLElement; + private readonly tabContainer: TabsPanel; 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; @@ -86,11 +85,11 @@ export class TabsContainer this._onWillShowOverlay.event; get panels(): string[] { - return this.tabs.map((_) => _.value.panel.id); + return this.tabContainer.panels; } get size(): number { - return this.tabs.length; + return this.tabContainer.length; } get hidden(): boolean { @@ -159,14 +158,11 @@ export class TabsContainer } public isActive(tab: Tab): boolean { - return ( - this.selectedIndex > -1 && - this.tabs[this.selectedIndex].value === tab - ); + return this.tabContainer.isActive(tab); } public indexOf(id: string): number { - return this.tabs.findIndex((tab) => tab.value.panel.id === id); + return this.tabContainer.indexOf(id); } constructor( @@ -193,18 +189,18 @@ export class TabsContainer this.preActionsContainer = document.createElement('div'); this.preActionsContainer.className = 'dv-pre-actions-container'; - this.tabContainer = document.createElement('div'); - this.tabContainer.className = 'dv-tabs-container'; + this.tabContainer = new TabsPanel(accessor); this.voidContainer = new VoidContainer(this.accessor, this.group); this._element.appendChild(this.preActionsContainer); - this._element.appendChild(this.tabContainer); + this._element.appendChild(this.tabContainer.element); this._element.appendChild(this.leftActionsContainer); this._element.appendChild(this.voidContainer.element); this._element.appendChild(this.rightActionsContainer); this.addDisposables( + this.tabContainer, this.accessor.onDidAddPanel((e) => { if (e.api.group === this.group) { toggleClass( @@ -237,7 +233,7 @@ export class TabsContainer this.voidContainer.onDrop((event) => { this._onDrop.fire({ event: event.nativeEvent, - index: this.tabs.length, + index: this.tabContainer.length, }); }), this.voidContainer.onWillShowOverlay((event) => { @@ -278,17 +274,21 @@ export class TabsContainer } } ), - addDisposableListener(this.tabContainer, 'pointerdown', (event) => { - if (event.defaultPrevented) { - return; - } + addDisposableListener( + this.tabContainer.element, + 'pointerdown', + (event) => { + if (event.defaultPrevented) { + return; + } - const isLeftClick = event.button === 0; + const isLeftClick = event.button === 0; - if (isLeftClick) { - this.accessor.doSetGroupActive(this.group); + if (isLeftClick) { + this.accessor.doSetGroupActive(this.group); + } } - }) + ) ); } @@ -298,52 +298,27 @@ export class TabsContainer private addTab( tab: IValueDisposable, - index: number = this.tabs.length + index: number = this.tabContainer.length ): void { - if (index < 0 || index > this.tabs.length) { - throw new Error('invalid location'); - } - - this.tabContainer.insertBefore( - tab.value.element, - this.tabContainer.children[index] - ); - - this.tabs = [ - ...this.tabs.slice(0, index), - tab, - ...this.tabs.slice(index), - ]; - - if (this.selectedIndex < 0) { - this.selectedIndex = index; - } + this.tabContainer.addTab(tab, index); } public delete(id: string): void { - const index = this.tabs.findIndex((tab) => tab.value.panel.id === id); - - const tabToRemove = this.tabs.splice(index, 1)[0]; - - const { value, disposable } = tabToRemove; - - disposable.dispose(); - value.dispose(); - value.element.remove(); + this.tabContainer.delete(id); } public setActivePanel(panel: IDockviewPanel): void { - this.tabs.forEach((tab) => { - const isActivePanel = panel.id === tab.value.panel.id; - tab.value.setActive(isActivePanel); + this.tabContainer.tabs.forEach((tab) => { + const isActivePanel = panel.id === tab.panel.id; + tab.setActive(isActivePanel); }); } public openPanel( panel: IDockviewPanel, - index: number = this.tabs.length + index: number = this.tabContainer.length ): void { - if (this.tabs.find((tab) => tab.value.panel.id === panel.id)) { + if (this.tabContainer.tabs.find((tab) => tab.panel.id === panel.id)) { return; } const tab = new Tab(panel, this.accessor, this.group); @@ -395,7 +370,7 @@ export class TabsContainer tab.onDrop((event) => { this._onDrop.fire({ event: event.nativeEvent, - index: this.tabs.findIndex((x) => x.value === tab), + index: this.tabContainer.tabs.findIndex((x) => x === tab), }); }), tab.onWillShowOverlay((event) => { @@ -419,15 +394,4 @@ export class TabsContainer public closePanel(panel: IDockviewPanel): void { this.delete(panel.id); } - - public dispose(): void { - super.dispose(); - - for (const { value, disposable } of this.tabs) { - disposable.dispose(); - value.dispose(); - } - - this.tabs = []; - } } diff --git a/packages/dockview-core/src/dockview/components/titlebar/tabsPanel.ts b/packages/dockview-core/src/dockview/components/titlebar/tabsPanel.ts new file mode 100644 index 000000000..8154e95af --- /dev/null +++ b/packages/dockview-core/src/dockview/components/titlebar/tabsPanel.ts @@ -0,0 +1,171 @@ +import { OverflowObserver } from '../../../dom'; +import { + addDisposableListener, + addDisposableWindowListener, +} from '../../../events'; +import { + CompositeDisposable, + Disposable, + IValueDisposable, + MutableDisposable, +} from '../../../lifecycle'; +import { DockviewComponent } from '../../dockviewComponent'; +import { Tab } from '../tab/tab'; + +class Dropdown { + private readonly _element: HTMLElement; + + get element(): HTMLElement { + return this._element; + } + + constructor() { + this._element = document.createElement('div'); + this._element.style.height = '200px'; + this._element.style.width = '200px'; + this._element.style.position = 'absolute'; + this._element.style.top = '0px'; + this._element.style.right = '0px'; + this._element.style.backgroundColor = 'yellow'; + } +} + +export class TabsPanel extends CompositeDisposable { + private readonly _element: HTMLElement; + private readonly _tabsList: HTMLElement; + + private _tabs: IValueDisposable[] = []; + private _selectedIndex = -1; + private _hasOverflow = false; + private _dropdownAnchor: HTMLElement | null = null; + + get element(): HTMLElement { + return this._element; + } + + get panels(): string[] { + return this._tabs.map((_) => _.value.panel.id); + } + + get length(): number { + return this._tabs.length; + } + + get tabs(): Tab[] { + return this._tabs.map((tab) => tab.value); + } + + constructor(private readonly accessor: DockviewComponent) { + super(); + this._element = document.createElement('div'); + this._element.className = 'dv-tabs-panel'; + this._element.style.display = 'flex'; + this._element.style.overflow = 'auto'; + this._tabsList = document.createElement('div'); + this._tabsList.className = 'dv-tabs-container'; + this._element.appendChild(this._tabsList); + + const overflowObserver = new OverflowObserver(this._tabsList); + + this.addDisposables( + overflowObserver, + overflowObserver.onDidChange((event) => { + const hasOverflow = event.hasScrollX || event.hasScrollY; + if (this._hasOverflow !== hasOverflow) { + this.toggleDropdown(hasOverflow); + } + }), + Disposable.from(() => { + for (const { value, disposable } of this._tabs) { + disposable.dispose(); + value.dispose(); + } + + this._tabs = []; + }) + ); + } + + toggleDropdown(show: boolean): void { + this._hasOverflow = show; + if (this._dropdownAnchor) { + this._dropdownAnchor.remove(); + this._dropdownAnchor = null; + } + + if (!show) { + return; + } + + this._dropdownAnchor = document.createElement('div'); + this._dropdownAnchor.style.width = '10px'; + this._dropdownAnchor.style.height = '100%'; + this._dropdownAnchor.style.flexShrink = '0'; + this._dropdownAnchor.style.backgroundColor = 'red'; + + this.element.appendChild(this._dropdownAnchor); + + addDisposableListener(this._dropdownAnchor, 'click', (event) => { + const el = document.createElement('div'); + el.style.width = '200px'; + el.style.maxHeight = '600px'; + el.style.overflow = 'auto'; + el.style.backgroundColor = 'lightgreen'; + + this.tabs.map((tab) => { + const tabEl = document.createElement('div'); + tabEl.textContent = tab.panel.api.title ?? '-'; + el.appendChild(tabEl); + }); + + this.accessor.popupService.openPopover(el, { + x: event.clientX, + y: event.clientY, + }); + }); + } + + addTab(tab: IValueDisposable, index: number = this.length): void { + if (index < 0 || index > this.length) { + throw new Error('invalid location'); + } + + this._tabsList.insertBefore( + tab.value.element, + this._tabsList.children[index] + ); + + this._tabs = [ + ...this._tabs.slice(0, index), + tab, + ...this._tabs.slice(index), + ]; + + if (this._selectedIndex < 0) { + this._selectedIndex = index; + } + } + + delete(id: string): void { + const index = this.tabs.findIndex((tab) => tab.panel.id === id); + + const tabToRemove = this._tabs.splice(index, 1)[0]; + + const { value, disposable } = tabToRemove; + + disposable.dispose(); + value.dispose(); + value.element.remove(); + } + + isActive(tab: Tab): boolean { + return ( + this._selectedIndex > -1 && + this._tabs[this._selectedIndex].value === tab + ); + } + + indexOf(id: string): number { + return this._tabs.findIndex((tab) => tab.value.panel.id === id); + } +} diff --git a/packages/dockview-core/src/dockview/dockviewComponent.scss b/packages/dockview-core/src/dockview/dockviewComponent.scss index 386bf2a82..b08c0ada0 100644 --- a/packages/dockview-core/src/dockview/dockviewComponent.scss +++ b/packages/dockview-core/src/dockview/dockviewComponent.scss @@ -18,7 +18,10 @@ .dv-groupview { &.dv-active-group { - > .dv-tabs-and-actions-container > .dv-tabs-container > .dv-tab { + > .dv-tabs-and-actions-container + > .dv-tabs-panel + > .dv-tabs-container + > .dv-tab { &.dv-active-tab { background-color: var( --dv-activegroup-visiblepanel-tab-background-color @@ -34,7 +37,10 @@ } } &.dv-inactive-group { - > .dv-tabs-and-actions-container > .dv-tabs-container > .dv-tab { + > .dv-tabs-and-actions-container + > .dv-tabs-panel + > .dv-tabs-container + > .dv-tab { &.dv-active-tab { background-color: var( --dv-inactivegroup-visiblepanel-tab-background-color diff --git a/packages/dockview-core/src/dockview/dockviewComponent.ts b/packages/dockview-core/src/dockview/dockviewComponent.ts index 5a20f5b84..5094f5301 100644 --- a/packages/dockview-core/src/dockview/dockviewComponent.ts +++ b/packages/dockview-core/src/dockview/dockviewComponent.ts @@ -73,6 +73,7 @@ import { OverlayRenderContainer, } from '../overlay/overlayRenderContainer'; import { PopoutWindow } from '../popoutWindow'; +import { PopupService } from './components/popupService'; const DEFAULT_ROOT_OVERLAY_MODEL: DroptargetOverlayModel = { activationSize: { type: 'pixels', value: 10 }, @@ -233,6 +234,8 @@ export class DockviewComponent readonly overlayRenderContainer: OverlayRenderContainer; + readonly popupService: PopupService; + private readonly _onWillDragPanel = new Emitter(); readonly onWillDragPanel: Event = this._onWillDragPanel.event; @@ -358,6 +361,8 @@ export class DockviewComponent this ); + this.popupService = new PopupService(this.element); + toggleClass(this.gridview.element, 'dv-dockview', true); toggleClass(this.element, 'dv-debug', !!options.debug); diff --git a/packages/dockview-core/src/theme.scss b/packages/dockview-core/src/theme.scss index 85aa66280..519d416b1 100644 --- a/packages/dockview-core/src/theme.scss +++ b/packages/dockview-core/src/theme.scss @@ -179,7 +179,7 @@ .dv-groupview { &.dv-active-group { > .dv-tabs-and-actions-container { - > .dv-tabs-container { + > .dv-tabs-panel > .dv-tabs-container { > .dv-tab.dv-active-tab { position: relative; @@ -199,7 +199,7 @@ } &.dv-inactive-group { > .dv-tabs-and-actions-container { - > .dv-tabs-container { + > .dv-tabs-panel > .dv-tabs-container { > .dv-tab.dv-active-tab { position: relative;