From 04e4ea1a70cc4c3d61c5bdaaf1fef7cde67f5ffa Mon Sep 17 00:00:00 2001 From: mathuo <6710312+mathuo@users.noreply.github.com> Date: Mon, 6 Jan 2025 20:18:18 +0000 Subject: [PATCH 01/40] tabs overflow menu --- .../src/dockview/components/popupService.ts | 82 ++++ .../dockview/components/titlebar/tabs.scss | 42 ++ .../src/dockview/components/titlebar/tabs.tsx | 263 ++++++++++++ .../components/titlebar/tabsContainer.scss | 61 +-- .../components/titlebar/tabsContainer.ts | 377 ++++++------------ .../src/dockview/dockviewComponent.scss | 10 +- .../src/dockview/dockviewComponent.ts | 4 + .../src/dockview/dockviewPanelModel.ts | 6 + 8 files changed, 532 insertions(+), 313 deletions(-) create mode 100644 packages/dockview-core/src/dockview/components/popupService.ts create mode 100644 packages/dockview-core/src/dockview/components/titlebar/tabs.scss create mode 100644 packages/dockview-core/src/dockview/components/titlebar/tabs.tsx 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/tabs.scss b/packages/dockview-core/src/dockview/components/titlebar/tabs.scss new file mode 100644 index 000000000..01b715404 --- /dev/null +++ b/packages/dockview-core/src/dockview/components/titlebar/tabs.scss @@ -0,0 +1,42 @@ +.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/tabs.tsx b/packages/dockview-core/src/dockview/components/titlebar/tabs.tsx new file mode 100644 index 000000000..0a86dd54b --- /dev/null +++ b/packages/dockview-core/src/dockview/components/titlebar/tabs.tsx @@ -0,0 +1,263 @@ +import { getPanelData } from '../../../dnd/dataTransfer'; +import { OverflowObserver } from '../../../dom'; +import { addDisposableListener, Emitter, Event } from '../../../events'; +import { + CompositeDisposable, + Disposable, + IValueDisposable, +} from '../../../lifecycle'; +import { DockviewComponent } from '../../dockviewComponent'; +import { DockviewGroupPanel } from '../../dockviewGroupPanel'; +import { WillShowOverlayLocationEvent } from '../../dockviewGroupPanelModel'; +import { DockviewPanel, IDockviewPanel } from '../../dockviewPanel'; +import { Tab } from '../tab/tab'; +import { TabDragEvent, TabDropIndexEvent } from './tabsContainer'; + +export class Tabs extends CompositeDisposable { + private readonly _element: HTMLElement; + private readonly _tabsList: HTMLElement; + + private tabs: IValueDisposable[] = []; + private selectedIndex = -1; + private _hasOverflow = false; + private _dropdownAnchor: HTMLElement | null = null; + + private readonly _onTabDragStart = new Emitter(); + readonly onTabDragStart: Event = this._onTabDragStart.event; + + private readonly _onDrop = new Emitter(); + readonly onDrop: Event = this._onDrop.event; + + private readonly _onWillShowOverlay = + new Emitter(); + readonly onWillShowOverlay: Event = + this._onWillShowOverlay.event; + + get element(): HTMLElement { + return this._element; + } + + get panels(): string[] { + return this.tabs.map((_) => _.value.panel.id); + } + + get size(): number { + return this.tabs.length; + } + + constructor( + private readonly group: DockviewGroupPanel, + 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 observer = new OverflowObserver(this._tabsList); + + this.addDisposables( + observer, + observer.onDidChange((event) => { + const hasOverflow = event.hasScrollX || event.hasScrollY; + if (this._hasOverflow !== hasOverflow) { + this.toggleDropdown(hasOverflow); + } + }), + addDisposableListener(this.element, 'pointerdown', (event) => { + if (event.defaultPrevented) { + return; + } + + const isLeftClick = event.button === 0; + + if (isLeftClick) { + this.accessor.doSetGroupActive(this.group); + } + }), + Disposable.from(() => { + for (const { value, disposable } of this.tabs) { + disposable.dispose(); + value.dispose(); + } + + this.tabs = []; + }) + ); + } + + indexOf(id: string): number { + return this.tabs.findIndex((tab) => tab.value.panel.id === id); + } + + isActive(tab: Tab): boolean { + return ( + this.selectedIndex > -1 && + this.tabs[this.selectedIndex].value === tab + ); + } + + setActivePanel(panel: IDockviewPanel): void { + this.tabs.forEach((tab) => { + const isActivePanel = panel.id === tab.value.panel.id; + tab.value.setActive(isActivePanel); + }); + } + + openPanel(panel: IDockviewPanel, index: number = this.tabs.length): void { + if (this.tabs.find((tab) => tab.value.panel.id === panel.id)) { + return; + } + const tab = new Tab(panel, this.accessor, this.group); + tab.setContent(panel.view.tab); + + const disposable = new CompositeDisposable( + tab.onDragStart((event) => { + this._onTabDragStart.fire({ nativeEvent: event, panel }); + }), + tab.onChanged((event) => { + const isFloatingGroupsEnabled = + !this.accessor.options.disableFloatingGroups; + + const isFloatingWithOnePanel = + this.group.api.location.type === 'floating' && + this.size === 1; + + if ( + isFloatingGroupsEnabled && + !isFloatingWithOnePanel && + event.shiftKey + ) { + event.preventDefault(); + + const panel = this.accessor.getGroupPanel(tab.panel.id); + + const { top, left } = tab.element.getBoundingClientRect(); + const { top: rootTop, left: rootLeft } = + this.accessor.element.getBoundingClientRect(); + + this.accessor.addFloatingGroup(panel as DockviewPanel, { + x: left - rootLeft, + y: top - rootTop, + inDragMode: true, + }); + return; + } + + const isLeftClick = event.button === 0; + + if (!isLeftClick || event.defaultPrevented) { + return; + } + + if (this.group.activePanel !== panel) { + this.group.model.openPanel(panel); + } + }), + tab.onDrop((event) => { + this._onDrop.fire({ + event: event.nativeEvent, + index: this.tabs.findIndex((x) => x.value === tab), + }); + }), + tab.onWillShowOverlay((event) => { + this._onWillShowOverlay.fire( + new WillShowOverlayLocationEvent(event, { + kind: 'tab', + panel: this.group.activePanel, + api: this.accessor.api, + group: this.group, + getData: getPanelData, + }) + ); + }) + ); + + const value: IValueDisposable = { value: tab, disposable }; + + this.addTab(value, index); + } + + delete(id: string): void { + const index = this.indexOf(id); + const tabToRemove = this.tabs.splice(index, 1)[0]; + + const { value, disposable } = tabToRemove; + + disposable.dispose(); + value.dispose(); + value.element.remove(); + } + + private addTab( + tab: IValueDisposable, + index: number = this.tabs.length + ): void { + if (index < 0 || index > this.tabs.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; + } + } + + private 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 tab2 = new Tab( + tab.value.panel, + this.accessor, + this.group + ); + tab2.setContent(tab.value.panel.view.newTab); + el.appendChild(tab2.element); + }); + + this.accessor.popupService.openPopover(el, { + x: event.clientX, + y: event.clientY, + }); + }); + } +} 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 d3bd0568b..41c1e5be8 100644 --- a/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts +++ b/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts @@ -1,17 +1,14 @@ -import { - IDisposable, - CompositeDisposable, - IValueDisposable, -} from '../../../lifecycle'; +import { IDisposable, CompositeDisposable } from '../../../lifecycle'; import { addDisposableListener, Emitter, Event } from '../../../events'; import { Tab } from '../tab/tab'; import { DockviewGroupPanel } from '../../dockviewGroupPanel'; import { VoidContainer } from './voidContainer'; import { toggleClass } from '../../../dom'; -import { DockviewPanel, IDockviewPanel } from '../../dockviewPanel'; +import { IDockviewPanel } from '../../dockviewPanel'; import { DockviewComponent } from '../../dockviewComponent'; import { WillShowOverlayLocationEvent } from '../../dockviewGroupPanelModel'; import { getPanelData } from '../../../dnd/dataTransfer'; +import { Tabs } from './tabs'; export interface TabDropIndexEvent { readonly event: DragEvent; @@ -56,14 +53,12 @@ export class TabsContainer implements ITabsContainer { private readonly _element: HTMLElement; - private readonly tabContainer: HTMLElement; + private readonly tabs: Tabs; 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; @@ -73,8 +68,9 @@ export class TabsContainer private readonly _onDrop = new Emitter(); readonly onDrop: Event = this._onDrop.event; - private readonly _onTabDragStart = new Emitter(); - readonly onTabDragStart: Event = this._onTabDragStart.event; + get onTabDragStart(): Event { + return this.tabs.onTabDragStart; + } private readonly _onGroupDragStart = new Emitter(); readonly onGroupDragStart: Event = @@ -86,11 +82,11 @@ export class TabsContainer this._onWillShowOverlay.event; get panels(): string[] { - return this.tabs.map((_) => _.value.panel.id); + return this.tabs.panels; } get size(): number { - return this.tabs.length; + return this.tabs.size; } get hidden(): boolean { @@ -102,6 +98,102 @@ export class TabsContainer this.element.style.display = value ? 'none' : ''; } + get element(): HTMLElement { + return this._element; + } + + constructor( + private readonly accessor: DockviewComponent, + private readonly group: DockviewGroupPanel + ) { + super(); + + this._element = document.createElement('div'); + this._element.className = 'dv-tabs-and-actions-container'; + + toggleClass( + this._element, + 'dv-full-width-single-tab', + this.accessor.options.singleTabMode === 'fullwidth' + ); + + this.rightActionsContainer = document.createElement('div'); + this.rightActionsContainer.className = 'dv-right-actions-container'; + + this.leftActionsContainer = document.createElement('div'); + this.leftActionsContainer.className = 'dv-left-actions-container'; + + this.preActionsContainer = document.createElement('div'); + this.preActionsContainer.className = 'dv-pre-actions-container'; + + this.tabs = new Tabs(group, accessor); + + this.voidContainer = new VoidContainer(this.accessor, this.group); + + 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); + + this.addDisposables( + this._onWillShowOverlay, + this._onDrop, + this._onGroupDragStart, + this.voidContainer, + this.voidContainer.onDragStart((event) => { + this._onGroupDragStart.fire({ + nativeEvent: event, + group: this.group, + }); + }), + this.voidContainer.onDrop((event) => { + this._onDrop.fire({ + event: event.nativeEvent, + index: this.tabs.size, + }); + }), + this.voidContainer.onWillShowOverlay((event) => { + this._onWillShowOverlay.fire( + new WillShowOverlayLocationEvent(event, { + kind: 'header_space', + panel: this.group.activePanel, + api: this.accessor.api, + group: this.group, + getData: getPanelData, + }) + ); + }), + addDisposableListener( + this.voidContainer.element, + 'pointerdown', + (event) => { + const isFloatingGroupsEnabled = + !this.accessor.options.disableFloatingGroups; + + if ( + isFloatingGroupsEnabled && + event.shiftKey && + this.group.api.location.type !== 'floating' + ) { + event.preventDefault(); + + const { top, left } = + this.element.getBoundingClientRect(); + const { top: rootTop, left: rootLeft } = + this.accessor.element.getBoundingClientRect(); + + this.accessor.addFloatingGroup(this.group, { + x: left - rootLeft + 20, + y: top - rootTop + 20, + inDragMode: true, + }); + } + } + ) + ); + } + show(): void { if (!this.hidden) { this.element.style.display = ''; @@ -154,269 +246,36 @@ export class TabsContainer } } - get element(): HTMLElement { - return this._element; + isActive(tab: Tab): boolean { + return this.tabs.isActive(tab); } - public isActive(tab: Tab): boolean { - return ( - this.selectedIndex > -1 && - this.tabs[this.selectedIndex].value === tab - ); + indexOf(id: string): number { + return this.tabs.indexOf(id); } - public indexOf(id: string): number { - return this.tabs.findIndex((tab) => tab.value.panel.id === id); - } - - constructor( - private readonly accessor: DockviewComponent, - private readonly group: DockviewGroupPanel - ) { - super(); - - this._element = document.createElement('div'); - this._element.className = 'dv-tabs-and-actions-container'; - - toggleClass( - this._element, - 'dv-full-width-single-tab', - this.accessor.options.singleTabMode === 'fullwidth' - ); - - this.rightActionsContainer = document.createElement('div'); - this.rightActionsContainer.className = 'dv-right-actions-container'; - - this.leftActionsContainer = document.createElement('div'); - this.leftActionsContainer.className = 'dv-left-actions-container'; - - this.preActionsContainer = document.createElement('div'); - this.preActionsContainer.className = 'dv-pre-actions-container'; - - this.tabContainer = document.createElement('div'); - this.tabContainer.className = 'dv-tabs-container'; - - this.voidContainer = new VoidContainer(this.accessor, this.group); - - this._element.appendChild(this.preActionsContainer); - this._element.appendChild(this.tabContainer); - this._element.appendChild(this.leftActionsContainer); - this._element.appendChild(this.voidContainer.element); - this._element.appendChild(this.rightActionsContainer); - - this.addDisposables( - this._onWillShowOverlay, - this._onDrop, - this._onTabDragStart, - this._onGroupDragStart, - this.voidContainer, - this.voidContainer.onDragStart((event) => { - this._onGroupDragStart.fire({ - nativeEvent: event, - group: this.group, - }); - }), - this.voidContainer.onDrop((event) => { - this._onDrop.fire({ - event: event.nativeEvent, - index: this.tabs.length, - }); - }), - this.voidContainer.onWillShowOverlay((event) => { - this._onWillShowOverlay.fire( - new WillShowOverlayLocationEvent(event, { - kind: 'header_space', - panel: this.group.activePanel, - api: this.accessor.api, - group: this.group, - getData: getPanelData, - }) - ); - }), - addDisposableListener( - this.voidContainer.element, - 'pointerdown', - (event) => { - const isFloatingGroupsEnabled = - !this.accessor.options.disableFloatingGroups; - - if ( - isFloatingGroupsEnabled && - event.shiftKey && - this.group.api.location.type !== 'floating' - ) { - event.preventDefault(); - - const { top, left } = - this.element.getBoundingClientRect(); - const { top: rootTop, left: rootLeft } = - this.accessor.element.getBoundingClientRect(); - - this.accessor.addFloatingGroup(this.group, { - x: left - rootLeft + 20, - y: top - rootTop + 20, - inDragMode: true, - }); - } - } - ), - addDisposableListener(this.tabContainer, 'pointerdown', (event) => { - if (event.defaultPrevented) { - return; - } - - const isLeftClick = event.button === 0; - - if (isLeftClick) { - this.accessor.doSetGroupActive(this.group); - } - }) - ); - } - - public setActive(_isGroupActive: boolean) { + setActive(_isGroupActive: boolean) { // noop } - 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(); - + delete(id: string): void { + this.tabs.delete(id); this.updateClassnames(); } - public setActivePanel(panel: IDockviewPanel): void { - this.tabs.forEach((tab) => { - const isActivePanel = panel.id === tab.value.panel.id; - tab.value.setActive(isActivePanel); - }); + setActivePanel(panel: IDockviewPanel): void { + this.tabs.setActivePanel(panel); } - public openPanel( - panel: IDockviewPanel, - index: number = this.tabs.length - ): void { - if (this.tabs.find((tab) => tab.value.panel.id === panel.id)) { - return; - } - const tab = new Tab(panel, this.accessor, this.group); - tab.setContent(panel.view.tab); - - const disposable = new CompositeDisposable( - tab.onDragStart((event) => { - this._onTabDragStart.fire({ nativeEvent: event, panel }); - }), - tab.onChanged((event) => { - const isFloatingGroupsEnabled = - !this.accessor.options.disableFloatingGroups; - - const isFloatingWithOnePanel = - this.group.api.location.type === 'floating' && - this.size === 1; - - if ( - isFloatingGroupsEnabled && - !isFloatingWithOnePanel && - event.shiftKey - ) { - event.preventDefault(); - - const panel = this.accessor.getGroupPanel(tab.panel.id); - - const { top, left } = tab.element.getBoundingClientRect(); - const { top: rootTop, left: rootLeft } = - this.accessor.element.getBoundingClientRect(); - - this.accessor.addFloatingGroup(panel as DockviewPanel, { - x: left - rootLeft, - y: top - rootTop, - inDragMode: true, - }); - return; - } - - const isLeftClick = event.button === 0; - - if (!isLeftClick || event.defaultPrevented) { - return; - } - - if (this.group.activePanel !== panel) { - this.group.model.openPanel(panel); - } - }), - tab.onDrop((event) => { - this._onDrop.fire({ - event: event.nativeEvent, - index: this.tabs.findIndex((x) => x.value === tab), - }); - }), - tab.onWillShowOverlay((event) => { - this._onWillShowOverlay.fire( - new WillShowOverlayLocationEvent(event, { - kind: 'tab', - panel: this.group.activePanel, - api: this.accessor.api, - group: this.group, - getData: getPanelData, - }) - ); - }) - ); - - const value: IValueDisposable = { value: tab, disposable }; - - this.addTab(value, index); + openPanel(panel: IDockviewPanel, index: number = this.tabs.size): void { + this.tabs.openPanel(panel, index); + this.updateClassnames(); } - public closePanel(panel: IDockviewPanel): void { + 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 = []; - } - - private addTab( - tab: IValueDisposable, - index: number = this.tabs.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.updateClassnames(); - } - private updateClassnames(): void { toggleClass(this._element, 'dv-single-tab', this.size === 1); } 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 63915751d..58618a025 100644 --- a/packages/dockview-core/src/dockview/dockviewComponent.ts +++ b/packages/dockview-core/src/dockview/dockviewComponent.ts @@ -74,6 +74,7 @@ import { } from '../overlay/overlayRenderContainer'; import { PopoutWindow } from '../popoutWindow'; import { StrictEventsSequencing } from './strictEventsSequencing'; +import { PopupService } from './components/popupService'; const DEFAULT_ROOT_OVERLAY_MODEL: DroptargetOverlayModel = { activationSize: { type: 'pixels', value: 10 }, @@ -256,6 +257,7 @@ export class DockviewComponent private watermark: IWatermarkRenderer | null = null; readonly overlayRenderContainer: OverlayRenderContainer; + readonly popupService: PopupService; private readonly _onWillDragPanel = new Emitter(); readonly onWillDragPanel: Event = this._onWillDragPanel.event; @@ -381,6 +383,8 @@ export class DockviewComponent className: options.className, }); + this.popupService = new PopupService(this.element); + this.overlayRenderContainer = new OverlayRenderContainer( this.gridview.element, this diff --git a/packages/dockview-core/src/dockview/dockviewPanelModel.ts b/packages/dockview-core/src/dockview/dockviewPanelModel.ts index 950b4d577..777717bad 100644 --- a/packages/dockview-core/src/dockview/dockviewPanelModel.ts +++ b/packages/dockview-core/src/dockview/dockviewPanelModel.ts @@ -14,6 +14,7 @@ export interface IDockviewPanelModel extends IDisposable { readonly tabComponent?: string; readonly content: IContentRenderer; readonly tab: ITabRenderer; + readonly newTab: ITabRenderer; update(event: PanelUpdateEvent): void; layout(width: number, height: number): void; init(params: GroupPanelPartInitParameters): void; @@ -42,6 +43,11 @@ export class DockviewPanelModel implements IDockviewPanelModel { this._tab = this.createTabComponent(this.id, tabComponent); } + get newTab() { + const cmp = this.createTabComponent(this.id, this.tabComponent); + return cmp; + } + init(params: GroupPanelPartInitParameters): void { this.content.init(params); this.tab.init(params); From 5ca5ffac8dfe65b7e48c703483eaddb7da593f2b Mon Sep 17 00:00:00 2001 From: mathuo <6710312+mathuo@users.noreply.github.com> Date: Sun, 19 Jan 2025 20:00:26 +0000 Subject: [PATCH 02/40] tmp --- .../dockview/components/titlebar/tabs.scss | 31 +++++++++++++++++++ .../src/dockview/components/titlebar/tabs.tsx | 30 +++++++++--------- 2 files changed, 47 insertions(+), 14 deletions(-) diff --git a/packages/dockview-core/src/dockview/components/titlebar/tabs.scss b/packages/dockview-core/src/dockview/components/titlebar/tabs.scss index 01b715404..62f600047 100644 --- a/packages/dockview-core/src/dockview/components/titlebar/tabs.scss +++ b/packages/dockview-core/src/dockview/components/titlebar/tabs.scss @@ -39,4 +39,35 @@ height: 100%; } } + + &.dv-tabs-overflow-container { + flex-direction: column; + height: unset; + + .dv-tab { + height: var(--dv-tabs-and-actions-container-height); + } + + .dv-active-tab { + background-color: var( + --dv-activegroup-visiblepanel-tab-background-color + ); + color: var(--dv-activegroup-visiblepanel-tab-color); + } + .dv-inactive-tab { + background-color: var( + --dv-activegroup-hiddenpanel-tab-background-color + ); + color: var(--dv-activegroup-hiddenpanel-tab-color); + } + } +} + +.dv-tabs-panel { + .dv-tabs-overflow-handle { + height: 100%; + width: 15px; + flex-shrink: 0; + background-color: red; + } } diff --git a/packages/dockview-core/src/dockview/components/titlebar/tabs.tsx b/packages/dockview-core/src/dockview/components/titlebar/tabs.tsx index 0a86dd54b..a477531a4 100644 --- a/packages/dockview-core/src/dockview/components/titlebar/tabs.tsx +++ b/packages/dockview-core/src/dockview/components/titlebar/tabs.tsx @@ -220,6 +220,7 @@ export class Tabs extends CompositeDisposable { private toggleDropdown(show: boolean): void { this._hasOverflow = show; + if (this._dropdownAnchor) { this._dropdownAnchor.remove(); this._dropdownAnchor = null; @@ -230,28 +231,29 @@ export class Tabs extends CompositeDisposable { } 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._dropdownAnchor.className = 'dv-tabs-overflow-handle'; 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'; + el.className = + 'dv-tabs-and-actions-container dv-tabs-container dv-tabs-overflow-container'; this.tabs.map((tab) => { - const tab2 = new Tab( - tab.value.panel, - this.accessor, - this.group - ); - tab2.setContent(tab.value.panel.view.newTab); - el.appendChild(tab2.element); + const child = tab.value.element.cloneNode(true); + + const wrapper = document.createElement('div'); + + wrapper.addEventListener('mousedown', () => { + this.accessor.popupService.close(); + tab.value.element.scrollIntoView(); + tab.value.panel.api.setActive(); + }); + wrapper.appendChild(child); + + el.appendChild(wrapper); }); this.accessor.popupService.openPopover(el, { From d811ca655406e03065e6d68f16253195bcb48eba Mon Sep 17 00:00:00 2001 From: mathuo <6710312+mathuo@users.noreply.github.com> Date: Thu, 30 Jan 2025 20:42:57 +0000 Subject: [PATCH 03/40] feat: improved dnd model --- .../src/__tests__/dnd/droptarget.spec.ts | 4 +- .../__tests__/dockview/components/tab.spec.ts | 51 ++-- .../components/titlebar/tabsContainer.spec.ts | 49 ++-- .../dockview/dockviewComponent.spec.ts | 40 +--- .../dockview/dockviewGroupPanelModel.spec.ts | 50 ++-- .../dockview-core/src/api/component.api.ts | 14 +- .../src/dnd/abstractDragHandler.ts | 6 +- .../src/dnd/dropTargetAnchorContainer.scss | 23 ++ .../src/dnd/dropTargetAnchorContainer.ts | 102 ++++++++ .../dockview-core/src/dnd/droptarget.scss | 10 +- packages/dockview-core/src/dnd/droptarget.ts | 204 +++++++++++++++- packages/dockview-core/src/dnd/ghost.ts | 5 +- .../dockview-core/src/dnd/groupDragHandler.ts | 4 +- .../src/dockview/components/panel/content.ts | 26 +- .../dockview/components/tab/defaultTab.scss | 4 +- .../src/dockview/components/tab/tab.ts | 32 ++- .../components/titlebar/tabsContainer.scss | 30 ++- .../components/titlebar/tabsContainer.ts | 5 +- .../components/titlebar/voidContainer.ts | 14 +- .../src/dockview/dockviewComponent.ts | 117 +++++++-- .../src/dockview/dockviewGroupPanelModel.ts | 37 +++ .../dockview-core/src/dockview/options.ts | 24 +- packages/dockview-core/src/dockview/theme.ts | 54 +++++ packages/dockview-core/src/index.ts | 1 + .../src/splitview/splitview.scss | 1 + .../dockview-core/src/splitview/splitview.ts | 2 + packages/dockview-core/src/theme.scss | 226 +++++++++++++----- .../src/theme/_drop-target-static-mixin.scss | 10 + .../src/theme/_sash-handle-mixin.scss | 53 ++++ .../dockview-core/src/theme/_space-mixin.scss | 52 ++++ packages/dockview/src/svg.tsx | 4 +- .../docs/docs/overview/getStarted/theme.mdx | 6 +- .../react/dockview/demo-dockview/src/app.scss | 1 + .../react/dockview/demo-dockview/src/app.tsx | 36 +-- .../dockview/demo-dockview/src/controls.tsx | 2 +- .../demo-dockview/src/gridActions.tsx | 40 +++- .../docs/src/components/frameworkSpecific.css | 27 ++- .../docs/src/components/frameworkSpecific.tsx | 5 +- .../src/components/ui/codeSandboxButton.scss | 2 +- .../src/components/ui/codeSandboxButton.tsx | 4 +- packages/docs/src/components/ui/container.tsx | 6 +- .../docs/src/components/ui/exampleFrame.tsx | 3 +- packages/docs/src/config/theme.config.ts | 33 ++- packages/docs/src/css/custom.scss | 28 ++- packages/docs/src/pages/demo.tsx | 135 ++++++++++- 45 files changed, 1232 insertions(+), 350 deletions(-) create mode 100644 packages/dockview-core/src/dnd/dropTargetAnchorContainer.scss create mode 100644 packages/dockview-core/src/dnd/dropTargetAnchorContainer.ts create mode 100644 packages/dockview-core/src/dockview/theme.ts create mode 100644 packages/dockview-core/src/theme/_drop-target-static-mixin.scss create mode 100644 packages/dockview-core/src/theme/_sash-handle-mixin.scss create mode 100644 packages/dockview-core/src/theme/_space-mixin.scss diff --git a/packages/dockview-core/src/__tests__/dnd/droptarget.spec.ts b/packages/dockview-core/src/__tests__/dnd/droptarget.spec.ts index 2d82095ab..b150ba896 100644 --- a/packages/dockview-core/src/__tests__/dnd/droptarget.spec.ts +++ b/packages/dockview-core/src/__tests__/dnd/droptarget.spec.ts @@ -16,10 +16,10 @@ describe('droptarget', () => { beforeEach(() => { element = document.createElement('div'); - jest.spyOn(element, 'clientHeight', 'get').mockImplementation( + jest.spyOn(element, 'offsetHeight', 'get').mockImplementation( () => 100 ); - jest.spyOn(element, 'clientWidth', 'get').mockImplementation(() => 200); + jest.spyOn(element, 'offsetWidth', 'get').mockImplementation(() => 200); }); test('that dragover events are marked', () => { diff --git a/packages/dockview-core/src/__tests__/dockview/components/tab.spec.ts b/packages/dockview-core/src/__tests__/dockview/components/tab.spec.ts index cb246b598..4a2f0c72d 100644 --- a/packages/dockview-core/src/__tests__/dockview/components/tab.spec.ts +++ b/packages/dockview-core/src/__tests__/dockview/components/tab.spec.ts @@ -8,6 +8,7 @@ import { DockviewGroupPanel } from '../../../dockview/dockviewGroupPanel'; import { DockviewGroupPanelModel } from '../../../dockview/dockviewGroupPanelModel'; import { Tab } from '../../../dockview/components/tab/tab'; import { IDockviewPanel } from '../../../dockview/dockviewPanel'; +import { fromPartial } from '@total-typescript/shoehorn'; describe('tab', () => { test('that empty tab has inactive-tab class', () => { @@ -46,15 +47,10 @@ describe('tab', () => { id: 'testcomponentid', }; }); - const groupviewMock = jest.fn, []>( - () => { - return { - canDisplayOverlay: jest.fn(), - }; - } - ); - const groupView = new groupviewMock() as DockviewGroupPanelModel; + const groupView = fromPartial({ + canDisplayOverlay: jest.fn(), + }); const groupPanelMock = jest.fn, []>(() => { return { @@ -72,38 +68,33 @@ describe('tab', () => { groupPanel ); - jest.spyOn(cut.element, 'clientHeight', 'get').mockImplementation( + jest.spyOn(cut.element, 'offsetHeight', 'get').mockImplementation( () => 100 ); - jest.spyOn(cut.element, 'clientWidth', 'get').mockImplementation( + jest.spyOn(cut.element, 'offsetWidth', 'get').mockImplementation( () => 100 ); fireEvent.dragEnter(cut.element); fireEvent.dragOver(cut.element); - expect(groupView.canDisplayOverlay).toBeCalled(); + expect(groupView.canDisplayOverlay).toHaveBeenCalled(); expect( cut.element.getElementsByClassName('dv-drop-target-dropzone').length ).toBe(0); }); - test('that if you drag over yourself no drop target is shown', () => { + test('that if you drag over yourself a drop target is shown', () => { const accessorMock = jest.fn, []>(() => { return { id: 'testcomponentid', }; }); - const groupviewMock = jest.fn, []>( - () => { - return { - canDisplayOverlay: jest.fn(), - }; - } - ); - const groupView = new groupviewMock() as DockviewGroupPanelModel; + const groupView = fromPartial({ + canDisplayOverlay: jest.fn(), + }); const groupPanelMock = jest.fn, []>(() => { return { @@ -121,10 +112,10 @@ describe('tab', () => { groupPanel ); - jest.spyOn(cut.element, 'clientHeight', 'get').mockImplementation( + jest.spyOn(cut.element, 'offsetHeight', 'get').mockImplementation( () => 100 ); - jest.spyOn(cut.element, 'clientWidth', 'get').mockImplementation( + jest.spyOn(cut.element, 'offsetWidth', 'get').mockImplementation( () => 100 ); @@ -136,11 +127,11 @@ describe('tab', () => { fireEvent.dragEnter(cut.element); fireEvent.dragOver(cut.element); - expect(groupView.canDisplayOverlay).toBeCalledTimes(0); + expect(groupView.canDisplayOverlay).toHaveBeenCalledTimes(0); expect( cut.element.getElementsByClassName('dv-drop-target-dropzone').length - ).toBe(0); + ).toBe(1); }); test('that if you drag over another tab a drop target is shown', () => { @@ -175,10 +166,10 @@ describe('tab', () => { groupPanel ); - jest.spyOn(cut.element, 'clientHeight', 'get').mockImplementation( + jest.spyOn(cut.element, 'offsetHeight', 'get').mockImplementation( () => 100 ); - jest.spyOn(cut.element, 'clientWidth', 'get').mockImplementation( + jest.spyOn(cut.element, 'offsetWidth', 'get').mockImplementation( () => 100 ); @@ -229,10 +220,10 @@ describe('tab', () => { groupPanel ); - jest.spyOn(cut.element, 'clientHeight', 'get').mockImplementation( + jest.spyOn(cut.element, 'offsetHeight', 'get').mockImplementation( () => 100 ); - jest.spyOn(cut.element, 'clientWidth', 'get').mockImplementation( + jest.spyOn(cut.element, 'offsetWidth', 'get').mockImplementation( () => 100 ); @@ -289,10 +280,10 @@ describe('tab', () => { groupPanel ); - jest.spyOn(cut.element, 'clientHeight', 'get').mockImplementation( + jest.spyOn(cut.element, 'offsetHeight', 'get').mockImplementation( () => 100 ); - jest.spyOn(cut.element, 'clientWidth', 'get').mockImplementation( + jest.spyOn(cut.element, 'offsetWidth', 'get').mockImplementation( () => 100 ); diff --git a/packages/dockview-core/src/__tests__/dockview/components/titlebar/tabsContainer.spec.ts b/packages/dockview-core/src/__tests__/dockview/components/titlebar/tabsContainer.spec.ts index eee78a588..bae8daa8d 100644 --- a/packages/dockview-core/src/__tests__/dockview/components/titlebar/tabsContainer.spec.ts +++ b/packages/dockview-core/src/__tests__/dockview/components/titlebar/tabsContainer.spec.ts @@ -42,16 +42,16 @@ describe('tabsContainer', () => { const emptySpace = cut.element .getElementsByClassName('dv-void-container') - .item(0); + .item(0) as HTMLElement; if (!emptySpace!) { fail('element not found'); } - jest.spyOn(emptySpace!, 'clientHeight', 'get').mockImplementation( + jest.spyOn(emptySpace!, 'offsetHeight', 'get').mockImplementation( () => 100 ); - jest.spyOn(emptySpace!, 'clientWidth', 'get').mockImplementation( + jest.spyOn(emptySpace!, 'offsetWidth', 'get').mockImplementation( () => 100 ); @@ -73,15 +73,14 @@ describe('tabsContainer', () => { options: {}, }); - const groupviewMock = jest.fn, []>( - () => { - return { - canDisplayOverlay: jest.fn(), - }; - } - ); + const dropTargetContainer = document.createElement('div'); - const groupView = new groupviewMock() as DockviewGroupPanelModel; + const groupView = fromPartial({ + canDisplayOverlay: jest.fn(), + // dropTargetContainer: new DropTargetAnchorContainer( + // dropTargetContainer + // ), + }); const groupPanelMock = jest.fn, []>(() => { return { @@ -97,16 +96,16 @@ describe('tabsContainer', () => { const emptySpace = cut.element .getElementsByClassName('dv-void-container') - .item(0); + .item(0) as HTMLElement; if (!emptySpace!) { fail('element not found'); } - jest.spyOn(emptySpace!, 'clientHeight', 'get').mockImplementation( + jest.spyOn(emptySpace!, 'offsetHeight', 'get').mockImplementation( () => 100 ); - jest.spyOn(emptySpace!, 'clientWidth', 'get').mockImplementation( + jest.spyOn(emptySpace!, 'offsetWidth', 'get').mockImplementation( () => 100 ); @@ -129,6 +128,10 @@ describe('tabsContainer', () => { expect( cut.element.getElementsByClassName('dv-drop-target-dropzone').length ).toBe(1); + // expect( + // dropTargetContainer.getElementsByClassName('dv-drop-target-anchor') + // .length + // ).toBe(1); }); test('that dropping over the empty space should render a drop target', () => { @@ -166,16 +169,16 @@ describe('tabsContainer', () => { const emptySpace = cut.element .getElementsByClassName('dv-void-container') - .item(0); + .item(0) as HTMLElement; if (!emptySpace!) { fail('element not found'); } - jest.spyOn(emptySpace!, 'clientHeight', 'get').mockImplementation( + jest.spyOn(emptySpace!, 'offsetHeight', 'get').mockImplementation( () => 100 ); - jest.spyOn(emptySpace!, 'clientWidth', 'get').mockImplementation( + jest.spyOn(emptySpace!, 'offsetWidth', 'get').mockImplementation( () => 100 ); @@ -229,16 +232,16 @@ describe('tabsContainer', () => { const emptySpace = cut.element .getElementsByClassName('dv-void-container') - .item(0); + .item(0) as HTMLElement; if (!emptySpace!) { fail('element not found'); } - jest.spyOn(emptySpace!, 'clientHeight', 'get').mockImplementation( + jest.spyOn(emptySpace!, 'offsetHeight', 'get').mockImplementation( () => 100 ); - jest.spyOn(emptySpace!, 'clientWidth', 'get').mockImplementation( + jest.spyOn(emptySpace!, 'offsetWidth', 'get').mockImplementation( () => 100 ); @@ -291,16 +294,16 @@ describe('tabsContainer', () => { const emptySpace = cut.element .getElementsByClassName('dv-void-container') - .item(0); + .item(0) as HTMLElement; if (!emptySpace!) { fail('element not found'); } - jest.spyOn(emptySpace!, 'clientHeight', 'get').mockImplementation( + jest.spyOn(emptySpace!, 'offsetHeight', 'get').mockImplementation( () => 100 ); - jest.spyOn(emptySpace!, 'clientWidth', 'get').mockImplementation( + jest.spyOn(emptySpace!, 'offsetWidth', 'get').mockImplementation( () => 100 ); diff --git a/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts b/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts index 0d00d1a31..8c0dab657 100644 --- a/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts +++ b/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts @@ -133,11 +133,11 @@ describe('dockviewComponent', () => { }, className: 'test-a test-b', }); - expect(dockview.element.className).toBe('test-a test-b'); + expect(dockview.element.className).toBe('test-a test-b dockview-theme-abyss'); dockview.updateOptions({ className: 'test-b test-c' }); - expect(dockview.element.className).toBe('test-b test-c'); + expect(dockview.element.className).toBe('dockview-theme-abyss test-b test-c'); }); describe('memory leakage', () => { @@ -3339,10 +3339,10 @@ describe('dockviewComponent', () => { position: { direction: 'right' }, }); - Object.defineProperty(dockview.element, 'clientWidth', { + Object.defineProperty(dockview.element, 'offsetWidth', { get: () => 100, }); - Object.defineProperty(dockview.element, 'clientHeight', { + Object.defineProperty(dockview.element, 'offsetHeight', { get: () => 100, }); @@ -6652,36 +6652,4 @@ describe('dockviewComponent', () => { expect(api.panels.length).toBe(3); expect(api.groups.length).toBe(3); }); - - describe('updateOptions', () => { - test('gap', () => { - const container = document.createElement('div'); - - const dockview = new DockviewComponent(container, { - createComponent(options) { - switch (options.name) { - case 'default': - return new PanelContentPartTest( - options.id, - options.name - ); - default: - throw new Error(`unsupported`); - } - }, - gap: 6, - }); - - expect(dockview.gap).toBe(6); - - dockview.updateOptions({ gap: 10 }); - expect(dockview.gap).toBe(10); - - dockview.updateOptions({}); - expect(dockview.gap).toBe(10); - - dockview.updateOptions({ gap: 15 }); - expect(dockview.gap).toBe(15); - }); - }); }); diff --git a/packages/dockview-core/src/__tests__/dockview/dockviewGroupPanelModel.spec.ts b/packages/dockview-core/src/__tests__/dockview/dockviewGroupPanelModel.spec.ts index 55aed39ec..19b811c4f 100644 --- a/packages/dockview-core/src/__tests__/dockview/dockviewGroupPanelModel.spec.ts +++ b/packages/dockview-core/src/__tests__/dockview/dockviewGroupPanelModel.spec.ts @@ -684,12 +684,12 @@ describe('dockviewGroupPanelModel', () => { const element = container .getElementsByClassName('dv-content-container') - .item(0)!; + .item(0)! as HTMLElement; - jest.spyOn(element, 'clientHeight', 'get').mockImplementation( + jest.spyOn(element, 'offsetHeight', 'get').mockImplementation( () => 100 ); - jest.spyOn(element, 'clientWidth', 'get').mockImplementation(() => 100); + jest.spyOn(element, 'offsetWidth', 'get').mockImplementation(() => 100); fireEvent.dragEnter(element); fireEvent.dragOver(element); @@ -744,12 +744,12 @@ describe('dockviewGroupPanelModel', () => { const element = container .getElementsByClassName('dv-content-container') - .item(0)!; + .item(0)! as HTMLElement; - jest.spyOn(element, 'clientHeight', 'get').mockImplementation( + jest.spyOn(element, 'offsetHeight', 'get').mockImplementation( () => 100 ); - jest.spyOn(element, 'clientWidth', 'get').mockImplementation(() => 100); + jest.spyOn(element, 'offsetWidth', 'get').mockImplementation(() => 100); function run(value: number) { fireEvent.dragEnter(element); @@ -792,7 +792,7 @@ describe('dockviewGroupPanelModel', () => { fireEvent.dragEnd(element); }); - test('that should not show drop target if dropping on self', () => { + test('that should show drop target if dropping on self', () => { const accessor = fromPartial({ id: 'testcomponentid', options: {}, @@ -806,15 +806,9 @@ describe('dockviewGroupPanelModel', () => { ), }); - const groupviewMock = jest.fn, []>( - () => { - return { - canDisplayOverlay: jest.fn(), - }; - } - ); - - const groupView = new groupviewMock() as DockviewGroupPanelModel; + const groupView = fromPartial({ + canDisplayOverlay: jest.fn(), + }); const groupPanelMock = jest.fn, []>(() => { return { @@ -842,12 +836,12 @@ describe('dockviewGroupPanelModel', () => { const element = container .getElementsByClassName('dv-content-container') - .item(0)!; + .item(0)! as HTMLElement; - jest.spyOn(element, 'clientHeight', 'get').mockImplementation( + jest.spyOn(element, 'offsetHeight', 'get').mockImplementation( () => 100 ); - jest.spyOn(element, 'clientWidth', 'get').mockImplementation(() => 100); + jest.spyOn(element, 'offsetWidth', 'get').mockImplementation(() => 100); LocalSelectionTransfer.getInstance().setData( [new PanelTransfer('testcomponentid', 'groupviewid', 'panel1')], @@ -861,10 +855,10 @@ describe('dockviewGroupPanelModel', () => { expect( element.getElementsByClassName('dv-drop-target-dropzone').length - ).toBe(0); + ).toBe(1); }); - test('that should not allow drop when dropping on self for same component id', () => { + test('that should allow drop when dropping on self for same component id', () => { const accessor = fromPartial({ id: 'testcomponentid', options: {}, @@ -915,12 +909,12 @@ describe('dockviewGroupPanelModel', () => { const element = container .getElementsByClassName('dv-content-container') - .item(0)!; + .item(0) as HTMLElement; - jest.spyOn(element, 'clientHeight', 'get').mockImplementation( + jest.spyOn(element, 'offsetHeight', 'get').mockImplementation( () => 100 ); - jest.spyOn(element, 'clientWidth', 'get').mockImplementation(() => 100); + jest.spyOn(element, 'offsetWidth', 'get').mockImplementation(() => 100); LocalSelectionTransfer.getInstance().setData( [new PanelTransfer('testcomponentid', 'groupviewid', 'panel1')], @@ -934,7 +928,7 @@ describe('dockviewGroupPanelModel', () => { expect( element.getElementsByClassName('dv-drop-target-dropzone').length - ).toBe(0); + ).toBe(1); }); test('that should not allow drop when not dropping for different component id', () => { @@ -988,12 +982,12 @@ describe('dockviewGroupPanelModel', () => { const element = container .getElementsByClassName('dv-content-container') - .item(0)!; + .item(0) as HTMLElement; - jest.spyOn(element, 'clientHeight', 'get').mockImplementation( + jest.spyOn(element, 'offsetHeight', 'get').mockImplementation( () => 100 ); - jest.spyOn(element, 'clientWidth', 'get').mockImplementation(() => 100); + jest.spyOn(element, 'offsetWidth', 'get').mockImplementation(() => 100); LocalSelectionTransfer.getInstance().setData( [new PanelTransfer('anothercomponentid', 'groupviewid', 'panel1')], diff --git a/packages/dockview-core/src/api/component.api.ts b/packages/dockview-core/src/api/component.api.ts index 6884c93aa..0b0850ed0 100644 --- a/packages/dockview-core/src/api/component.api.ts +++ b/packages/dockview-core/src/api/component.api.ts @@ -629,10 +629,20 @@ export class DockviewApi implements CommonApi { return this.component.totalPanels; } + /** + * @deprecated dockview: dockviewComponent.gap has been deprecated. Use `theme` instead. This will be removed in a future version. + */ get gap(): number { return this.component.gap; } + /** + * @deprecated dockview: dockviewComponent.setGap has been deprecated. Use `theme` instead. This will be removed in a future version. + */ + setGap(gap: number | undefined): void { + this.component.updateOptions({ gap: gap }); + } + /** * Invoked when the active group changes. May be undefined if no group is active. */ @@ -914,10 +924,6 @@ export class DockviewApi implements CommonApi { return this.component.addPopoutGroup(item, options); } - setGap(gap: number | undefined): void { - this.component.updateOptions({ gap }); - } - updateOptions(options: Partial) { this.component.updateOptions(options); } diff --git a/packages/dockview-core/src/dnd/abstractDragHandler.ts b/packages/dockview-core/src/dnd/abstractDragHandler.ts index 84345c160..7ba701034 100644 --- a/packages/dockview-core/src/dnd/abstractDragHandler.ts +++ b/packages/dockview-core/src/dnd/abstractDragHandler.ts @@ -67,7 +67,7 @@ export abstract class DragHandler extends CompositeDisposable { * For example: in react-dnd if dataTransfer.types is not set then the dragStart event will be cancelled * through .preventDefault(). Since this is applied globally to all drag events this would break dockviews * dnd logic. You can see the code at - * https://github.com/react-dnd/react-dnd/blob/main/packages/backend-html5/src/HTML5BackendImpl.ts#L542 + P * https://github.com/react-dnd/react-dnd/blob/main/packages/backend-html5/src/HTML5BackendImpl.ts#L542 */ event.dataTransfer.setData('text/plain', ''); } @@ -75,7 +75,9 @@ export abstract class DragHandler extends CompositeDisposable { }), addDisposableListener(this.el, 'dragend', () => { this.pointerEventsDisposable.dispose(); - this.dataDisposable.dispose(); + setTimeout(() => { + this.dataDisposable.dispose(); // allow the data to be read by other handlers before disposing + }, 0); }) ); } diff --git a/packages/dockview-core/src/dnd/dropTargetAnchorContainer.scss b/packages/dockview-core/src/dnd/dropTargetAnchorContainer.scss new file mode 100644 index 000000000..0fd3dc5a5 --- /dev/null +++ b/packages/dockview-core/src/dnd/dropTargetAnchorContainer.scss @@ -0,0 +1,23 @@ +.dv-drop-target-container { + position: absolute; + z-index: 9999; + top: 0px; + left: 0px; + height: 100%; + width: 100%; + pointer-events: none; + overflow: hidden; + --dv-transition-duration: 300ms; + + .dv-drop-target-anchor { + position: relative; + border: var(--dv-drag-over-border); + transition: opacity var(--dv-transition-duration) ease-in, + top var(--dv-transition-duration) ease-out, + left var(--dv-transition-duration) ease-out, + width var(--dv-transition-duration) ease-out, + height var(--dv-transition-duration) ease-out; + background-color: var(--dv-drag-over-background-color); + opacity: 1; + } +} diff --git a/packages/dockview-core/src/dnd/dropTargetAnchorContainer.ts b/packages/dockview-core/src/dnd/dropTargetAnchorContainer.ts new file mode 100644 index 000000000..e43989c9e --- /dev/null +++ b/packages/dockview-core/src/dnd/dropTargetAnchorContainer.ts @@ -0,0 +1,102 @@ +import { CompositeDisposable, Disposable } from '../lifecycle'; +import { DropTargetTargetModel } from './droptarget'; + +export class DropTargetAnchorContainer extends CompositeDisposable { + private _model: + | { root: HTMLElement; overlay: HTMLElement; changed: boolean } + | undefined; + + private _outline: HTMLElement | undefined; + + private _disabled = false; + + get disabled(): boolean { + return this._disabled; + } + + set disabled(value: boolean) { + if (this.disabled === value) { + return; + } + + this._disabled = value; + + if (value) { + this.model?.clear(); + } + } + + get model(): DropTargetTargetModel | undefined { + if (this.disabled) { + return undefined; + } + + return { + clear: () => { + if (this._model) { + this._model.root.parentElement?.removeChild( + this._model.root + ); + } + this._model = undefined; + }, + exists: () => { + return !!this._model; + }, + getElements: (event?: DragEvent, outline?: HTMLElement) => { + const changed = this._outline !== outline; + this._outline = outline; + + if (this._model) { + this._model.changed = changed; + return this._model; + } + + const container = this.createContainer(); + const anchor = this.createAnchor(); + + this._model = { root: container, overlay: anchor, changed }; + + container.appendChild(anchor); + this.element.appendChild(container); + + if (event?.target instanceof HTMLElement) { + const targetBox = event.target.getBoundingClientRect(); + const box = this.element.getBoundingClientRect(); + + anchor.style.left = `${targetBox.left - box.left}px`; + anchor.style.top = `${targetBox.top - box.top}px`; + } + + return this._model; + }, + }; + } + + constructor(readonly element: HTMLElement, options: { disabled: boolean }) { + super(); + + this._disabled = options.disabled; + + this.addDisposables( + Disposable.from(() => { + this.model?.clear(); + }) + ); + } + + private createContainer(): HTMLElement { + const el = document.createElement('div'); + el.className = 'dv-drop-target-container'; + + return el; + } + + private createAnchor(): HTMLElement { + const el = document.createElement('div'); + el.className = 'dv-drop-target-anchor'; + el.style.visibility = 'hidden'; + + return el; + } +} diff --git a/packages/dockview-core/src/dnd/droptarget.scss b/packages/dockview-core/src/dnd/droptarget.scss index f23f318f7..7f2c8cb8b 100644 --- a/packages/dockview-core/src/dnd/droptarget.scss +++ b/packages/dockview-core/src/dnd/droptarget.scss @@ -1,5 +1,6 @@ .dv-drop-target { position: relative; + --dv-transition-duration: 70ms; > .dv-drop-target-dropzone { position: absolute; @@ -15,10 +16,13 @@ box-sizing: border-box; height: 100%; width: 100%; + border: var(--dv-drag-over-border); background-color: var(--dv-drag-over-background-color); - transition: top 70ms ease-out, left 70ms ease-out, - width 70ms ease-out, height 70ms ease-out, - opacity 0.15s ease-out; + transition: top var(--dv-transition-duration) ease-out, + left var(--dv-transition-duration) ease-out, + width var(--dv-transition-duration) ease-out, + height var(--dv-transition-duration) ease-out, + opacity var(--dv-transition-duration) ease-out; will-change: transform; pointer-events: none; diff --git a/packages/dockview-core/src/dnd/droptarget.ts b/packages/dockview-core/src/dnd/droptarget.ts index 702fed867..c611f890b 100644 --- a/packages/dockview-core/src/dnd/droptarget.ts +++ b/packages/dockview-core/src/dnd/droptarget.ts @@ -93,10 +93,26 @@ const DEFAULT_SIZE: MeasuredValue = { const SMALL_WIDTH_BOUNDARY = 100; const SMALL_HEIGHT_BOUNDARY = 100; +export interface DropTargetTargetModel { + getElements( + event?: DragEvent, + outline?: HTMLElement + ): { + root: HTMLElement; + overlay: HTMLElement; + changed: boolean; + }; + exists(): boolean; + clear(): void; +} + export interface DroptargetOptions { canDisplayOverlay: CanDisplayOverlay; acceptedTargetZones: Position[]; overlayModel?: DroptargetOverlayModel; + getOverrideTarget?: () => DropTargetTargetModel | undefined; + className?: string; + getOverlayOutline?: () => HTMLElement | null; } export class Droptarget extends CompositeDisposable { @@ -116,6 +132,18 @@ export class Droptarget extends CompositeDisposable { private static USED_EVENT_ID = '__dockview_droptarget_event_is_used__'; + private static ACTUAL_TARGET: Droptarget | undefined; + + private _disabled: boolean; + + get disabled(): boolean { + return this._disabled; + } + + set disabled(value: boolean) { + this._disabled = value; + } + get state(): Position | undefined { return this._state; } @@ -126,21 +154,35 @@ export class Droptarget extends CompositeDisposable { ) { super(); + this._disabled = false; + // use a set to take advantage of #.has this._acceptedTargetZonesSet = new Set( this.options.acceptedTargetZones ); this.dnd = new DragAndDropObserver(this.element, { - onDragEnter: () => undefined, + onDragEnter: () => { + this.options.getOverrideTarget?.()?.getElements(); + }, onDragOver: (e) => { + Droptarget.ACTUAL_TARGET = this; + + const overrideTraget = this.options.getOverrideTarget?.(); + if (this._acceptedTargetZonesSet.size === 0) { + if (overrideTraget) { + return; + } this.removeDropTarget(); return; } - const width = this.element.clientWidth; - const height = this.element.clientHeight; + const target = + this.options.getOverlayOutline?.() ?? this.element; + + const width = target.offsetWidth; + const height = target.offsetHeight; if (width === 0 || height === 0) { return; // avoid div!0 @@ -149,8 +191,8 @@ export class Droptarget extends CompositeDisposable { const rect = ( e.currentTarget as HTMLElement ).getBoundingClientRect(); - const x = e.clientX - rect.left; - const y = e.clientY - rect.top; + const x = (e.clientX ?? 0) - rect.left; + const y = (e.clientY ?? 0) - rect.top; const quadrant = this.calculateQuadrant( this._acceptedTargetZonesSet, @@ -172,6 +214,9 @@ export class Droptarget extends CompositeDisposable { } if (!this.options.canDisplayOverlay(e, quadrant)) { + if (overrideTraget) { + return; + } this.removeDropTarget(); return; } @@ -194,7 +239,9 @@ export class Droptarget extends CompositeDisposable { this.markAsUsed(e); - if (!this.targetElement) { + if (overrideTraget) { + // + } else if (!this.targetElement) { this.targetElement = document.createElement('div'); this.targetElement.className = 'dv-drop-target-dropzone'; this.overlayElement = document.createElement('div'); @@ -202,8 +249,16 @@ export class Droptarget extends CompositeDisposable { this._state = 'center'; this.targetElement.appendChild(this.overlayElement); - this.element.classList.add('dv-drop-target'); - this.element.append(this.targetElement); + target.classList.add('dv-drop-target'); + target.append(this.targetElement); + + // this.overlayElement.style.opacity = '0'; + + // requestAnimationFrame(() => { + // if (this.overlayElement) { + // this.overlayElement.style.opacity = ''; + // } + // }); } this.toggleClasses(quadrant, width, height); @@ -211,10 +266,32 @@ export class Droptarget extends CompositeDisposable { this._state = quadrant; }, onDragLeave: () => { + const target = this.options.getOverrideTarget?.(); + + if (target) { + return; + } + this.removeDropTarget(); }, - onDragEnd: () => { + onDragEnd: (e) => { + const target = this.options.getOverrideTarget?.(); + + if (target && Droptarget.ACTUAL_TARGET === this) { + if (this._state) { + // only stop the propagation of the event if we are dealing with it + // which is only when the target has state + e.stopPropagation(); + this._onDrop.fire({ + position: this._state, + nativeEvent: e, + }); + } + } + this.removeDropTarget(); + + target?.clear(); }, onDrop: (e) => { e.preventDefault(); @@ -223,6 +300,8 @@ export class Droptarget extends CompositeDisposable { this.removeDropTarget(); + this.options.getOverrideTarget?.()?.clear(); + if (state) { // only stop the propagation of the event if we are dealing with it // which is only when the target has state @@ -268,7 +347,9 @@ export class Droptarget extends CompositeDisposable { width: number, height: number ): void { - if (!this.overlayElement) { + const target = this.options.getOverrideTarget?.(); + + if (!target && !this.overlayElement) { return; } @@ -300,6 +381,103 @@ export class Droptarget extends CompositeDisposable { } } + if (target) { + const outlineEl = + this.options.getOverlayOutline?.() ?? this.element; + const elBox = outlineEl.getBoundingClientRect(); + + const ta = target.getElements(undefined, outlineEl); + const el = ta.root; + const overlay = ta.overlay; + + const bigbox = el.getBoundingClientRect(); + + const rootTop = elBox.top - bigbox.top; + const rootLeft = elBox.left - bigbox.left; + + const box = { + top: rootTop, + left: rootLeft, + width: width, + height: height, + }; + + if (rightClass) { + box.left = rootLeft + width * (1 - size); + box.width = width * size; + } else if (leftClass) { + box.width = width * size; + } else if (topClass) { + box.height = height * size; + } else if (bottomClass) { + box.top = rootTop + height * (1 - size); + box.height = height * size; + } + + if (isSmallX && isLeft) { + box.width = 4; + } + if (isSmallX && isRight) { + box.left = rootLeft + width - 4; + box.width = 4; + } + + const topPx = `${Math.round(box.top)}px`; + const leftPx = `${Math.round(box.left)}px`; + const widthPx = `${Math.round(box.width)}px`; + const heightPx = `${Math.round(box.height)}px`; + + if ( + overlay.style.top === topPx && + overlay.style.left === leftPx && + overlay.style.width === widthPx && + overlay.style.height === heightPx + ) { + return; + } + + overlay.style.top = topPx; + overlay.style.left = leftPx; + overlay.style.width = widthPx; + overlay.style.height = heightPx; + overlay.style.visibility = 'visible'; + + overlay.className = `dv-drop-target-anchor${ + this.options.className ? ` ${this.options.className}` : '' + }`; + + toggleClass(overlay, 'dv-drop-target-left', isLeft); + toggleClass(overlay, 'dv-drop-target-right', isRight); + toggleClass(overlay, 'dv-drop-target-top', isTop); + toggleClass(overlay, 'dv-drop-target-bottom', isBottom); + toggleClass( + overlay, + 'dv-drop-target-center', + quadrant === 'center' + ); + + if (ta.changed) { + toggleClass( + overlay, + 'dv-drop-target-anchor-container-changed', + true + ); + setTimeout(() => { + toggleClass( + overlay, + 'dv-drop-target-anchor-container-changed', + false + ); + }, 10); + } + + return; + } + + if (!this.overlayElement) { + return; + } + const box = { top: '0px', left: '0px', width: '100%', height: '100%' }; /** @@ -396,10 +574,12 @@ export class Droptarget extends CompositeDisposable { private removeDropTarget(): void { if (this.targetElement) { this._state = undefined; - this.element.removeChild(this.targetElement); + this.targetElement.parentElement?.classList.remove( + 'dv-drop-target' + ); + this.targetElement.remove(); this.targetElement = undefined; this.overlayElement = undefined; - this.element.classList.remove('dv-drop-target'); } } } diff --git a/packages/dockview-core/src/dnd/ghost.ts b/packages/dockview-core/src/dnd/ghost.ts index 2ff9c569f..df976c7cf 100644 --- a/packages/dockview-core/src/dnd/ghost.ts +++ b/packages/dockview-core/src/dnd/ghost.ts @@ -2,13 +2,14 @@ import { addClasses, removeClasses } from '../dom'; export function addGhostImage( dataTransfer: DataTransfer, - ghostElement: HTMLElement + ghostElement: HTMLElement, + options?: { x?: number; y?: number } ): void { // class dockview provides to force ghost image to be drawn on a different layer and prevent weird rendering issues addClasses(ghostElement, 'dv-dragged'); document.body.appendChild(ghostElement); - dataTransfer.setDragImage(ghostElement, 0, 0); + dataTransfer.setDragImage(ghostElement, options?.x ?? 0, options?.y ?? 0); setTimeout(() => { removeClasses(ghostElement, 'dv-dragged'); diff --git a/packages/dockview-core/src/dnd/groupDragHandler.ts b/packages/dockview-core/src/dnd/groupDragHandler.ts index bdda2be3b..2e3c9d281 100644 --- a/packages/dockview-core/src/dnd/groupDragHandler.ts +++ b/packages/dockview-core/src/dnd/groupDragHandler.ts @@ -72,9 +72,11 @@ export class GroupDragHandler extends DragHandler { ghostElement.style.lineHeight = '20px'; ghostElement.style.borderRadius = '12px'; ghostElement.style.position = 'absolute'; + ghostElement.style.pointerEvents = 'none'; + ghostElement.style.top = '-9999px'; ghostElement.textContent = `Multiple Panels (${this.group.size})`; - addGhostImage(dataTransfer, ghostElement); + addGhostImage(dataTransfer, ghostElement, { y: -10, x: 30 }); } return { diff --git a/packages/dockview-core/src/dockview/components/panel/content.ts b/packages/dockview-core/src/dockview/components/panel/content.ts index 4f66b03d3..08703179d 100644 --- a/packages/dockview-core/src/dockview/components/panel/content.ts +++ b/packages/dockview-core/src/dockview/components/panel/content.ts @@ -55,7 +55,15 @@ export class ContentContainer this.addDisposables(this._onDidFocus, this._onDidBlur); + const target = group.dropTargetContainer; + this.dropTarget = new Droptarget(this.element, { + getOverlayOutline: () => { + return accessor.options.theme?.includeHeaderWhenHoverOverContent + ? this.element.parentElement + : null; + }, + className: 'dv-drop-target-content', acceptedTargetZones: ['top', 'bottom', 'left', 'right', 'center'], canDisplayOverlay: (event, position) => { if ( @@ -76,26 +84,12 @@ export class ContentContainer } if (data && data.viewId === this.accessor.id) { - if (data.groupId === this.group.id) { - if (position === 'center') { - // don't allow to drop on self for center position - return false; - } - if (data.panelId === null) { - // don't allow group move to drop anywhere on self - return false; - } - } - - const groupHasOnePanelAndIsActiveDragElement = - this.group.panels.length === 1 && - data.groupId === this.group.id; - - return !groupHasOnePanelAndIsActiveDragElement; + return true; } return this.group.canDisplayOverlay(event, position, 'content'); }, + getOverrideTarget: target ? () => target.model : undefined, }); this.addDisposables(this.dropTarget); diff --git a/packages/dockview-core/src/dockview/components/tab/defaultTab.scss b/packages/dockview-core/src/dockview/components/tab/defaultTab.scss index 0fdf53d78..3d2865583 100644 --- a/packages/dockview-core/src/dockview/components/tab/defaultTab.scss +++ b/packages/dockview-core/src/dockview/components/tab/defaultTab.scss @@ -58,15 +58,13 @@ position: relative; height: 100%; display: flex; - min-width: 80px; align-items: center; - padding: 0px 8px; white-space: nowrap; text-overflow: ellipsis; .dv-default-tab-content { - padding: 0px 8px; flex-grow: 1; + margin-right: 4px; } .dv-default-tab-action { diff --git a/packages/dockview-core/src/dockview/components/tab/tab.ts b/packages/dockview-core/src/dockview/components/tab/tab.ts index 1eb1174d8..9b1975d4a 100644 --- a/packages/dockview-core/src/dockview/components/tab/tab.ts +++ b/packages/dockview-core/src/dockview/components/tab/tab.ts @@ -16,6 +16,7 @@ import { } from '../../../dnd/droptarget'; import { DragHandler } from '../../../dnd/abstractDragHandler'; import { IDockviewPanel } from '../../dockviewPanel'; +import { addGhostImage } from '../../../dnd/ghost'; class TabDragHandler extends DragHandler { private readonly panelTransfer = @@ -86,7 +87,8 @@ export class Tab extends CompositeDisposable { ); this.dropTarget = new Droptarget(this._element, { - acceptedTargetZones: ['center'], + acceptedTargetZones: ['left', 'right'], + overlayModel: { activationSize: { value: 50, type: 'percentage' } }, canDisplayOverlay: (event, position) => { if (this.group.locked) { return false; @@ -95,15 +97,7 @@ export class Tab extends CompositeDisposable { const data = getPanelData(); if (data && this.accessor.id === data.viewId) { - if ( - data.panelId === null && - data.groupId === this.group.id - ) { - // don't allow group move to drop on self - return false; - } - - return this.panel.id !== data.panelId; + return true; } return this.group.model.canDisplayOverlay( @@ -112,6 +106,7 @@ export class Tab extends CompositeDisposable { 'tab' ); }, + getOverrideTarget: () => group.model.dropTargetContainer?.model, }); this.onWillShowOverlay = this.dropTarget.onWillShowOverlay; @@ -121,6 +116,23 @@ export class Tab extends CompositeDisposable { this._onDropped, this._onDragStart, dragHandler.onDragStart((event) => { + if (event.dataTransfer) { + const style = getComputedStyle(this.element); + const newNode = this.element.cloneNode(true) as HTMLElement; + Array.from(style).forEach((key) => + newNode.style.setProperty( + key, + style.getPropertyValue(key), + style.getPropertyPriority(key) + ) + ); + newNode.style.position = 'absolute'; + + addGhostImage(event.dataTransfer, newNode, { + y: -10, + x: 30, + }); + } this._onDragStart.fire(event); }), dragHandler, diff --git a/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.scss b/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.scss index fef520e03..86777ec9e 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 { @@ -50,10 +50,20 @@ .dv-tab { -webkit-user-drag: element; outline: none; - min-width: 75px; + padding: 0.25rem 0.5rem; cursor: pointer; position: relative; box-sizing: border-box; + font-size: var(-dv-tab-font-size); + margin: var(--dv-tab-margin); + + &:first-child { + margin-right: 0; + } + + &:not(:nth-last-child(1)) { + margin-left: 0; + } &:not(:first-child)::before { content: ' '; diff --git a/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts b/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts index d3bd0568b..c6af57973 100644 --- a/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts +++ b/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts @@ -10,7 +10,10 @@ import { VoidContainer } from './voidContainer'; import { toggleClass } from '../../../dom'; import { DockviewPanel, IDockviewPanel } from '../../dockviewPanel'; import { DockviewComponent } from '../../dockviewComponent'; -import { WillShowOverlayLocationEvent } from '../../dockviewGroupPanelModel'; +import { + DockviewGroupPanelModel, + WillShowOverlayLocationEvent, +} from '../../dockviewGroupPanelModel'; import { getPanelData } from '../../../dnd/dataTransfer'; export interface TabDropIndexEvent { diff --git a/packages/dockview-core/src/dockview/components/titlebar/voidContainer.ts b/packages/dockview-core/src/dockview/components/titlebar/voidContainer.ts index 6e9ea0c47..29e31b9b6 100644 --- a/packages/dockview-core/src/dockview/components/titlebar/voidContainer.ts +++ b/packages/dockview-core/src/dockview/components/titlebar/voidContainer.ts @@ -1,4 +1,3 @@ -import { last } from '../../../array'; import { getPanelData } from '../../../dnd/dataTransfer'; import { Droptarget, @@ -10,6 +9,7 @@ import { DockviewComponent } from '../../dockviewComponent'; import { addDisposableListener, Emitter, Event } from '../../../events'; import { CompositeDisposable } from '../../../lifecycle'; import { DockviewGroupPanel } from '../../dockviewGroupPanel'; +import { DockviewGroupPanelModel } from '../../dockviewGroupPanelModel'; export class VoidContainer extends CompositeDisposable { private readonly _element: HTMLElement; @@ -54,16 +54,7 @@ export class VoidContainer extends CompositeDisposable { const data = getPanelData(); if (data && this.accessor.id === data.viewId) { - if ( - data.panelId === null && - data.groupId === this.group.id - ) { - // don't allow group move to drop on self - return false; - } - - // don't show the overlay if the tab being dragged is the last panel of this group - return last(this.group.panels)?.id !== data.panelId; + return true; } return group.model.canDisplayOverlay( @@ -72,6 +63,7 @@ export class VoidContainer extends CompositeDisposable { 'header_space' ); }, + getOverrideTarget: () => group.model.dropTargetContainer?.model, }); this.onWillShowOverlay = this.dropTraget.onWillShowOverlay; diff --git a/packages/dockview-core/src/dockview/dockviewComponent.ts b/packages/dockview-core/src/dockview/dockviewComponent.ts index 0dde93454..86465c015 100644 --- a/packages/dockview-core/src/dockview/dockviewComponent.ts +++ b/packages/dockview-core/src/dockview/dockviewComponent.ts @@ -54,6 +54,7 @@ import { Parameters } from '../panel/types'; import { Overlay } from '../overlay/overlay'; import { addTestId, + Classnames, getDockviewTheme, toggleClass, watchElementResize, @@ -74,6 +75,8 @@ import { } from '../overlay/overlayRenderContainer'; import { PopoutWindow } from '../popoutWindow'; import { StrictEventsSequencing } from './strictEventsSequencing'; +import { DropTargetAnchorContainer } from '../dnd/dropTargetAnchorContainer'; +import { DockviewTheme, themeAbyss } from './theme'; const DEFAULT_ROOT_OVERLAY_MODEL: DroptargetOverlayModel = { activationSize: { type: 'pixels', value: 10 }, @@ -191,6 +194,9 @@ export interface IDockviewComponent extends IBaseGrid { readonly totalPanels: number; readonly panels: IDockviewPanel[]; readonly orientation: Orientation; + /** + * @deprecated use `theme` instead. This will be removed in a future version + */ readonly gap: number; readonly onDidDrop: Event; readonly onWillDrop: Event; @@ -253,9 +259,11 @@ export class DockviewComponent private readonly _deserializer = new DefaultDockviewDeserialzier(this); private readonly _api: DockviewApi; private _options: Exclude; - private watermark: IWatermarkRenderer | null = null; + private _watermark: IWatermarkRenderer | null = null; + private readonly _themeClassnames: Classnames; readonly overlayRenderContainer: OverlayRenderContainer; + readonly rootDropTargetContainer: DropTargetAnchorContainer; private readonly _onWillDragPanel = new Emitter(); readonly onWillDragPanel: Event = this._onWillDragPanel.event; @@ -361,6 +369,9 @@ export class DockviewComponent } get gap(): number { + console.warn( + 'dockview: dockviewComponent.gap has been deprecated. Use `theme` instead. This will be removed in a future version.' + ); return this.gridview.margin; } @@ -377,10 +388,18 @@ export class DockviewComponent : undefined, disableAutoResizing: options.disableAutoResizing, locked: options.locked, - margin: options.gap, + margin: options.theme?.gap ?? 0, className: options.className, }); + this.updateDropTargetModel(options); + + this._themeClassnames = new Classnames(this.element); + + this.rootDropTargetContainer = new DropTargetAnchorContainer( + this.element, + { disabled: true } + ); this.overlayRenderContainer = new OverlayRenderContainer( this.gridview.element, this @@ -394,6 +413,7 @@ export class DockviewComponent } this.addDisposables( + this.rootDropTargetContainer, this.overlayRenderContainer, this._onWillDragPanel, this._onWillDragGroup, @@ -464,8 +484,10 @@ export class DockviewComponent ); this._options = options; + this.updateTheme(); this._rootDropTarget = new Droptarget(this.element, { + className: 'dv-drop-target-edge', canDisplayOverlay: (event, position) => { const data = getPanelData(); @@ -506,6 +528,7 @@ export class DockviewComponent acceptedTargetZones: ['top', 'bottom', 'left', 'right', 'center'], overlayModel: this.options.rootOverlayModel ?? DEFAULT_ROOT_OVERLAY_MODEL, + getOverrideTarget: () => this.rootDropTargetContainer?.model, }); this.addDisposables( @@ -756,6 +779,15 @@ export class DockviewComponent popoutContainer.appendChild(group.element); + const anchor = document.createElement('div'); + const dropTargetContainer = new DropTargetAnchorContainer( + anchor, + { disabled: this.rootDropTargetContainer.disabled } + ); + popoutContainer.appendChild(anchor); + + group.model.dropTargetContainer = dropTargetContainer; + group.model.location = { type: 'popout', getWindow: () => _window.window!, @@ -844,6 +876,8 @@ export class DockviewComponent } else if (this.getPanel(group.id)) { group.model.renderContainer = this.overlayRenderContainer; + group.model.dropTargetContainer = + this.rootDropTargetContainer; returnedGroup = group; const alreadyRemoved = !this._popoutGroups.find( @@ -1134,6 +1168,13 @@ export class DockviewComponent override updateOptions(options: Partial): void { super.updateOptions(options); + if ('gap' in options) { + console.warn( + 'dockview: dockviewComponent.setGap has been deprecated. Use `theme` instead. This will be removed in a future version.' + ); + this.gridview.margin = options.gap ?? 0; + } + if ('floatingGroupBounds' in options) { for (const group of this._floatingGroups) { switch (options.floatingGroupBounds) { @@ -1158,18 +1199,14 @@ export class DockviewComponent } } - if ('rootOverlayModel' in options) { - this._rootDropTarget.setOverlayModel( - options.rootOverlayModel ?? DEFAULT_ROOT_OVERLAY_MODEL - ); - } - - if ('gap' in options) { - this.gridview.margin = options.gap ?? 0; - } + this.updateDropTargetModel(options); this._options = { ...this.options, ...options }; + if ('theme' in options) { + this.updateTheme(); + } + this.layout(this.gridview.width, this.gridview.height, true); } @@ -1745,24 +1782,24 @@ export class DockviewComponent (x) => x.api.location.type === 'grid' && x.api.isVisible ).length === 0 ) { - if (!this.watermark) { - this.watermark = this.createWatermarkComponent(); + if (!this._watermark) { + this._watermark = this.createWatermarkComponent(); - this.watermark.init({ + this._watermark.init({ containerApi: new DockviewApi(this), }); const watermarkContainer = document.createElement('div'); watermarkContainer.className = 'dv-watermark-container'; addTestId(watermarkContainer, 'watermark-component'); - watermarkContainer.appendChild(this.watermark.element); + watermarkContainer.appendChild(this._watermark.element); this.gridview.element.appendChild(watermarkContainer); } - } else if (this.watermark) { - this.watermark.element.parentElement!.remove(); - this.watermark.dispose?.(); - this.watermark = null; + } else if (this._watermark) { + this._watermark.element.parentElement!.remove(); + this._watermark.dispose?.(); + this._watermark = null; } } @@ -2404,9 +2441,11 @@ export class DockviewComponent if (this._moving) { return; } + if (event.panel !== this.activePanel) { return; } + if (this._onDidActivePanelChange.value !== event.panel) { this._onDidActivePanelChange.fire(event.panel); } @@ -2489,4 +2528,44 @@ export class DockviewComponent ? rootOrientation : orthogonal(rootOrientation); } + + private updateDropTargetModel(options: Partial) { + if ('dndEdges' in options) { + this._rootDropTarget.disabled = + typeof options.dndEdges === 'boolean' && + options.dndEdges === false; + + if ( + typeof options.dndEdges === 'object' && + options.dndEdges !== null + ) { + this._rootDropTarget.setOverlayModel(options.dndEdges); + } else { + this._rootDropTarget.setOverlayModel( + DEFAULT_ROOT_OVERLAY_MODEL + ); + } + } + + if ('rootOverlayModel' in options) { + this.updateDropTargetModel({ dndEdges: options.dndEdges }); + } + } + + private updateTheme(): void { + const theme = this._options.theme ?? themeAbyss; + this._themeClassnames.setClassNames(theme.className); + + this.gridview.margin = theme.gap ?? 0; + + switch (theme.dndOverlayMounting) { + case 'absolute': + this.rootDropTargetContainer.disabled = false; + break; + case 'relative': + default: + this.rootDropTargetContainer.disabled = true; + break; + } + } } diff --git a/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts b/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts index a34d5ef10..c56babf91 100644 --- a/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts +++ b/packages/dockview-core/src/dockview/dockviewGroupPanelModel.ts @@ -39,6 +39,7 @@ import { import { OverlayRenderContainer } from '../overlay/overlayRenderContainer'; import { TitleEvent } from '../api/dockviewPanelApi'; import { Contraints } from '../gridview/gridviewPanel'; +import { DropTargetAnchorContainer } from '../dnd/dropTargetAnchorContainer'; interface GroupMoveEvent { groupId: string; @@ -265,6 +266,8 @@ export class DockviewGroupPanelModel private mostRecentlyUsed: IDockviewPanel[] = []; private _overwriteRenderContainer: OverlayRenderContainer | null = null; + private _overwriteDropTargetContainer: DropTargetAnchorContainer | null = + null; private readonly _onDidChange = new Emitter(); readonly onDidChange: Event = @@ -535,6 +538,17 @@ export class DockviewGroupPanelModel ); } + set dropTargetContainer(value: DropTargetAnchorContainer | null) { + this._overwriteDropTargetContainer = value; + } + + get dropTargetContainer(): DropTargetAnchorContainer | null { + return ( + this._overwriteDropTargetContainer ?? + this.accessor.rootDropTargetContainer + ); + } + initialize(): void { if (this.options.panels) { this.options.panels.forEach((panel) => { @@ -1049,6 +1063,29 @@ export class DockviewGroupPanelModel const data = getPanelData(); if (data && data.viewId === this.accessor.id) { + if (type === 'content') { + if (data.groupId === this.id) { + // don't allow to drop on self for center position + + if (position === 'center') { + return; + } + + if (data.panelId === null) { + // don't allow group move to drop anywhere on self + return; + } + } + } + + if (type === 'header') { + if (data.groupId === this.id) { + if (data.panelId === null) { + return; + } + } + } + if (data.panelId === null) { // this is a group move dnd event const { groupId } = data; diff --git a/packages/dockview-core/src/dockview/options.ts b/packages/dockview-core/src/dockview/options.ts index 3f7b94367..4b096c941 100644 --- a/packages/dockview-core/src/dockview/options.ts +++ b/packages/dockview-core/src/dockview/options.ts @@ -17,6 +17,7 @@ import { IGroupHeaderProps } from './framework'; import { FloatingGroupOptions } from './dockviewComponent'; import { Contraints } from '../gridview/gridviewPanel'; import { AcceptableEvent, IAcceptableEvent } from '../events'; +import { DockviewTheme } from './theme'; export interface IHeaderActionsRenderer extends IDisposable { readonly element: HTMLElement; @@ -51,19 +52,26 @@ export interface DockviewOptions { }; popoutUrl?: string; defaultRenderer?: DockviewPanelRenderer; - debug?: boolean; - rootOverlayModel?: DroptargetOverlayModel; - locked?: boolean; - disableDnd?: boolean; - className?: string; /** - * Pixel gap between groups + * @deprecated dockview: dockviewComponent.gap has been deprecated. Use `theme` instead. This will be removed in a future version. */ gap?: number; + debug?: boolean; + // #start dnd + dndEdges?: false | DroptargetOverlayModel; + /** + * @deprecated use `dndEdges` instead. To be removed in a future version. + * */ + rootOverlayModel?: DroptargetOverlayModel; + disableDnd?: boolean; + // #end dnd + locked?: boolean; + className?: string; /** * Define the behaviour of the dock when there are no panels to display. Defaults to `watermark`. */ noPanelsOverlay?: 'emptyGroup' | 'watermark'; + theme?: DockviewTheme; } export interface DockviewDndOverlayEvent extends IAcceptableEvent { @@ -106,9 +114,11 @@ export const PROPERTY_KEYS_DOCKVIEW: (keyof DockviewOptions)[] = (() => { rootOverlayModel: undefined, locked: undefined, disableDnd: undefined, - gap: undefined, className: undefined, noPanelsOverlay: undefined, + dndEdges: undefined, + theme: undefined, + gap: undefined, }; return Object.keys(properties) as (keyof DockviewOptions)[]; diff --git a/packages/dockview-core/src/dockview/theme.ts b/packages/dockview-core/src/dockview/theme.ts new file mode 100644 index 000000000..4a921e2db --- /dev/null +++ b/packages/dockview-core/src/dockview/theme.ts @@ -0,0 +1,54 @@ +export interface DockviewTheme { + name: string; + className: string; + gap?: number; + dndOverlayMounting?: 'absolute' | 'relative'; + includeHeaderWhenHoverOverContent?: boolean; +} + +export const themeDark: DockviewTheme = { + name: 'dark', + className: 'dockview-theme-dark', +}; + +export const themeLight: DockviewTheme = { + name: 'light', + className: 'dockview-theme-light', +}; + +export const themeVisualStudio: DockviewTheme = { + name: 'visualStudio', + className: 'dockview-theme-vs', +}; + +export const themeAbyss: DockviewTheme = { + name: 'abyss', + className: 'dockview-theme-abyss', +}; + +export const themeDracula: DockviewTheme = { + name: 'dracula', + className: 'dockview-theme-dracula', +}; + +export const themeReplit: DockviewTheme = { + name: 'replit', + className: 'dockview-theme-replit', + gap: 10, +}; + +export const themeAbyssSpaced: DockviewTheme = { + name: 'abyssSpaced', + className: 'dockview-theme-abyss-spaced', + gap: 10, + dndOverlayMounting: 'absolute', + includeHeaderWhenHoverOverContent: true, +}; + +export const themeLightSpaced: DockviewTheme = { + name: 'lightSpaced', + className: 'dockview-theme-light-spaced', + gap: 10, + dndOverlayMounting: 'absolute', + includeHeaderWhenHoverOverContent: true, +}; diff --git a/packages/dockview-core/src/index.ts b/packages/dockview-core/src/index.ts index 9764f658a..d9f66a787 100644 --- a/packages/dockview-core/src/index.ts +++ b/packages/dockview-core/src/index.ts @@ -64,6 +64,7 @@ export { } from './dockview/framework'; export * from './dockview/options'; +export * from './dockview/theme'; export * from './dockview/dockviewPanel'; export { DefaultTab } from './dockview/components/tab/defaultTab'; export { diff --git a/packages/dockview-core/src/splitview/splitview.scss b/packages/dockview-core/src/splitview/splitview.scss index adf09b368..047b382cf 100644 --- a/packages/dockview-core/src/splitview/splitview.scss +++ b/packages/dockview-core/src/splitview/splitview.scss @@ -116,6 +116,7 @@ -moz-user-select: none; // Firefox -ms-user-select: none; // IE 10 and IE 11 touch-action: none; + background-color: var(--dv-sash-color, transparent); &:not(.disabled):active { transition: background-color 0.1s ease-in-out; diff --git a/packages/dockview-core/src/splitview/splitview.ts b/packages/dockview-core/src/splitview/splitview.ts index 77fbb9cf5..c511d54f6 100644 --- a/packages/dockview-core/src/splitview/splitview.ts +++ b/packages/dockview-core/src/splitview/splitview.ts @@ -219,6 +219,8 @@ export class Splitview { set margin(value: number) { this._margin = value; + + toggleClass(this.element, 'dv-splitview-has-margin', value !== 0); } constructor( diff --git a/packages/dockview-core/src/theme.scss b/packages/dockview-core/src/theme.scss index 5eb3f7442..ee28b9565 100644 --- a/packages/dockview-core/src/theme.scss +++ b/packages/dockview-core/src/theme.scss @@ -1,17 +1,29 @@ +@import 'theme/_sash-handle-mixin'; +@import 'theme/_drop-target-static-mixin'; +@import 'theme/_space-mixin'; + @mixin dockview-theme-core-mixin { --dv-paneview-active-outline-color: dodgerblue; --dv-tabs-and-actions-container-font-size: 13px; --dv-tabs-and-actions-container-height: 35px; --dv-drag-over-background-color: rgba(83, 89, 93, 0.5); - --dv-drag-over-border-color: white; + --dv-drag-over-border-color: transparent; --dv-tabs-container-scrollbar-color: #888; --dv-icon-hover-background-color: rgba(90, 93, 94, 0.31); --dv-floating-box-shadow: 8px 8px 8px 0px rgba(83, 89, 93, 0.5); --dv-overlay-z-index: 999; + // + + --dv-tab-font-size: inherit; + --dv-border-radius: 0px; + --dv-tab-margin: 0; + --dv-sash-color: transparent; + --dv-active-sash-color: transparent; } @mixin dockview-theme-dark-mixin { @include dockview-theme-core-mixin(); + @include dockview-drop-target-no-travel(); // --dv-group-view-background-color: #1e1e1e; @@ -35,6 +47,8 @@ @mixin dockview-theme-light-mixin { @include dockview-theme-core-mixin(); + @include dockview-drop-target-no-travel(); + // --dv-group-view-background-color: white; // @@ -131,30 +145,49 @@ @mixin dockview-theme-abyss-mixin { @include dockview-theme-core-mixin(); + @include dockview-drop-target-no-travel(); + + --dv-color-abyss-dark: #000c18; + --dv-color-abyss: #10192c; + --dv-color-abyss-light: #1c1c2a; + --dv-color-abyss-lighter: #2b2b4a; + --dv-color-abyss-accent: rgb(91, 30, 207); + + --dv-color-abyss-primary-text: white; + --dv-color-abyss-secondary-text: rgb(148, 151, 169); + // - --dv-group-view-background-color: #000c18; + --dv-group-view-background-color: var(--dv-color-abyss-dark); // - --dv-tabs-and-actions-container-background-color: #1c1c2a; + --dv-tabs-and-actions-container-background-color: var( + --dv-color-abyss-light + ); // - --dv-activegroup-visiblepanel-tab-background-color: #000c18; - --dv-activegroup-hiddenpanel-tab-background-color: #10192c; - --dv-inactivegroup-visiblepanel-tab-background-color: #000c18; - --dv-inactivegroup-hiddenpanel-tab-background-color: #10192c; - --dv-tab-divider-color: #2b2b4a; + --dv-activegroup-visiblepanel-tab-background-color: var( + --dv-color-abyss-dark + ); + --dv-activegroup-hiddenpanel-tab-background-color: var(--dv-color-abyss); + --dv-inactivegroup-visiblepanel-tab-background-color: var( + --dv-color-abyss-dark + ); + --dv-inactivegroup-hiddenpanel-tab-background-color: var(--dv-color-abyss); + --dv-tab-divider-color: var(--dv-color-abyss-lighter); // --dv-activegroup-visiblepanel-tab-color: white; --dv-activegroup-hiddenpanel-tab-color: rgba(255, 255, 255, 0.5); --dv-inactivegroup-visiblepanel-tab-color: rgba(255, 255, 255, 0.5); --dv-inactivegroup-hiddenpanel-tab-color: rgba(255, 255, 255, 0.25); // - --dv-separator-border: #2b2b4a; - --dv-paneview-header-border-color: #2b2b4a; + --dv-separator-border: var(--dv-color-abyss-lighter); + --dv-paneview-header-border-color: var(--dv-color-abyss-lighter); --dv-paneview-active-outline-color: #596f99; } @mixin dockview-theme-dracula-mixin { @include dockview-theme-core-mixin(); + @include dockview-drop-target-no-travel(); + // --dv-group-view-background-color: #282a36; // @@ -229,10 +262,17 @@ } @mixin dockview-design-replit-mixin { + @include dockview-drop-target-no-travel(); + .dv-resize-container:has(> .dv-groupview) { border-radius: 8px; } + .dv-resize-container { + border-radius: 10px !important; + border: none; + } + .dv-groupview { overflow: hidden; border-radius: 10px; @@ -266,59 +306,16 @@ border: 1px solid transparent; } } - - .dv-vertical > .dv-sash-container > .dv-sash { - &:not(.disabled) { - &::after { - content: ''; - height: 4px; - width: 40px; - border-radius: 2px; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - background-color: var(--dv-separator-handle-background-color); - position: absolute; - } - - &:hover { - &::after { - background-color: var( - --dv-separator-handle-hover-background-color - ); - } - } - } - } - - .dv-horizontal > .dv-sash-container > .dv-sash { - &:not(.disabled) { - &::after { - content: ''; - height: 40px; - width: 4px; - border-radius: 2px; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - background-color: var(--dv-separator-handle-background-color); - position: absolute; - } - - &:hover { - &::after { - background-color: var( - --dv-separator-handle-hover-background-color - ); - } - } - } - } } .dockview-theme-replit { @include dockview-theme-core-mixin(); @include dockview-design-replit-mixin(); + @include dockview-design-handle-mixin(); + + padding: 10px; + background-color: #ebeced; + // --dv-group-view-background-color: #ebeced; // @@ -339,6 +336,115 @@ --dv-paneview-header-border-color: rgb(51, 51, 51); ///// - --dv-separator-handle-background-color: #cfd1d3; - --dv-separator-handle-hover-background-color: #babbbb; + --dv-sash-color: #cfd1d3; + --dv-active-sash-color: #babbbb; +} + +.dockview-theme-abyss-spaced { + @include dockview-theme-core-mixin(); + @include dockview-design-space-mixin(); + + // stylesheet + --dv-color-abyss-dark: rgb(11, 6, 17); + --dv-color-abyss: #16121f; + --dv-color-abyss-light: #201d2b; + --dv-color-abyss-lighter: #2a2837; + --dv-color-abyss-accent: rgb(91, 30, 207); + --dv-color-abyss-primary-text: white; + --dv-color-abyss-secondary-text: rgb(148, 151, 169); + + // + --dv-drag-over-border: 2px solid var(--dv-color-abyss-accent); + --dv-drag-over-background-color: ''; + // + + // + --dv-group-view-background-color: var(--dv-color-abyss-dark); + // + --dv-tabs-and-actions-container-background-color: var(--dv-color-abyss); + // + --dv-activegroup-visiblepanel-tab-background-color: var( + --dv-color-abyss-lighter + ); + --dv-activegroup-hiddenpanel-tab-background-color: var( + --dv-color-abyss-light + ); + --dv-inactivegroup-visiblepanel-tab-background-color: var( + --dv-color-abyss-lighter + ); + --dv-inactivegroup-hiddenpanel-tab-background-color: var( + --dv-color-abyss-light + ); + --dv-tab-divider-color: transparent; + // + --dv-activegroup-visiblepanel-tab-color: var(--dv-color-abyss-primary-text); + --dv-activegroup-hiddenpanel-tab-color: var( + --dv-color-abyss-secondary-text + ); + --dv-inactivegroup-visiblepanel-tab-color: var( + --dv-color-abyss-primary-text + ); + --dv-inactivegroup-hiddenpanel-tab-color: var( + --dv-color-abyss-secondary-text + ); + // + --dv-separator-border: transparent; + --dv-paneview-header-border-color: rgb(51, 51, 51); + + ///// + --dv-active-sash-color: var(--dv-color-abyss-accent); + // + --dv-floating-box-shadow: 8px 8px 8px 0px rgba(0, 0, 0, 0.5); + + padding: 10px; + background-color: var(--dv-color-abyss-dark); + + .dv-resize-container { + .dv-groupview { + border: 2px solid var(--dv-color-abyss-dark); + } + } +} + +.dockview-theme-light-spaced { + @include dockview-theme-core-mixin(); + @include dockview-design-space-mixin(); + + // + --dv-drag-over-border: 2px solid rgb(91, 30, 207); + --dv-drag-over-background-color: ''; + // + + // + --dv-group-view-background-color: #f6f5f9; + // + --dv-tabs-and-actions-container-background-color: white; + // + --dv-activegroup-visiblepanel-tab-background-color: #ededf0; + --dv-activegroup-hiddenpanel-tab-background-color: #f9f9fa; + --dv-inactivegroup-visiblepanel-tab-background-color: #ededf0; + --dv-inactivegroup-hiddenpanel-tab-background-color: #f9f9fa; + --dv-tab-divider-color: transparent; + // + --dv-activegroup-visiblepanel-tab-color: rgb(104, 107, 130); + --dv-activegroup-hiddenpanel-tab-color: rgb(148, 151, 169); + --dv-inactivegroup-visiblepanel-tab-color: rgb(104, 107, 130); + --dv-inactivegroup-hiddenpanel-tab-color: rgb(148, 151, 169); + // + --dv-separator-border: transparent; + --dv-paneview-header-border-color: rgb(51, 51, 51); + + ///// + --dv-active-sash-color: rgb(91, 30, 207); + // + --dv-floating-box-shadow: 8px 8px 8px 0px rgba(0, 0, 0, 0.1); + + padding: 10px; + background-color: #f6f5f9; + + .dv-resize-container { + .dv-groupview { + border: 2px solid rgb(255, 255, 255, 0.1); + } + } } diff --git a/packages/dockview-core/src/theme/_drop-target-static-mixin.scss b/packages/dockview-core/src/theme/_drop-target-static-mixin.scss new file mode 100644 index 000000000..b2e0e4ba7 --- /dev/null +++ b/packages/dockview-core/src/theme/_drop-target-static-mixin.scss @@ -0,0 +1,10 @@ +@mixin dockview-drop-target-no-travel { + .dv-drop-target-container { + .dv-drop-target-anchor { + &.dv-drop-target-anchor-container-changed { + opacity: 0; + transition: none; + } + } + } +} diff --git a/packages/dockview-core/src/theme/_sash-handle-mixin.scss b/packages/dockview-core/src/theme/_sash-handle-mixin.scss new file mode 100644 index 000000000..31e5822da --- /dev/null +++ b/packages/dockview-core/src/theme/_sash-handle-mixin.scss @@ -0,0 +1,53 @@ +@mixin dockview-design-handle-mixin { + .dv-vertical > .dv-sash-container > .dv-sash { + background-color: transparent; + + &:not(.disabled) { + &::after { + content: ''; + height: 4px; + width: 40px; + border-radius: 2px; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background-color: var(--dv-sash-color); + position: absolute; + } + + &:hover, + &:active { + background-color: transparent; + &::after { + background-color: var(--dv-active-sash-color); + } + } + } + } + + .dv-horizontal > .dv-sash-container > .dv-sash { + background-color: transparent; + + &:not(.disabled) { + &::after { + content: ''; + height: 40px; + width: 4px; + border-radius: 2px; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background-color: var(--dv-sash-color); + position: absolute; + } + + &:hover, + &:active { + background-color: transparent; + &::after { + background-color: var(--dv-active-sash-color); + } + } + } + } +} diff --git a/packages/dockview-core/src/theme/_space-mixin.scss b/packages/dockview-core/src/theme/_space-mixin.scss new file mode 100644 index 000000000..b2e84cdbd --- /dev/null +++ b/packages/dockview-core/src/theme/_space-mixin.scss @@ -0,0 +1,52 @@ +@mixin dockview-design-space-mixin { + --dv-tab-font-size: 12px; + --dv-border-radius: 20px; + --dv-tab-margin: 0.5rem 0.25rem; + --dv-tabs-and-actions-container-height: 44px; + + + --dv-border-radius + + .dv-resize-container:has(> .dv-groupview) { + border-radius: 8px; + } + + .dv-sash { + border-radius: 4px; + } + + .dv-drop-target-anchor { + border-radius: calc(var(--dv-border-radius) / 4); + &.dv-drop-target-content { + border-radius: var(--dv-border-radius); + } + } + + .dv-resize-container { + border-radius: var(--dv-border-radius) !important; + border: none; + } + + .dv-groupview { + border-radius: var(--dv-border-radius); + + .dv-tabs-and-actions-container { + padding: 0px calc(var(--dv-border-radius) / 2); + + .dv-tab { + border-radius: 8px; + + .dv-svg { + height: 8px; + width: 8px; + } + } + } + + .dv-content-container { + background-color: var( + --dv-tabs-and-actions-container-background-color + ); + } + } +} diff --git a/packages/dockview/src/svg.tsx b/packages/dockview/src/svg.tsx index 76143411a..eccf52bb6 100644 --- a/packages/dockview/src/svg.tsx +++ b/packages/dockview/src/svg.tsx @@ -7,7 +7,7 @@ export const CloseButton = () => ( viewBox="0 0 28 28" aria-hidden={'false'} focusable={false} - className="dockview-svg" + className="dv-svg" > @@ -21,7 +21,7 @@ export const ExpandMore = () => { viewBox="0 0 24 15" aria-hidden={'false'} focusable={false} - className="dockview-svg" + className="dv-svg" > diff --git a/packages/docs/docs/overview/getStarted/theme.mdx b/packages/docs/docs/overview/getStarted/theme.mdx index 2e89eefe6..6240340b9 100644 --- a/packages/docs/docs/overview/getStarted/theme.mdx +++ b/packages/docs/docs/overview/getStarted/theme.mdx @@ -7,7 +7,9 @@ title: Theme import { CSSVariablesTable, ThemeTable } from '@site/src/components/cssVariables'; -Theming is controlled through CSS and is highly customizable. + +Dockview components accept a `theme` property which is highly customizable, the theme is largly controlled through CSS however some properties can only be adjusted +by direct editing variables of the `theme` object. Firstly, you should import `dockview.css`: @@ -38,7 +40,7 @@ To use a `dockview` theme the CSS must encapsulate the component. The current li :::info -The source code for all themes can be found [here](https://github.com/mathuo/dockview/blob/master/packages/dockview-core/src/theme.scss). +The source code for all themes can be found [here](https://github.com/mathuo/dockview/blob/master/packages/dockview-core/src/theme.scss) and the associated CSS [here](https://github.com/mathuo/dockview/blob/master/packages/dockview-core/src/theme.scss). ::: ## Customizing Theme diff --git a/packages/docs/sandboxes/react/dockview/demo-dockview/src/app.scss b/packages/docs/sandboxes/react/dockview/demo-dockview/src/app.scss index 57549c075..2f7a940fc 100644 --- a/packages/docs/sandboxes/react/dockview/demo-dockview/src/app.scss +++ b/packages/docs/sandboxes/react/dockview/demo-dockview/src/app.scss @@ -11,6 +11,7 @@ &:hover { border-radius: 2px; + color: var(--dv-activegroup-visiblepanel-tab-color); background-color: var(--dv-icon-hover-background-color); } } diff --git a/packages/docs/sandboxes/react/dockview/demo-dockview/src/app.tsx b/packages/docs/sandboxes/react/dockview/demo-dockview/src/app.tsx index e1ccaef0f..a69632ebf 100644 --- a/packages/docs/sandboxes/react/dockview/demo-dockview/src/app.tsx +++ b/packages/docs/sandboxes/react/dockview/demo-dockview/src/app.tsx @@ -5,6 +5,7 @@ import { IDockviewPanelHeaderProps, IDockviewPanelProps, DockviewApi, + DockviewTheme, } from 'dockview'; import * as React from 'react'; import './app.scss'; @@ -80,6 +81,7 @@ const components = { ); }, nested: (props: IDockviewPanelProps) => { + const theme = React.useContext(ThemeContext); return ( ); }, @@ -141,7 +143,9 @@ const WatermarkComponent = () => { return
custom watermark
; }; -const DockviewDemo = (props: { theme?: string }) => { +const ThemeContext = React.createContext(undefined); + +const DockviewDemo = (props: { theme?: DockviewTheme }) => { const [logLines, setLogLines] = React.useState< { text: string; timestamp?: Date; backgroundColor?: string }[] >([]); @@ -380,18 +384,22 @@ const DockviewDemo = (props: { theme?: string }) => { }} > - + + + 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 63032b5f4..c9fd5e19f 100644 --- a/packages/docs/sandboxes/react/dockview/demo-dockview/src/controls.tsx +++ b/packages/docs/sandboxes/react/dockview/demo-dockview/src/controls.tsx @@ -81,7 +81,7 @@ export const RightControls = (props: IDockviewHeaderActionsProps) => { alignItems: 'center', padding: '0px 8px', height: '100%', - color: 'var(--dv-activegroup-visiblepanel-tab-color)', + color: 'var(--dv-activegroup-hiddenpanel-tab-color)', }} > {props.isGroupActive && } diff --git a/packages/docs/sandboxes/react/dockview/demo-dockview/src/gridActions.tsx b/packages/docs/sandboxes/react/dockview/demo-dockview/src/gridActions.tsx index 40e57b2fa..ec16135ed 100644 --- a/packages/docs/sandboxes/react/dockview/demo-dockview/src/gridActions.tsx +++ b/packages/docs/sandboxes/react/dockview/demo-dockview/src/gridActions.tsx @@ -151,11 +151,20 @@ export const GridActions = (props: { props.api?.addGroup(); }; - const [gap, setGap] = React.useState(0); + // const [gap, setGap] = React.useState(undefined); - React.useEffect(() => { - props.api?.setGap(gap); - }, [gap, props.api]); + const [overlayMode, setOverlayMode] = React.useState(false); + + // React.useEffect(() => { + // if (!props.api) { + // return; + // } + // if (typeof gap === 'number') { + // props.api.setGap(gap); + // } else { + // setGap(props.api.gap); + // } + // }, [gap, props.api]); return (
@@ -191,6 +200,23 @@ export const GridActions = (props: { Use Custom Watermark + {/* + + */} @@ -204,7 +230,7 @@ export const GridActions = (props: { Reset -
+ {/*
Grid Gap setGap(Number(event.target.value))} /> -
+
*/}
); }; diff --git a/packages/docs/src/components/frameworkSpecific.css b/packages/docs/src/components/frameworkSpecific.css index a92d63d29..9a03811a8 100644 --- a/packages/docs/src/components/frameworkSpecific.css +++ b/packages/docs/src/components/frameworkSpecific.css @@ -1,13 +1,16 @@ .DropdownMenuContent { /* min-width: 220px; */ - background-color: rgba(255, 255, 255, 0.1); + background-color: var(--ifm-dropdown-background-color); + color: var(--ifm-color-primary); + border: var(--ifm-dropdown-border); border-radius: 6px; padding: 5px; box-shadow: 0px 10px 38px -10px rgba(22, 23, 24, 0.35), 0px 10px 20px -15px rgba(22, 23, 24, 0.2); animation-duration: 400ms; animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1); will-change: transform, opacity; + z-index: 99; } .DropdownMenuContent[data-side='top'], .DropdownMenuSubContent[data-side='top'] { @@ -39,25 +42,33 @@ display: flex; align-items: center; justify-content: space-between; - width: 100px; - height: 25px; + width: 120px; + /* height: 25px; */ padding: 4px 8px; - font-size: 13px; + font-size: 1rem; + font-weight: normal; cursor: pointer; + + color: var(--ifm-menu-color); + + &:hover { + background-color: var(--ifm-hover-overlay); + } } .framework-menu-item-select { display: flex; align-items: center; justify-content: space-between; - width: 120px; + width: 130px; height: 35px; padding: 4px 8px; border-radius: 6px; - font-size: 13px; - background-color: rgba(255, 255, 255, 0.1); + font-size: 1rem; + font-weight: normal; cursor: pointer; - border: 1px solid rgba(0,0,0, 0.1); + + border: 1px solid rgba(60, 60, 66,0.5); } @keyframes slideUpAndFade { diff --git a/packages/docs/src/components/frameworkSpecific.tsx b/packages/docs/src/components/frameworkSpecific.tsx index 65ae1d549..14d8ea830 100644 --- a/packages/docs/src/components/frameworkSpecific.tsx +++ b/packages/docs/src/components/frameworkSpecific.tsx @@ -1,6 +1,8 @@ import BrowserOnly from '@docusaurus/BrowserOnly'; import { DockviewEmitter } from 'dockview'; import * as React from 'react'; +import * as DropdownMenu from '@radix-ui/react-dropdown-menu'; +import useBaseUrl from '@docusaurus/useBaseUrl'; import './frameworkSpecific.css'; export interface FrameworkDescriptor { @@ -51,8 +53,7 @@ export function useActiveFramework(): [ return [option, setter]; } -import * as DropdownMenu from '@radix-ui/react-dropdown-menu'; -import useBaseUrl from '@docusaurus/useBaseUrl'; + const FrameworkSelector1 = () => { const [activeFramework, setActiveFramework] = useActiveFramework(); diff --git a/packages/docs/src/components/ui/codeSandboxButton.scss b/packages/docs/src/components/ui/codeSandboxButton.scss index 8c31be3b8..5fda1291b 100644 --- a/packages/docs/src/components/ui/codeSandboxButton.scss +++ b/packages/docs/src/components/ui/codeSandboxButton.scss @@ -28,7 +28,7 @@ } } -.dockview-svg { +.dv-svg { display: inline-block; fill: currentcolor; line-height: 1; diff --git a/packages/docs/src/components/ui/codeSandboxButton.tsx b/packages/docs/src/components/ui/codeSandboxButton.tsx index a68aa34d7..c2c6e02ff 100644 --- a/packages/docs/src/components/ui/codeSandboxButton.tsx +++ b/packages/docs/src/components/ui/codeSandboxButton.tsx @@ -17,7 +17,7 @@ const createSvgElementFromPath = (params: { width={params.width} viewBox={params.viewbox} focusable={false} - className={'dockview-svg'} + className={'dv-svg'} > @@ -54,7 +54,7 @@ export const CodeSandboxButton = (props: { { return ( { const JavascriptIcon = (props: { height: number; width: number }) => { return ( { return (
{ diff --git a/packages/docs/src/config/theme.config.ts b/packages/docs/src/config/theme.config.ts index 2e8b9741f..0946720df 100644 --- a/packages/docs/src/config/theme.config.ts +++ b/packages/docs/src/config/theme.config.ts @@ -1,33 +1,54 @@ +import { + themeAbyss, + themeDark, + themeDracula, + themeAbyssSpaced, + themeLightSpaced, + themeLight, + themeReplit, + themeVisualStudio, +} from 'dockview'; + export const themeConfig = [ { - id: 'dockview-theme-dark', + id: themeDark, key: '**[dockview-theme-dark](/demo?theme=dockview-theme-dark)**', text: '', }, { - id: 'dockview-theme-light', + id: themeLight, key: '**[dockview-theme-light](/demo?theme=dockview-theme-light)**', text: '', }, { - id: 'dockview-theme-vs', + id: themeVisualStudio, key: '**[dockview-theme-vs](/demo?theme=dockview-theme-vs)**', text: 'Based on [Visual Studio](https://visualstudio.microsoft.com)', }, { - id: 'dockview-theme-abyss', + id: themeAbyss, key: '**[dockview-theme-abyss](/demo?theme=dockview-theme-abyss)**', text: 'Based on [Visual Studio Code](https://code.visualstudio.com/docs/getstarted/themes) abyss theme', }, { - id: 'dockview-theme-dracula', + id: themeDracula, key: '**[dockview-theme-dracula](/demo?theme=dockview-theme-dracula)**', text: 'Based on [Visual Studio Code](https://code.visualstudio.com/docs/getstarted/themes) dracula theme', }, { - id: 'dockview-theme-replit', + id: themeReplit, key: '**[dockview-theme-replit](/demo?theme=dockview-theme-replit)**', text: 'Based on [Replit](https://replit.com)', }, + { + id: themeLightSpaced, + key: '**[dockview-theme-replit](/demo?theme=dockview-theme-kraken)**', + text: '', + }, + { + id: themeAbyssSpaced, + key: '**[dockview-theme-replit](/demo?theme=dockview-theme-kraken)**', + text: '', + }, ]; diff --git a/packages/docs/src/css/custom.scss b/packages/docs/src/css/custom.scss index 9d446add3..0ab665892 100644 --- a/packages/docs/src/css/custom.scss +++ b/packages/docs/src/css/custom.scss @@ -11,10 +11,10 @@ /* You can override the default Infima variables here. */ :root { - --ifm-font-family-base: "IBM Plex Sans", ui-sans-serif, system-ui, -apple-system, - BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, - sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, - Noto Color Emoji; + --ifm-font-family-base: 'IBM Plex Sans', ui-sans-serif, system-ui, + -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, + Arial, Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, + Segoe UI Symbol, Noto Color Emoji; --ifm-font-weight-bold: 600; @@ -36,6 +36,9 @@ --ifm-color-primary: black; + --ifm-dropdown-background-color: white; + --ifm-dropdown-border: 1px solid var(--ifm-color-primary-darkest); + --ifm-navbar-link-color: white; --ifm-navbar-link-hover-color: white; @@ -54,15 +57,18 @@ } /* --ifm-color-primary: #0c111d; */ - --ifm-color-primary: #25c2a0; - --ifm-color-primary-dark: #21af90; - --ifm-color-primary-darker: #1fa588; - --ifm-color-primary-darkest: #1a8870; - --ifm-color-primary-light: #29d5b0; - --ifm-color-primary-lighter: #32d8b4; - --ifm-color-primary-lightest: #4fddbf; + --ifm-color-primary: #98a2b3; + --ifm-color-primary-dark: #828a99; + --ifm-color-primary-darker: #6a707c; + --ifm-color-primary-darkest: #474b53; + --ifm-color-primary-light: #acb7ca; + --ifm-color-primary-lighter: #bcc9df; + --ifm-color-primary-lightest: #d2e1fa; --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3); + --ifm-dropdown-background-color: #373d4b; + --ifm-dropdown-border: 1px solid var(--ifm-color-primary-darkest); + --dv-docs-markdown-text-color: #cdced8; } diff --git a/packages/docs/src/pages/demo.tsx b/packages/docs/src/pages/demo.tsx index cab33d7ef..d04d76f60 100644 --- a/packages/docs/src/pages/demo.tsx +++ b/packages/docs/src/pages/demo.tsx @@ -3,11 +3,29 @@ import Layout from '@theme/Layout'; import { themeConfig } from '../config/theme.config'; import ExampleFrame from '../components/ui/exampleFrame'; import BrowserOnly from '@docusaurus/BrowserOnly'; +import { DockviewTheme, themeAbyss } from 'dockview'; + +const updateTheme = (theme: DockviewTheme) => { + const urlParams = new URLSearchParams(window.location.search); + + urlParams.set('theme', theme.name); + + const newUrl = window.location.pathname + '?' + urlParams.toString(); + + window.history.pushState({ path: newUrl }, '', newUrl); +}; const ThemeToggle: React.FC = () => { - const [theme, setTheme] = React.useState( - new URLSearchParams(location.search).get('theme') ?? themeConfig[3].id - ); + const [theme, setTheme] = React.useState(themeAbyss); + + React.useEffect(() => { + const urlParams = new URLSearchParams(window.location.search); + const themeName = urlParams.get('theme'); + const newTheme = + themeConfig.find((c) => c.id.name === themeName)?.id ?? themeAbyss; + setTheme(newTheme); + updateTheme(newTheme); + }, []); return ( <> @@ -16,20 +34,48 @@ const ThemeToggle: React.FC = () => { height: '40px', display: 'flex', alignItems: 'center', + padding: '0px 15px', }} > - { - const url = new URL(window.location.href); - url.searchParams.set('theme', event.target.value); - window.location.href = url.toString(); + const theme = themeConfig.find( + (theme) => theme.id.name === event.target.value + ).id; + setTheme(theme); + updateTheme(theme); }} - value={theme} + value={theme.name} > {themeConfig.map((theme) => { - return ; + return ( + + ); })} - + */}
); } + +import { + DropdownMenu, + DropdownMenuItem, + DropdownMenuContent, + DropdownMenuTrigger, +} from '@radix-ui/react-dropdown-menu'; + +const ThemeSelector = (props: { + options: string[]; + value: string; + onChanged: (value: string) => void; +}) => { + const ref = React.useRef(null); + + return ( +
+ { + if (!open) { + return; + } + + if (!ref.current) { + return; + } + + requestAnimationFrame(() => { + const el = ref.current!.querySelector( + `[data-dropdown-menu-value="${props.value}"]` + ); + if (el) { + (el as HTMLElement).focus(); + } + }); + }} + > + +
+ {props.value} +
+
+ + {props.options.map((option) => { + return ( + props.onChanged(option)} + className="DropdownMenuItem" + > +
+ {option} + + {option === props.value ? '✓' : ''} + +
+
+ ); + })} +
+
+
+ ); +}; From 6511b8d9362e0c56b4384eafe3fb5e2417b8cdff Mon Sep 17 00:00:00 2001 From: Hieu Date: Mon, 24 Feb 2025 03:03:00 +0700 Subject: [PATCH 04/40] Make addGroup wiithout reference panel/group allow id option --- .../src/dockview/dockviewComponent.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/dockview-core/src/dockview/dockviewComponent.ts b/packages/dockview-core/src/dockview/dockviewComponent.ts index 20343ae6c..9c8484df7 100644 --- a/packages/dockview-core/src/dockview/dockviewComponent.ts +++ b/packages/dockview-core/src/dockview/dockviewComponent.ts @@ -1100,7 +1100,7 @@ export class DockviewComponent this.updateWatermark(); } - private orthogonalize(position: Position): DockviewGroupPanel { + private orthogonalize(position: Position, options?: GroupOptions): DockviewGroupPanel { switch (position) { case 'top': case 'bottom': @@ -1126,10 +1126,10 @@ export class DockviewComponent case 'top': case 'left': case 'center': - return this.createGroupAtLocation([0]); // insert into first position + return this.createGroupAtLocation([0], undefined, options); // insert into first position case 'bottom': case 'right': - return this.createGroupAtLocation([this.gridview.length]); // insert into last position + return this.createGroupAtLocation([this.gridview.length], undefined, options); // insert into last position default: throw new Error(`unsupported position ${position}`); } @@ -1808,7 +1808,8 @@ export class DockviewComponent } } else { const group = this.orthogonalize( - directionToPosition(options.direction) + directionToPosition(options.direction), + options ); if (!options.skipSetActive) { this.doSetGroupAndPanelActive(group); @@ -2474,9 +2475,10 @@ export class DockviewComponent private createGroupAtLocation( location: number[], - size?: number + size?: number, + options?: GroupOptions ): DockviewGroupPanel { - const group = this.createGroup(); + const group = this.createGroup(options); this.doAddGroup(group, location, size); return group; } From 7a6b2cb26da3e6349a3e060c27d7d847f6834ebe Mon Sep 17 00:00:00 2001 From: mathuo <6710312+mathuo@users.noreply.github.com> Date: Tue, 25 Feb 2025 21:05:38 +0000 Subject: [PATCH 05/40] feat: tab panel overflow dropdown --- .../__mocks__/mockDockviewPanelModel.ts | 8 +- .../dockview/dockviewComponent.spec.ts | 6 +- .../dockview/dockviewGroupPanelModel.spec.ts | 7 +- .../dockview/components/titlebar/tabs.scss | 158 ++++++++----- .../components/titlebar/{tabs.tsx => tabs.ts} | 214 ++++++++++++++---- .../components/titlebar/tabsContainer.ts | 5 + .../src/dockview/dockviewPanelModel.ts | 30 ++- .../dockview-core/src/dockview/framework.ts | 4 +- packages/dockview-core/src/dockview/types.ts | 7 +- packages/dockview-core/src/dom.ts | 22 ++ .../src/gridview/baseComponentGridview.ts | 3 + .../dockview-core/src/theme/_space-mixin.scss | 27 ++- packages/dockview-vue/src/utils.ts | 5 +- packages/dockview/src/dockview/defaultTab.tsx | 3 +- .../dockview/src/dockview/reactHeaderPart.ts | 11 +- 15 files changed, 360 insertions(+), 150 deletions(-) rename packages/dockview-core/src/dockview/components/titlebar/{tabs.tsx => tabs.ts} (53%) diff --git a/packages/dockview-core/src/__tests__/__mocks__/mockDockviewPanelModel.ts b/packages/dockview-core/src/__tests__/__mocks__/mockDockviewPanelModel.ts index 79e21b88a..35a301025 100644 --- a/packages/dockview-core/src/__tests__/__mocks__/mockDockviewPanelModel.ts +++ b/packages/dockview-core/src/__tests__/__mocks__/mockDockviewPanelModel.ts @@ -1,11 +1,12 @@ import { IDockviewPanelModel } from '../../dockview/dockviewPanelModel'; import { DockviewGroupPanel } from '../../dockview/dockviewGroupPanel'; import { - GroupPanelPartInitParameters, + TabPartInitParameters, IContentRenderer, ITabRenderer, } from '../../dockview/types'; import { PanelUpdateEvent } from '../../panel/types'; +import { TabLocation } from '../../dockview/framework'; export class DockviewPanelModelMock implements IDockviewPanelModel { constructor( @@ -17,8 +18,11 @@ export class DockviewPanelModelMock implements IDockviewPanelModel { // } + copyTabComponent(tabLocation: TabLocation): ITabRenderer { + return this.tab; + } - init(params: GroupPanelPartInitParameters): void { + init(params: TabPartInitParameters): void { // } diff --git a/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts b/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts index 9df09dc2f..ad6088d09 100644 --- a/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts +++ b/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts @@ -2453,17 +2453,17 @@ describe('dockviewComponent', () => { const group = dockview.getGroupPanel('panel2')!.api.group; const viewQuery = group.element.querySelectorAll( - '.dv-groupview > .dv-tabs-and-actions-container > .dv-tabs-container > .dv-tab' + '.dv-groupview > .dv-tabs-and-actions-container > .dv-tabs-panel > .dv-tabs-container > .dv-tab' ); expect(viewQuery.length).toBe(2); const viewQuery2 = group.element.querySelectorAll( - '.dv-groupview > .dv-tabs-and-actions-container > .dv-tabs-container > .dv-tab > .dv-default-tab' + '.dv-groupview > .dv-tabs-and-actions-container > .dv-tabs-panel > .dv-tabs-container > .dv-tab > .dv-default-tab' ); expect(viewQuery2.length).toBe(1); const viewQuery3 = group.element.querySelectorAll( - '.dv-groupview > .dv-tabs-and-actions-container > .dv-tabs-container > .dv-tab > .panel-tab-part-panel2' + '.dv-groupview > .dv-tabs-and-actions-container > .dv-tabs-panel > .dv-tabs-container > .dv-tab > .panel-tab-part-panel2' ); expect(viewQuery3.length).toBe(1); }); diff --git a/packages/dockview-core/src/__tests__/dockview/dockviewGroupPanelModel.spec.ts b/packages/dockview-core/src/__tests__/dockview/dockviewGroupPanelModel.spec.ts index 19b811c4f..d87413609 100644 --- a/packages/dockview-core/src/__tests__/dockview/dockviewGroupPanelModel.spec.ts +++ b/packages/dockview-core/src/__tests__/dockview/dockviewGroupPanelModel.spec.ts @@ -24,6 +24,7 @@ import { createOffsetDragOverEvent } from '../__test_utils__/utils'; import { OverlayRenderContainer } from '../../overlay/overlayRenderContainer'; import { Emitter } from '../../events'; import { fromPartial } from '@total-typescript/shoehorn'; +import { TabLocation } from '../../dockview/framework'; enum GroupChangeKind2 { ADD_PANEL, @@ -36,12 +37,16 @@ class TestModel implements IDockviewPanelModel { readonly contentComponent: string; readonly tab: ITabRenderer; - constructor(id: string) { + constructor(readonly id: string) { this.content = new TestHeaderPart(id); this.contentComponent = id; this.tab = new TestContentPart(id); } + copyTabComponent(tabLocation: TabLocation): ITabRenderer { + return new TestHeaderPart(this.id); + } + update(event: PanelUpdateEvent): void { // } diff --git a/packages/dockview-core/src/dockview/components/titlebar/tabs.scss b/packages/dockview-core/src/dockview/components/titlebar/tabs.scss index d797c5f06..df15a09e5 100644 --- a/packages/dockview-core/src/dockview/components/titlebar/tabs.scss +++ b/packages/dockview-core/src/dockview/components/titlebar/tabs.scss @@ -1,23 +1,93 @@ -.dv-tabs-container { - display: flex; - overflow-x: overlay; - overflow-y: hidden; +.dv-tabs-panel { + overflow: hidden; - scrollbar-width: thin; // firefox + &.dv-horizontal { + .dv-tabs-container { + .dv-tab { + &:last-child { + margin-right: 0; + } - &::-webkit-scrollbar { - height: 3px; + &:not(:nth-last-child(1)) { + margin-left: 0; + } + + &: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%; + } + } + } } - /* Track */ - &::-webkit-scrollbar-track { - background: transparent; + .dv-tabs-container { + display: flex; + overflow: 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; + padding: 0.25rem 0.5rem; + cursor: pointer; + position: relative; + box-sizing: border-box; + font-size: var(--dv-tab-font-size); + margin: var(--dv-tab-margin); + } } - /* Handle */ - &::-webkit-scrollbar-thumb { - background: var(--dv-tabs-container-scrollbar-color); + .dv-tabs-overflow-dropdown-default { + background-color: var( + --dv-activegroup-hiddenpanel-tab-background-color + ); + height: 100%; + color: var(--dv-activegroup-hiddenpanel-tab-color); + border-left: 1px solid var(--dv-tab-divider-color); + + margin: var(--dv-tab-margin); + display: flex; + align-items: center; + flex-shrink: 0; + padding: 0.25rem 0.5rem; + cursor: pointer; + + > span { + padding-left: 0.25rem; + } + + > svg { + transform: rotate(90deg); + } } +} + +.dv-tabs-overflow-container { + flex-direction: column; + height: unset; + border: 1px solid var(--dv-tab-divider-color); + background-color: var(--dv-group-view-background-color); .dv-tab { -webkit-user-drag: element; @@ -26,58 +96,24 @@ cursor: pointer; position: relative; box-sizing: border-box; - font-size: var(-dv-tab-font-size); + font-size: var(--dv-tab-font-size); margin: var(--dv-tab-margin); - &:first-child { - margin-right: 0; - } - - &:not(:nth-last-child(1)) { - margin-left: 0; - } - - &: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%; + &:not(:last-child) { + border-bottom: 1px solid var(--dv-tab-divider-color); } } - &.dv-tabs-overflow-container { - flex-direction: column; - height: unset; - - .dv-tab { - height: var(--dv-tabs-and-actions-container-height); - } - - .dv-active-tab { - background-color: var( - --dv-activegroup-visiblepanel-tab-background-color - ); - color: var(--dv-activegroup-visiblepanel-tab-color); - } - .dv-inactive-tab { - background-color: var( - --dv-activegroup-hiddenpanel-tab-background-color - ); - color: var(--dv-activegroup-hiddenpanel-tab-color); - } - } -} - -.dv-tabs-panel { - .dv-tabs-overflow-handle { - height: 100%; - width: 15px; - flex-shrink: 0; - background-color: red; + .dv-active-tab { + background-color: var( + --dv-activegroup-visiblepanel-tab-background-color + ); + color: var(--dv-activegroup-visiblepanel-tab-color); + } + .dv-inactive-tab { + background-color: var( + --dv-activegroup-hiddenpanel-tab-background-color + ); + color: var(--dv-activegroup-hiddenpanel-tab-color); } } diff --git a/packages/dockview-core/src/dockview/components/titlebar/tabs.tsx b/packages/dockview-core/src/dockview/components/titlebar/tabs.ts similarity index 53% rename from packages/dockview-core/src/dockview/components/titlebar/tabs.tsx rename to packages/dockview-core/src/dockview/components/titlebar/tabs.ts index a477531a4..659061b89 100644 --- a/packages/dockview-core/src/dockview/components/titlebar/tabs.tsx +++ b/packages/dockview-core/src/dockview/components/titlebar/tabs.ts @@ -1,11 +1,17 @@ import { getPanelData } from '../../../dnd/dataTransfer'; -import { OverflowObserver } from '../../../dom'; +import { + isChildEntirelyVisibleWithinParent, + OverflowObserver, + toggleClass, +} from '../../../dom'; import { addDisposableListener, Emitter, Event } from '../../../events'; import { CompositeDisposable, Disposable, IValueDisposable, + MutableDisposable, } from '../../../lifecycle'; +import { createChevronRightButton } from '../../../svg'; import { DockviewComponent } from '../../dockviewComponent'; import { DockviewGroupPanel } from '../../dockviewGroupPanel'; import { WillShowOverlayLocationEvent } from '../../dockviewGroupPanelModel'; @@ -13,14 +19,38 @@ import { DockviewPanel, IDockviewPanel } from '../../dockviewPanel'; import { Tab } from '../tab/tab'; import { TabDragEvent, TabDropIndexEvent } from './tabsContainer'; +type DropdownElement = { + element: HTMLElement; + update: (params: { tabs: number }) => void; + dispose?: () => void; +}; + +function createDropdownElementHandle(): DropdownElement { + const el = document.createElement('div'); + el.className = 'dv-tabs-overflow-dropdown-default'; + + const text = document.createElement('span'); + text.textContent = ``; + const icon = createChevronRightButton(); + el.appendChild(icon); + el.appendChild(text); + + return { + element: el, + update: (params: { tabs: number }) => { + text.textContent = `${params.tabs}`; + }, + }; +} + export class Tabs extends CompositeDisposable { private readonly _element: HTMLElement; private readonly _tabsList: HTMLElement; private tabs: IValueDisposable[] = []; private selectedIndex = -1; - private _hasOverflow = false; - private _dropdownAnchor: HTMLElement | null = null; + + private readonly _dropdownDisposable = new MutableDisposable(); private readonly _onTabDragStart = new Emitter(); readonly onTabDragStart: Event = this._onTabDragStart.event; @@ -33,6 +63,9 @@ export class Tabs extends CompositeDisposable { readonly onWillShowOverlay: Event = this._onWillShowOverlay.event; + private dropdownPart: DropdownElement | null = null; + private _overflowTabs: string[] = []; + get element(): HTMLElement { return this._element; } @@ -52,7 +85,7 @@ export class Tabs extends CompositeDisposable { super(); this._element = document.createElement('div'); - this._element.className = 'dv-tabs-panel'; + this._element.className = 'dv-tabs-panel dv-horizontal'; this._element.style.display = 'flex'; this._element.style.overflow = 'auto'; this._tabsList = document.createElement('div'); @@ -62,12 +95,17 @@ export class Tabs extends CompositeDisposable { const observer = new OverflowObserver(this._tabsList); this.addDisposables( + this._dropdownDisposable, + this._onWillShowOverlay, + this._onDrop, + this._onTabDragStart, observer, observer.onDidChange((event) => { const hasOverflow = event.hasScrollX || event.hasScrollY; - if (this._hasOverflow !== hasOverflow) { - this.toggleDropdown(hasOverflow); - } + this.toggleDropdown({ reset: !hasOverflow }); + }), + addDisposableListener(this._tabsList, 'scroll', () => { + this.toggleDropdown({ reset: false }); }), addDisposableListener(this.element, 'pointerdown', (event) => { if (event.defaultPrevented) { @@ -103,10 +141,27 @@ export class Tabs extends CompositeDisposable { } setActivePanel(panel: IDockviewPanel): void { - this.tabs.forEach((tab) => { + let runningWidth = 0; + + for (const tab of this.tabs) { const isActivePanel = panel.id === tab.value.panel.id; tab.value.setActive(isActivePanel); - }); + + if (isActivePanel) { + const element = tab.value.element; + const parentElement = element.parentElement!; + + if ( + runningWidth < parentElement.scrollLeft || + runningWidth + element.clientWidth > + parentElement.scrollLeft + parentElement.clientWidth + ) { + parentElement.scrollLeft = runningWidth; + } + } + + runningWidth += tab.value.element.clientWidth; + } } openPanel(panel: IDockviewPanel, index: number = this.tabs.length): void { @@ -120,7 +175,11 @@ export class Tabs extends CompositeDisposable { tab.onDragStart((event) => { this._onTabDragStart.fire({ nativeEvent: event, panel }); }), - tab.onChanged((event) => { + tab.onPointerDown((event) => { + if (event.defaultPrevented) { + return; + } + const isFloatingGroupsEnabled = !this.accessor.options.disableFloatingGroups; @@ -149,14 +208,12 @@ export class Tabs extends CompositeDisposable { return; } - const isLeftClick = event.button === 0; - - if (!isLeftClick || event.defaultPrevented) { - return; - } - - if (this.group.activePanel !== panel) { - this.group.model.openPanel(panel); + switch (event.button) { + case 0: // left click or touch + if (this.group.activePanel !== panel) { + this.group.model.openPanel(panel); + } + break; } }), tab.onDrop((event) => { @@ -218,48 +275,105 @@ export class Tabs extends CompositeDisposable { } } - private toggleDropdown(show: boolean): void { - this._hasOverflow = show; + private toggleDropdown(options: { reset: boolean }): void { + const tabs = options.reset + ? [] + : this.tabs + .filter( + (tab) => + !isChildEntirelyVisibleWithinParent( + tab.value.element, + this._tabsList + ) + ) + .map((x) => x.value.panel.id); - if (this._dropdownAnchor) { - this._dropdownAnchor.remove(); - this._dropdownAnchor = null; - } + this._overflowTabs = tabs; - if (!show) { + if (this._overflowTabs.length > 0 && this.dropdownPart) { + this.dropdownPart.update({ tabs: tabs.length }); return; } - this._dropdownAnchor = document.createElement('div'); - this._dropdownAnchor.className = 'dv-tabs-overflow-handle'; + if (this._overflowTabs.length === 0) { + this._dropdownDisposable.dispose(); + return; + } - this.element.appendChild(this._dropdownAnchor); + const root = document.createElement('div'); + root.className = 'dv-tabs-overflow-dropdown-root'; - addDisposableListener(this._dropdownAnchor, 'click', (event) => { - const el = document.createElement('div'); - el.style.overflow = 'auto'; - el.className = - 'dv-tabs-and-actions-container dv-tabs-container dv-tabs-overflow-container'; + const part = createDropdownElementHandle(); + part.update({ tabs: tabs.length }); - this.tabs.map((tab) => { - const child = tab.value.element.cloneNode(true); + this.dropdownPart = part; - const wrapper = document.createElement('div'); + root.appendChild(part.element); + this.element.appendChild(root); - wrapper.addEventListener('mousedown', () => { - this.accessor.popupService.close(); - tab.value.element.scrollIntoView(); - tab.value.panel.api.setActive(); + this._dropdownDisposable.value = new CompositeDisposable( + Disposable.from(() => { + root.remove(); + this.dropdownPart?.dispose?.(); + this.dropdownPart = null; + }), + addDisposableListener( + root, + 'pointerdown', + (event) => { + event.preventDefault(); + }, + { capture: true } + ), + addDisposableListener(root, 'click', (event) => { + const el = document.createElement('div'); + el.style.overflow = 'auto'; + el.className = 'dv-tabs-overflow-container'; + + this.tabs + .filter((tab) => + this._overflowTabs.includes(tab.value.panel.id) + ) + .map((tab) => { + const panelObject = this.group.panels.find( + (panel) => panel === tab.value.panel + )!; + + const tabComponent = + panelObject.view.createTabRenderer( + 'headerOverflow' + ); + + const child = tabComponent.element; + + const wrapper = document.createElement('div'); + toggleClass(wrapper, 'dv-tab', true); + toggleClass( + wrapper, + 'dv-active-tab', + panelObject.api.isActive + ); + toggleClass( + wrapper, + 'dv-inactive-tab', + !panelObject.api.isActive + ); + + wrapper.addEventListener('mousedown', () => { + this.accessor.popupService.close(); + tab.value.element.scrollIntoView(); + tab.value.panel.api.setActive(); + }); + wrapper.appendChild(child); + + el.appendChild(wrapper); + }); + + this.accessor.popupService.openPopover(el, { + x: event.clientX, + y: event.clientY, }); - wrapper.appendChild(child); - - el.appendChild(wrapper); - }); - - this.accessor.popupService.openPopover(el, { - x: event.clientX, - y: event.clientY, - }); - }); + }) + ); } } diff --git a/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts b/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts index 602bc67fb..e41952087 100644 --- a/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts +++ b/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts @@ -140,6 +140,7 @@ export class TabsContainer this._element.appendChild(this.rightActionsContainer); this.addDisposables( + this.tabs, this._onWillShowOverlay, this._onDrop, this._onGroupDragStart, @@ -171,6 +172,10 @@ export class TabsContainer this.voidContainer.element, 'pointerdown', (event) => { + if (event.defaultPrevented) { + return; + } + const isFloatingGroupsEnabled = !this.accessor.options.disableFloatingGroups; diff --git a/packages/dockview-core/src/dockview/dockviewPanelModel.ts b/packages/dockview-core/src/dockview/dockviewPanelModel.ts index 777717bad..903fcc095 100644 --- a/packages/dockview-core/src/dockview/dockviewPanelModel.ts +++ b/packages/dockview-core/src/dockview/dockviewPanelModel.ts @@ -4,27 +4,29 @@ import { IContentRenderer, ITabRenderer, } from './types'; -import { DockviewGroupPanel } from './dockviewGroupPanel'; import { IDisposable } from '../lifecycle'; import { IDockviewComponent } from './dockviewComponent'; import { PanelUpdateEvent } from '../panel/types'; +import { TabLocation } from './framework'; export interface IDockviewPanelModel extends IDisposable { readonly contentComponent: string; readonly tabComponent?: string; readonly content: IContentRenderer; readonly tab: ITabRenderer; - readonly newTab: ITabRenderer; update(event: PanelUpdateEvent): void; layout(width: number, height: number): void; init(params: GroupPanelPartInitParameters): void; - updateParentGroup(group: DockviewGroupPanel, isPanelVisible: boolean): void; + createTabRenderer(tabLocation: TabLocation): ITabRenderer; } export class DockviewPanelModel implements IDockviewPanelModel { private readonly _content: IContentRenderer; private readonly _tab: ITabRenderer; + private _params: GroupPanelPartInitParameters | undefined; + private _updateEvent: PanelUpdateEvent | undefined; + get content(): IContentRenderer { return this._content; } @@ -43,21 +45,23 @@ export class DockviewPanelModel implements IDockviewPanelModel { this._tab = this.createTabComponent(this.id, tabComponent); } - get newTab() { + createTabRenderer(tabLocation: TabLocation): ITabRenderer { const cmp = this.createTabComponent(this.id, this.tabComponent); + if (this._params) { + cmp.init({ ...this._params, tabLocation }); + } + if (this._updateEvent) { + cmp.update?.(this._updateEvent); + } + return cmp; } init(params: GroupPanelPartInitParameters): void { - this.content.init(params); - this.tab.init(params); - } + this._params = params; - updateParentGroup( - _group: DockviewGroupPanel, - _isPanelVisible: boolean - ): void { - // noop + this.content.init(params); + this.tab.init({ ...params, tabLocation: 'header' }); } layout(width: number, height: number): void { @@ -65,6 +69,8 @@ export class DockviewPanelModel implements IDockviewPanelModel { } update(event: PanelUpdateEvent): void { + this._updateEvent = event; + this.content.update?.(event); this.tab.update?.(event); } diff --git a/packages/dockview-core/src/dockview/framework.ts b/packages/dockview-core/src/dockview/framework.ts index bd6ccaff5..1ed239b46 100644 --- a/packages/dockview-core/src/dockview/framework.ts +++ b/packages/dockview-core/src/dockview/framework.ts @@ -11,9 +11,11 @@ export interface IGroupPanelBaseProps containerApi: DockviewApi; } +export type TabLocation = 'header' | 'headerOverflow'; + export type IDockviewPanelHeaderProps< T extends { [index: string]: any } = any -> = IGroupPanelBaseProps; +> = IGroupPanelBaseProps & { tabLocation: TabLocation }; export type IDockviewPanelProps = IGroupPanelBaseProps; diff --git a/packages/dockview-core/src/dockview/types.ts b/packages/dockview-core/src/dockview/types.ts index 3b2c8f112..5706f0721 100644 --- a/packages/dockview-core/src/dockview/types.ts +++ b/packages/dockview-core/src/dockview/types.ts @@ -4,6 +4,7 @@ import { DockviewApi } from '../api/component.api'; import { Optional } from '../types'; import { IDockviewGroupPanel } from './dockviewGroupPanel'; import { DockviewPanelRenderer } from '../overlay/overlayRenderContainer'; +import { TabLocation } from './framework'; export interface HeaderPartInitParameters { title: string; @@ -34,10 +35,14 @@ export interface IWatermarkRenderer init: (params: WatermarkRendererInitParameters) => void; } +export interface TabPartInitParameters extends GroupPanelPartInitParameters { + tabLocation: TabLocation; +} + export interface ITabRenderer extends Optional, RendererMethodOptionalList> { readonly element: HTMLElement; - init(parameters: GroupPanelPartInitParameters): void; + init(parameters: TabPartInitParameters): void; } export interface IContentRenderer diff --git a/packages/dockview-core/src/dom.ts b/packages/dockview-core/src/dom.ts index d61b1cacb..3dc9000a9 100644 --- a/packages/dockview-core/src/dom.ts +++ b/packages/dockview-core/src/dom.ts @@ -357,3 +357,25 @@ export class Classnames { } } } + +export function isChildEntirelyVisibleWithinParent( + child: HTMLElement, + parent: HTMLElement +): boolean { + // + const childPosition = getDomNodePagePosition(child); + const parentPosition = getDomNodePagePosition(parent); + + if (childPosition.left < parentPosition.left) { + return false; + } + + if ( + childPosition.left + childPosition.width > + parentPosition.left + parentPosition.width + ) { + return false; + } + + return true; +} diff --git a/packages/dockview-core/src/gridview/baseComponentGridview.ts b/packages/dockview-core/src/gridview/baseComponentGridview.ts index 93779f5a0..2ef754396 100644 --- a/packages/dockview-core/src/gridview/baseComponentGridview.ts +++ b/packages/dockview-core/src/gridview/baseComponentGridview.ts @@ -11,6 +11,9 @@ import { Classnames } from '../dom'; const nextLayoutId = sequentialNumberGenerator(); +/** + * A direction in which a panel can be moved or placed relative to another panel. + */ export type Direction = 'left' | 'right' | 'above' | 'below' | 'within'; export function toTarget(direction: Direction): Position { diff --git a/packages/dockview-core/src/theme/_space-mixin.scss b/packages/dockview-core/src/theme/_space-mixin.scss index b2e84cdbd..2482b6841 100644 --- a/packages/dockview-core/src/theme/_space-mixin.scss +++ b/packages/dockview-core/src/theme/_space-mixin.scss @@ -4,8 +4,7 @@ --dv-tab-margin: 0.5rem 0.25rem; --dv-tabs-and-actions-container-height: 44px; - - --dv-border-radius + --dv-border-radius: 20px; .dv-resize-container:has(> .dv-groupview) { border-radius: 8px; @@ -27,20 +26,26 @@ border: none; } + .dv-tabs-overflow-container, + .dv-tabs-overflow-dropdown-default { + border-radius: 8px; + height: unset !important; + } + + .dv-tab { + border-radius: 8px; + + .dv-svg { + height: 8px; + width: 8px; + } + } + .dv-groupview { border-radius: var(--dv-border-radius); .dv-tabs-and-actions-container { padding: 0px calc(var(--dv-border-radius) / 2); - - .dv-tab { - border-radius: 8px; - - .dv-svg { - height: 8px; - width: 8px; - } - } } .dv-content-container { diff --git a/packages/dockview-vue/src/utils.ts b/packages/dockview-vue/src/utils.ts index 45c23fafd..3b4c4fa71 100644 --- a/packages/dockview-vue/src/utils.ts +++ b/packages/dockview-vue/src/utils.ts @@ -2,7 +2,6 @@ import type { DockviewApi, DockviewGroupPanel, DockviewPanelApi, - GroupPanelPartInitParameters, IContentRenderer, IDockviewPanelHeaderProps, IGroupHeaderProps, @@ -12,6 +11,7 @@ import type { IWatermarkRenderer, PanelUpdateEvent, Parameters, + TabPartInitParameters, WatermarkRendererInitParameters, } from 'dockview-core'; import { @@ -121,7 +121,7 @@ export class VueRenderer private _api: DockviewPanelApi | undefined; private _containerApi: DockviewApi | undefined; - init(parameters: GroupPanelPartInitParameters): void { + init(parameters: TabPartInitParameters): void { this._api = parameters.api; this._containerApi = parameters.containerApi; @@ -129,6 +129,7 @@ export class VueRenderer params: parameters.params, api: parameters.api, containerApi: parameters.containerApi, + tabLocation: parameters.tabLocation, }; this._renderDisposable?.dispose(); diff --git a/packages/dockview/src/dockview/defaultTab.tsx b/packages/dockview/src/dockview/defaultTab.tsx index e12e61465..f8164560f 100644 --- a/packages/dockview/src/dockview/defaultTab.tsx +++ b/packages/dockview/src/dockview/defaultTab.tsx @@ -35,6 +35,7 @@ export const DockviewDefaultTab: React.FunctionComponent< onPointerDown, onPointerUp, onPointerLeave, + tabLocation, ...rest }) => { const title = useTitle(api); @@ -96,7 +97,7 @@ export const DockviewDefaultTab: React.FunctionComponent< className="dv-default-tab" > {title} - {!hideClose && ( + {!hideClose && tabLocation !== 'headerOverflow' && (
; + private part?: ReactPart; get element(): HTMLElement { return this._element; @@ -17,7 +17,7 @@ export class ReactPanelHeaderPart implements ITabRenderer { constructor( public readonly id: string, - private readonly component: React.FunctionComponent, + private readonly component: React.FunctionComponent, private readonly reactPortalStore: ReactPortalStore ) { this._element = document.createElement('div'); @@ -30,7 +30,7 @@ export class ReactPanelHeaderPart implements ITabRenderer { //noop } - public init(parameters: GroupPanelPartInitParameters): void { + public init(parameters: TabPartInitParameters): void { this.part = new ReactPart( this.element, this.reactPortalStore, @@ -39,6 +39,7 @@ export class ReactPanelHeaderPart implements ITabRenderer { params: parameters.params, api: parameters.api, containerApi: parameters.containerApi, + tabLocation: parameters.tabLocation, } ); } From cfe37766a95160e534189579a6f91e65f830bac7 Mon Sep 17 00:00:00 2001 From: mathuo <6710312+mathuo@users.noreply.github.com> Date: Sat, 1 Mar 2025 21:04:06 +0000 Subject: [PATCH 06/40] feat: scrollbars --- .../__mocks__/mockDockviewPanelModel.ts | 2 +- .../dockview/dockviewComponent.spec.ts | 14 +- .../dockview/dockviewGroupPanelModel.spec.ts | 2 +- .../titlebar/tabOverflowControl.scss | 19 ++ .../components/titlebar/tabOverflowControl.ts | 25 ++ .../dockview/components/titlebar/tabs.scss | 73 ++---- .../src/dockview/components/titlebar/tabs.ts | 222 ++++++------------ .../components/titlebar/tabsContainer.scss | 4 + .../components/titlebar/tabsContainer.ts | 117 ++++++++- .../src/dockview/dockviewComponent.scss | 4 +- packages/dockview-core/src/scrollbar.scss | 28 +++ packages/dockview-core/src/scrollbar.ts | 131 +++++++++++ .../__tests__/dockview/defaultTab.spec.tsx | 7 + 13 files changed, 432 insertions(+), 216 deletions(-) create mode 100644 packages/dockview-core/src/dockview/components/titlebar/tabOverflowControl.scss create mode 100644 packages/dockview-core/src/dockview/components/titlebar/tabOverflowControl.ts create mode 100644 packages/dockview-core/src/scrollbar.scss create mode 100644 packages/dockview-core/src/scrollbar.ts diff --git a/packages/dockview-core/src/__tests__/__mocks__/mockDockviewPanelModel.ts b/packages/dockview-core/src/__tests__/__mocks__/mockDockviewPanelModel.ts index 35a301025..2a43a1796 100644 --- a/packages/dockview-core/src/__tests__/__mocks__/mockDockviewPanelModel.ts +++ b/packages/dockview-core/src/__tests__/__mocks__/mockDockviewPanelModel.ts @@ -18,7 +18,7 @@ export class DockviewPanelModelMock implements IDockviewPanelModel { // } - copyTabComponent(tabLocation: TabLocation): ITabRenderer { + createTabRenderer(tabLocation: TabLocation): ITabRenderer { return this.tab; } diff --git a/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts b/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts index ad6088d09..d84a723e4 100644 --- a/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts +++ b/packages/dockview-core/src/__tests__/dockview/dockviewComponent.spec.ts @@ -133,11 +133,15 @@ describe('dockviewComponent', () => { }, className: 'test-a test-b', }); - expect(dockview.element.className).toBe('test-a test-b dockview-theme-abyss'); + expect(dockview.element.className).toBe( + 'test-a test-b dockview-theme-abyss' + ); dockview.updateOptions({ className: 'test-b test-c' }); - expect(dockview.element.className).toBe('dockview-theme-abyss test-b test-c'); + expect(dockview.element.className).toBe( + 'dockview-theme-abyss test-b test-c' + ); }); describe('memory leakage', () => { @@ -2453,17 +2457,17 @@ describe('dockviewComponent', () => { const group = dockview.getGroupPanel('panel2')!.api.group; const viewQuery = group.element.querySelectorAll( - '.dv-groupview > .dv-tabs-and-actions-container > .dv-tabs-panel > .dv-tabs-container > .dv-tab' + '.dv-groupview > .dv-tabs-and-actions-container > .dv-scrollable > .dv-tabs-container > .dv-tab' ); expect(viewQuery.length).toBe(2); const viewQuery2 = group.element.querySelectorAll( - '.dv-groupview > .dv-tabs-and-actions-container > .dv-tabs-panel > .dv-tabs-container > .dv-tab > .dv-default-tab' + '.dv-groupview > .dv-tabs-and-actions-container > .dv-scrollable > .dv-tabs-container > .dv-tab > .dv-default-tab' ); expect(viewQuery2.length).toBe(1); const viewQuery3 = group.element.querySelectorAll( - '.dv-groupview > .dv-tabs-and-actions-container > .dv-tabs-panel > .dv-tabs-container > .dv-tab > .panel-tab-part-panel2' + '.dv-groupview > .dv-tabs-and-actions-container > .dv-scrollable > .dv-tabs-container > .dv-tab > .panel-tab-part-panel2' ); expect(viewQuery3.length).toBe(1); }); diff --git a/packages/dockview-core/src/__tests__/dockview/dockviewGroupPanelModel.spec.ts b/packages/dockview-core/src/__tests__/dockview/dockviewGroupPanelModel.spec.ts index d87413609..719c26ca0 100644 --- a/packages/dockview-core/src/__tests__/dockview/dockviewGroupPanelModel.spec.ts +++ b/packages/dockview-core/src/__tests__/dockview/dockviewGroupPanelModel.spec.ts @@ -43,7 +43,7 @@ class TestModel implements IDockviewPanelModel { this.tab = new TestContentPart(id); } - copyTabComponent(tabLocation: TabLocation): ITabRenderer { + createTabRenderer(tabLocation: TabLocation): ITabRenderer { return new TestHeaderPart(this.id); } diff --git a/packages/dockview-core/src/dockview/components/titlebar/tabOverflowControl.scss b/packages/dockview-core/src/dockview/components/titlebar/tabOverflowControl.scss new file mode 100644 index 000000000..2d87ce7ca --- /dev/null +++ b/packages/dockview-core/src/dockview/components/titlebar/tabOverflowControl.scss @@ -0,0 +1,19 @@ +.dv-tabs-overflow-dropdown-default { + height: 100%; + color: var(--dv-activegroup-hiddenpanel-tab-color); + + margin: var(--dv-tab-margin); + display: flex; + align-items: center; + flex-shrink: 0; + padding: 0.25rem 0.5rem; + cursor: pointer; + + > span { + padding-left: 0.25rem; + } + + > svg { + transform: rotate(90deg); + } +} diff --git a/packages/dockview-core/src/dockview/components/titlebar/tabOverflowControl.ts b/packages/dockview-core/src/dockview/components/titlebar/tabOverflowControl.ts new file mode 100644 index 000000000..1e27c661e --- /dev/null +++ b/packages/dockview-core/src/dockview/components/titlebar/tabOverflowControl.ts @@ -0,0 +1,25 @@ +import { createChevronRightButton } from '../../../svg'; + +export type DropdownElement = { + element: HTMLElement; + update: (params: { tabs: number }) => void; + dispose?: () => void; +}; + +export function createDropdownElementHandle(): DropdownElement { + const el = document.createElement('div'); + el.className = 'dv-tabs-overflow-dropdown-default'; + + const text = document.createElement('span'); + text.textContent = ``; + const icon = createChevronRightButton(); + el.appendChild(icon); + el.appendChild(text); + + return { + element: el, + update: (params: { tabs: number }) => { + text.textContent = `${params.tabs}`; + }, + }; +} diff --git a/packages/dockview-core/src/dockview/components/titlebar/tabs.scss b/packages/dockview-core/src/dockview/components/titlebar/tabs.scss index df15a09e5..5b9e7487f 100644 --- a/packages/dockview-core/src/dockview/components/titlebar/tabs.scss +++ b/packages/dockview-core/src/dockview/components/titlebar/tabs.scss @@ -1,6 +1,5 @@ -.dv-tabs-panel { +.dv-tabs-container { overflow: hidden; - &.dv-horizontal { .dv-tabs-container { .dv-tab { @@ -27,62 +26,38 @@ } } - .dv-tabs-container { - display: flex; - overflow: hidden; - scrollbar-width: thin; // firefox + display: flex; + height: 100%; + overflow: 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; - padding: 0.25rem 0.5rem; - cursor: pointer; - position: relative; - box-sizing: border-box; - font-size: var(--dv-tab-font-size); - margin: var(--dv-tab-margin); - } + &::-webkit-scrollbar { + height: 3px; } - .dv-tabs-overflow-dropdown-default { - background-color: var( - --dv-activegroup-hiddenpanel-tab-background-color - ); - height: 100%; - color: var(--dv-activegroup-hiddenpanel-tab-color); - border-left: 1px solid var(--dv-tab-divider-color); + /* Track */ + &::-webkit-scrollbar-track { + background: transparent; + } - margin: var(--dv-tab-margin); - display: flex; - align-items: center; - flex-shrink: 0; + /* Handle */ + &::-webkit-scrollbar-thumb { + background: var(--dv-tabs-container-scrollbar-color); + } + + .dv-tab { + -webkit-user-drag: element; + outline: none; padding: 0.25rem 0.5rem; cursor: pointer; - - > span { - padding-left: 0.25rem; - } - - > svg { - transform: rotate(90deg); - } + position: relative; + box-sizing: border-box; + font-size: var(--dv-tab-font-size); + margin: var(--dv-tab-margin); } } + .dv-tabs-overflow-container { flex-direction: column; height: unset; diff --git a/packages/dockview-core/src/dockview/components/titlebar/tabs.ts b/packages/dockview-core/src/dockview/components/titlebar/tabs.ts index 659061b89..b8a6d5499 100644 --- a/packages/dockview-core/src/dockview/components/titlebar/tabs.ts +++ b/packages/dockview-core/src/dockview/components/titlebar/tabs.ts @@ -2,7 +2,6 @@ import { getPanelData } from '../../../dnd/dataTransfer'; import { isChildEntirelyVisibleWithinParent, OverflowObserver, - toggleClass, } from '../../../dom'; import { addDisposableListener, Emitter, Event } from '../../../events'; import { @@ -11,7 +10,7 @@ import { IValueDisposable, MutableDisposable, } from '../../../lifecycle'; -import { createChevronRightButton } from '../../../svg'; +import { Scrollbar } from '../../../scrollbar'; import { DockviewComponent } from '../../dockviewComponent'; import { DockviewGroupPanel } from '../../dockviewGroupPanel'; import { WillShowOverlayLocationEvent } from '../../dockviewGroupPanelModel'; @@ -19,38 +18,14 @@ import { DockviewPanel, IDockviewPanel } from '../../dockviewPanel'; import { Tab } from '../tab/tab'; import { TabDragEvent, TabDropIndexEvent } from './tabsContainer'; -type DropdownElement = { - element: HTMLElement; - update: (params: { tabs: number }) => void; - dispose?: () => void; -}; - -function createDropdownElementHandle(): DropdownElement { - const el = document.createElement('div'); - el.className = 'dv-tabs-overflow-dropdown-default'; - - const text = document.createElement('span'); - text.textContent = ``; - const icon = createChevronRightButton(); - el.appendChild(icon); - el.appendChild(text); - - return { - element: el, - update: (params: { tabs: number }) => { - text.textContent = `${params.tabs}`; - }, - }; -} - export class Tabs extends CompositeDisposable { private readonly _element: HTMLElement; private readonly _tabsList: HTMLElement; + private readonly _observerDisposable = new MutableDisposable(); - private tabs: IValueDisposable[] = []; + private _tabs: IValueDisposable[] = []; private selectedIndex = -1; - - private readonly _dropdownDisposable = new MutableDisposable(); + private _showTabsOverflowControl = false; private readonly _onTabDragStart = new Emitter(); readonly onTabDragStart: Event = this._onTabDragStart.event; @@ -63,50 +38,79 @@ export class Tabs extends CompositeDisposable { readonly onWillShowOverlay: Event = this._onWillShowOverlay.event; - private dropdownPart: DropdownElement | null = null; - private _overflowTabs: string[] = []; + private readonly _onOverflowTabsChange = new Emitter<{ + tabs: string[]; + reset: boolean; + }>(); + readonly onOverflowTabsChange = this._onOverflowTabsChange.event; + + get showTabsOverflowControl(): boolean { + return this._showTabsOverflowControl; + } + + set showTabsOverflowControl(value: boolean) { + if (this._showTabsOverflowControl == value) { + return; + } + + this._showTabsOverflowControl = value; + + if (value) { + const observer = new OverflowObserver(this._tabsList); + + this._observerDisposable.value = new CompositeDisposable( + observer, + observer.onDidChange((event) => { + const hasOverflow = event.hasScrollX || event.hasScrollY; + this.toggleDropdown({ reset: !hasOverflow }); + }), + addDisposableListener(this._tabsList, 'scroll', () => { + this.toggleDropdown({ reset: false }); + }) + ); + } + } get element(): HTMLElement { return this._element; } get panels(): string[] { - return this.tabs.map((_) => _.value.panel.id); + return this._tabs.map((_) => _.value.panel.id); } get size(): number { - return this.tabs.length; + return this._tabs.length; + } + + get tabs(): Tab[] { + return this._tabs.map((_) => _.value); } constructor( private readonly group: DockviewGroupPanel, - private readonly accessor: DockviewComponent + private readonly accessor: DockviewComponent, + options: { + showTabsOverflowControl: boolean; + } ) { super(); - this._element = document.createElement('div'); - this._element.className = 'dv-tabs-panel dv-horizontal'; - 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); + this._tabsList.className = 'dv-tabs-container dv-horizontal'; - const observer = new OverflowObserver(this._tabsList); + this.showTabsOverflowControl = options.showTabsOverflowControl; + + const scrollbar = new Scrollbar(this._tabsList); + this._element = scrollbar.element; this.addDisposables( - this._dropdownDisposable, + this._onOverflowTabsChange, + this._observerDisposable, + scrollbar, this._onWillShowOverlay, this._onDrop, this._onTabDragStart, - observer, - observer.onDidChange((event) => { - const hasOverflow = event.hasScrollX || event.hasScrollY; - this.toggleDropdown({ reset: !hasOverflow }); - }), - addDisposableListener(this._tabsList, 'scroll', () => { - this.toggleDropdown({ reset: false }); - }), addDisposableListener(this.element, 'pointerdown', (event) => { if (event.defaultPrevented) { return; @@ -119,31 +123,31 @@ export class Tabs extends CompositeDisposable { } }), Disposable.from(() => { - for (const { value, disposable } of this.tabs) { + for (const { value, disposable } of this._tabs) { disposable.dispose(); value.dispose(); } - this.tabs = []; + this._tabs = []; }) ); } indexOf(id: string): number { - return this.tabs.findIndex((tab) => tab.value.panel.id === id); + return this._tabs.findIndex((tab) => tab.value.panel.id === id); } isActive(tab: Tab): boolean { return ( this.selectedIndex > -1 && - this.tabs[this.selectedIndex].value === tab + this._tabs[this.selectedIndex].value === tab ); } setActivePanel(panel: IDockviewPanel): void { let runningWidth = 0; - for (const tab of this.tabs) { + for (const tab of this._tabs) { const isActivePanel = panel.id === tab.value.panel.id; tab.value.setActive(isActivePanel); @@ -164,8 +168,8 @@ export class Tabs extends CompositeDisposable { } } - openPanel(panel: IDockviewPanel, index: number = this.tabs.length): void { - if (this.tabs.find((tab) => tab.value.panel.id === panel.id)) { + openPanel(panel: IDockviewPanel, index: number = this._tabs.length): void { + if (this._tabs.find((tab) => tab.value.panel.id === panel.id)) { return; } const tab = new Tab(panel, this.accessor, this.group); @@ -219,7 +223,7 @@ export class Tabs extends CompositeDisposable { tab.onDrop((event) => { this._onDrop.fire({ event: event.nativeEvent, - index: this.tabs.findIndex((x) => x.value === tab), + index: this._tabs.findIndex((x) => x.value === tab), }); }), tab.onWillShowOverlay((event) => { @@ -242,7 +246,7 @@ export class Tabs extends CompositeDisposable { delete(id: string): void { const index = this.indexOf(id); - const tabToRemove = this.tabs.splice(index, 1)[0]; + const tabToRemove = this._tabs.splice(index, 1)[0]; const { value, disposable } = tabToRemove; @@ -253,9 +257,9 @@ export class Tabs extends CompositeDisposable { private addTab( tab: IValueDisposable, - index: number = this.tabs.length + index: number = this._tabs.length ): void { - if (index < 0 || index > this.tabs.length) { + if (index < 0 || index > this._tabs.length) { throw new Error('invalid location'); } @@ -264,10 +268,10 @@ export class Tabs extends CompositeDisposable { this._tabsList.children[index] ); - this.tabs = [ - ...this.tabs.slice(0, index), + this._tabs = [ + ...this._tabs.slice(0, index), tab, - ...this.tabs.slice(index), + ...this._tabs.slice(index), ]; if (this.selectedIndex < 0) { @@ -278,7 +282,7 @@ export class Tabs extends CompositeDisposable { private toggleDropdown(options: { reset: boolean }): void { const tabs = options.reset ? [] - : this.tabs + : this._tabs .filter( (tab) => !isChildEntirelyVisibleWithinParent( @@ -288,92 +292,6 @@ export class Tabs extends CompositeDisposable { ) .map((x) => x.value.panel.id); - this._overflowTabs = tabs; - - if (this._overflowTabs.length > 0 && this.dropdownPart) { - this.dropdownPart.update({ tabs: tabs.length }); - return; - } - - if (this._overflowTabs.length === 0) { - this._dropdownDisposable.dispose(); - return; - } - - const root = document.createElement('div'); - root.className = 'dv-tabs-overflow-dropdown-root'; - - const part = createDropdownElementHandle(); - part.update({ tabs: tabs.length }); - - this.dropdownPart = part; - - root.appendChild(part.element); - this.element.appendChild(root); - - this._dropdownDisposable.value = new CompositeDisposable( - Disposable.from(() => { - root.remove(); - this.dropdownPart?.dispose?.(); - this.dropdownPart = null; - }), - addDisposableListener( - root, - 'pointerdown', - (event) => { - event.preventDefault(); - }, - { capture: true } - ), - addDisposableListener(root, 'click', (event) => { - const el = document.createElement('div'); - el.style.overflow = 'auto'; - el.className = 'dv-tabs-overflow-container'; - - this.tabs - .filter((tab) => - this._overflowTabs.includes(tab.value.panel.id) - ) - .map((tab) => { - const panelObject = this.group.panels.find( - (panel) => panel === tab.value.panel - )!; - - const tabComponent = - panelObject.view.createTabRenderer( - 'headerOverflow' - ); - - const child = tabComponent.element; - - const wrapper = document.createElement('div'); - toggleClass(wrapper, 'dv-tab', true); - toggleClass( - wrapper, - 'dv-active-tab', - panelObject.api.isActive - ); - toggleClass( - wrapper, - 'dv-inactive-tab', - !panelObject.api.isActive - ); - - wrapper.addEventListener('mousedown', () => { - this.accessor.popupService.close(); - tab.value.element.scrollIntoView(); - tab.value.panel.api.setActive(); - }); - wrapper.appendChild(child); - - el.appendChild(wrapper); - }); - - this.accessor.popupService.openPopover(el, { - x: event.clientX, - y: event.clientY, - }); - }) - ); + this._onOverflowTabsChange.fire({ tabs, reset: options.reset }); } } diff --git a/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.scss b/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.scss index 14815f8bc..1ed918a1c 100644 --- a/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.scss +++ b/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.scss @@ -25,4 +25,8 @@ flex-grow: 1; cursor: grab; } + + .dv-right-actions-container { + display: flex; + } } diff --git a/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts b/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts index e41952087..6a8b803e3 100644 --- a/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts +++ b/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts @@ -1,4 +1,9 @@ -import { IDisposable, CompositeDisposable } from '../../../lifecycle'; +import { + IDisposable, + CompositeDisposable, + Disposable, + MutableDisposable, +} from '../../../lifecycle'; import { addDisposableListener, Emitter, Event } from '../../../events'; import { Tab } from '../tab/tab'; import { DockviewGroupPanel } from '../../dockviewGroupPanel'; @@ -6,12 +11,13 @@ import { VoidContainer } from './voidContainer'; import { toggleClass } from '../../../dom'; import { IDockviewPanel } from '../../dockviewPanel'; import { DockviewComponent } from '../../dockviewComponent'; -import { - DockviewGroupPanelModel, - WillShowOverlayLocationEvent, -} from '../../dockviewGroupPanelModel'; +import { WillShowOverlayLocationEvent } from '../../dockviewGroupPanelModel'; import { getPanelData } from '../../../dnd/dataTransfer'; import { Tabs } from './tabs'; +import { + createDropdownElementHandle, + DropdownElement, +} from './tabOverflowControl'; export interface TabDropIndexEvent { readonly event: DragEvent; @@ -68,6 +74,10 @@ export class TabsContainer private _hidden = false; + private dropdownPart: DropdownElement | null = null; + private _overflowTabs: string[] = []; + private readonly _dropdownDisposable = new MutableDisposable(); + private readonly _onDrop = new Emitter(); readonly onDrop: Event = this._onDrop.event; @@ -129,7 +139,13 @@ export class TabsContainer this.preActionsContainer = document.createElement('div'); this.preActionsContainer.className = 'dv-pre-actions-container'; - this.tabs = new Tabs(group, accessor); + this.tabs = new Tabs(group, accessor, { + showTabsOverflowControl: false, + }); + + this.tabs.onOverflowTabsChange((event) => { + this.toggleDropdown(event); + }); this.voidContainer = new VoidContainer(this.accessor, this.group); @@ -287,4 +303,93 @@ export class TabsContainer private updateClassnames(): void { toggleClass(this._element, 'dv-single-tab', this.size === 1); } + + private toggleDropdown(options: { tabs: string[]; reset: boolean }): void { + const tabs = options.reset ? [] : options.tabs; + this._overflowTabs = tabs; + + if (this._overflowTabs.length > 0 && this.dropdownPart) { + this.dropdownPart.update({ tabs: tabs.length }); + return; + } + + if (this._overflowTabs.length === 0) { + this._dropdownDisposable.dispose(); + return; + } + + const root = document.createElement('div'); + root.className = 'dv-tabs-overflow-dropdown-root'; + + const part = createDropdownElementHandle(); + part.update({ tabs: tabs.length }); + + this.dropdownPart = part; + + root.appendChild(part.element); + this.rightActionsContainer.prepend(root); + + this._dropdownDisposable.value = new CompositeDisposable( + Disposable.from(() => { + root.remove(); + this.dropdownPart?.dispose?.(); + this.dropdownPart = null; + }), + addDisposableListener( + root, + 'pointerdown', + (event) => { + event.preventDefault(); + }, + { capture: true } + ), + addDisposableListener(root, 'click', (event) => { + const el = document.createElement('div'); + el.style.overflow = 'auto'; + el.className = 'dv-tabs-overflow-container'; + + this.tabs.tabs + .filter((tab) => this._overflowTabs.includes(tab.panel.id)) + .map((tab) => { + const panelObject = this.group.panels.find( + (panel) => panel === tab.panel + )!; + + const tabComponent = + panelObject.view.createTabRenderer( + 'headerOverflow' + ); + + const child = tabComponent.element; + + const wrapper = document.createElement('div'); + toggleClass(wrapper, 'dv-tab', true); + toggleClass( + wrapper, + 'dv-active-tab', + panelObject.api.isActive + ); + toggleClass( + wrapper, + 'dv-inactive-tab', + !panelObject.api.isActive + ); + + wrapper.addEventListener('mousedown', () => { + this.accessor.popupService.close(); + tab.element.scrollIntoView(); + tab.panel.api.setActive(); + }); + wrapper.appendChild(child); + + el.appendChild(wrapper); + }); + + this.accessor.popupService.openPopover(el, { + x: event.clientX, + y: event.clientY, + }); + }) + ); + } } diff --git a/packages/dockview-core/src/dockview/dockviewComponent.scss b/packages/dockview-core/src/dockview/dockviewComponent.scss index b08c0ada0..3340e976a 100644 --- a/packages/dockview-core/src/dockview/dockviewComponent.scss +++ b/packages/dockview-core/src/dockview/dockviewComponent.scss @@ -19,7 +19,7 @@ .dv-groupview { &.dv-active-group { > .dv-tabs-and-actions-container - > .dv-tabs-panel + > .dv-scrollable > .dv-tabs-container > .dv-tab { &.dv-active-tab { @@ -38,7 +38,7 @@ } &.dv-inactive-group { > .dv-tabs-and-actions-container - > .dv-tabs-panel + > .dv-scrollable > .dv-tabs-container > .dv-tab { &.dv-active-tab { diff --git a/packages/dockview-core/src/scrollbar.scss b/packages/dockview-core/src/scrollbar.scss new file mode 100644 index 000000000..1276b51d8 --- /dev/null +++ b/packages/dockview-core/src/scrollbar.scss @@ -0,0 +1,28 @@ +.dv-scrollable { + position: relative; + overflow: hidden; + + .dv-scrollbar-horizontal { + position: absolute; + bottom: 0px; + left: 0px; + height: 4px; + border-radius: 2px; + background-color: transparent; + transition-property: background-color; + transition-timing-function: ease-in-out; + transition-duration: 1s; + transition-delay: 0s; + } + + &:hover, + &.dv-scrollable-resizing, + &.dv-scrollable-scrolling { + .dv-scrollbar-horizontal { + background-color: var( + --dv-scrollbar-background-color, + rgba(255, 255, 255, 0.25) + ); + } + } +} diff --git a/packages/dockview-core/src/scrollbar.ts b/packages/dockview-core/src/scrollbar.ts new file mode 100644 index 000000000..535ebd8e2 --- /dev/null +++ b/packages/dockview-core/src/scrollbar.ts @@ -0,0 +1,131 @@ +import { toggleClass, watchElementResize } from './dom'; +import { addDisposableListener } from './events'; +import { CompositeDisposable } from './lifecycle'; +import { clamp } from './math'; + +export class Scrollbar extends CompositeDisposable { + private _element: HTMLElement; + private _horizontalScrollbar: HTMLElement; + private _scrollLeft: number = 0; + private _animationTimer: any; + static MouseWheelSpeed = 1; + + get element(): HTMLElement { + return this._element; + } + + constructor(private readonly scrollableElement: HTMLElement) { + super(); + + this._element = document.createElement('div'); + this._element.className = 'dv-scrollable'; + + this._horizontalScrollbar = document.createElement('div'); + this._horizontalScrollbar.className = 'dv-scrollbar-horizontal'; + + this.element.appendChild(scrollableElement); + this.element.appendChild(this._horizontalScrollbar); + + this.addDisposables( + addDisposableListener(this.element, 'wheel', (event) => { + this._scrollLeft += event.deltaY * Scrollbar.MouseWheelSpeed; + + this.calculateScrollbarStyles(); + }), + addDisposableListener( + this._horizontalScrollbar, + 'pointerdown', + (event) => { + event.preventDefault(); + + toggleClass(this.element, 'dv-scrollable-scrolling', true); + + const originalClientX = event.clientX; + const originalScrollLeft = this._scrollLeft; + + const onPointerMove = (event: PointerEvent) => { + const deltaX = event.clientX - originalClientX; + + const { clientWidth } = this.element; + const { scrollWidth } = this.scrollableElement; + const p = clientWidth / scrollWidth; + + this._scrollLeft = originalScrollLeft + deltaX / p; + this.calculateScrollbarStyles(); + }; + + const onEnd = () => { + toggleClass( + this.element, + 'dv-scrollable-scrolling', + false + ); + + document.removeEventListener( + 'pointermove', + onPointerMove + ); + document.removeEventListener('pointerup', onEnd); + document.removeEventListener('pointercancel', onEnd); + }; + + document.addEventListener('pointermove', onPointerMove); + document.addEventListener('pointerup', onEnd); + document.addEventListener('pointercancel', onEnd); + } + ), + addDisposableListener(this.element, 'scroll', () => { + this.calculateScrollbarStyles(); + }), + addDisposableListener(this.scrollableElement, 'scroll', () => { + this._scrollLeft = this.scrollableElement.scrollLeft; + this.calculateScrollbarStyles(); + }), + watchElementResize(this.element, () => { + toggleClass(this.element, 'dv-scrollable-resizing', true); + + if (this._animationTimer) { + clearTimeout(this._animationTimer); + } + + this._animationTimer = setTimeout(() => { + clearTimeout(this._animationTimer); + toggleClass(this.element, 'dv-scrollable-resizing', false); + }, 500); + + this.calculateScrollbarStyles(); + }) + ); + } + + private calculateScrollbarStyles(): void { + const { clientWidth } = this.element; + const { scrollWidth } = this.scrollableElement; + + const hasScrollbar = scrollWidth > clientWidth; + + if (hasScrollbar) { + const px = clientWidth * (clientWidth / scrollWidth); + this._horizontalScrollbar.style.width = `${px}px`; + + this._scrollLeft = clamp( + this._scrollLeft, + 0, + this.scrollableElement.scrollWidth - clientWidth + ); + + this.scrollableElement.scrollLeft = this._scrollLeft; + + const percentageComplete = + this._scrollLeft / (scrollWidth - clientWidth); + + this._horizontalScrollbar.style.left = `${ + (clientWidth - px) * percentageComplete + }px`; + } else { + this._horizontalScrollbar.style.width = `0px`; + this._horizontalScrollbar.style.left = `0px`; + this._scrollLeft = 0; + } + } +} diff --git a/packages/dockview/src/__tests__/dockview/defaultTab.spec.tsx b/packages/dockview/src/__tests__/dockview/defaultTab.spec.tsx index 93da0a000..85a8fe0ce 100644 --- a/packages/dockview/src/__tests__/dockview/defaultTab.spec.tsx +++ b/packages/dockview/src/__tests__/dockview/defaultTab.spec.tsx @@ -19,6 +19,7 @@ describe('defaultTab', () => { render( { render( { render( { render( { render( { render( { render( Date: Mon, 3 Mar 2025 20:47:54 +0000 Subject: [PATCH 07/40] chore: update sonarqube build version --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 96a4de786..8f11fa57c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -27,7 +27,7 @@ jobs: - run: npm run build - run: npm run test:cov - name: SonarCloud Scan - uses: sonarsource/sonarqube-scan-action@v4.1.0 + uses: sonarsource/sonarqube-scan-action@v5 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} From 96d6947aa623783af6bf150a976dc1eb81f4a8c1 Mon Sep 17 00:00:00 2001 From: mathuo <6710312+mathuo@users.noreply.github.com> Date: Mon, 3 Mar 2025 20:53:59 +0000 Subject: [PATCH 08/40] feat: scrollbars --- .../__tests__/api/dockviewPanelApi.spec.ts | 3 + .../components/titlebar/tabsContainer.spec.ts | 14 ++++ .../dockview/dockviewGroupPanel.spec.ts | 3 + .../dockview/dockviewGroupPanelModel.spec.ts | 6 ++ .../__tests__/gridview/gridviewPanel.spec.ts | 1 + .../src/dockview/components/popupService.ts | 2 +- .../dockview/components/titlebar/tabs.scss | 10 +-- .../components/titlebar/tabsContainer.ts | 73 ++++++++++--------- .../src/dockview/dockviewComponent.ts | 4 + .../dockview-core/src/dockview/options.ts | 2 + packages/dockview-core/src/scrollbar.ts | 6 +- 11 files changed, 78 insertions(+), 46 deletions(-) diff --git a/packages/dockview-core/src/__tests__/api/dockviewPanelApi.spec.ts b/packages/dockview-core/src/__tests__/api/dockviewPanelApi.spec.ts index 0bc24eaa5..d4255ab22 100644 --- a/packages/dockview-core/src/__tests__/api/dockviewPanelApi.spec.ts +++ b/packages/dockview-core/src/__tests__/api/dockviewPanelApi.spec.ts @@ -10,6 +10,7 @@ describe('groupPanelApi', () => { onDidAddPanel: jest.fn(), onDidRemovePanel: jest.fn(), options: {}, + onDidOptionsChange: jest.fn(), }); const panelMock = jest.fn(() => { @@ -50,6 +51,7 @@ describe('groupPanelApi', () => { onDidAddPanel: jest.fn(), onDidRemovePanel: jest.fn(), options: {}, + onDidOptionsChange: jest.fn(), }); const groupViewPanel = new DockviewGroupPanel( @@ -82,6 +84,7 @@ describe('groupPanelApi', () => { onDidAddPanel: jest.fn(), onDidRemovePanel: jest.fn(), options: {}, + onDidOptionsChange: jest.fn(), }); const groupViewPanel = new DockviewGroupPanel( diff --git a/packages/dockview-core/src/__tests__/dockview/components/titlebar/tabsContainer.spec.ts b/packages/dockview-core/src/__tests__/dockview/components/titlebar/tabsContainer.spec.ts index bae8daa8d..bb79949cf 100644 --- a/packages/dockview-core/src/__tests__/dockview/components/titlebar/tabsContainer.spec.ts +++ b/packages/dockview-core/src/__tests__/dockview/components/titlebar/tabsContainer.spec.ts @@ -18,6 +18,7 @@ describe('tabsContainer', () => { onDidAddPanel: jest.fn(), onDidRemovePanel: jest.fn(), options: {}, + onDidOptionsChange: jest.fn(), }); const groupviewMock = jest.fn, []>( @@ -71,6 +72,7 @@ describe('tabsContainer', () => { onDidAddPanel: jest.fn(), onDidRemovePanel: jest.fn(), options: {}, + onDidOptionsChange: jest.fn(), }); const dropTargetContainer = document.createElement('div'); @@ -140,6 +142,7 @@ describe('tabsContainer', () => { onDidAddPanel: jest.fn(), onDidRemovePanel: jest.fn(), options: {}, + onDidOptionsChange: jest.fn(), }); const groupviewMock = jest.fn, []>( @@ -203,6 +206,7 @@ describe('tabsContainer', () => { onDidAddPanel: jest.fn(), onDidRemovePanel: jest.fn(), options: {}, + onDidOptionsChange: jest.fn(), }); const groupviewMock = jest.fn, []>( @@ -266,6 +270,7 @@ describe('tabsContainer', () => { onDidAddPanel: jest.fn(), onDidRemovePanel: jest.fn(), options: {}, + onDidOptionsChange: jest.fn(), }); const groupviewMock = jest.fn, []>( @@ -334,6 +339,7 @@ describe('tabsContainer', () => { onDidAddPanel: jest.fn(), onDidRemovePanel: jest.fn(), options: {}, + onDidOptionsChange: jest.fn(), }); const groupPanelMock = jest.fn(() => { @@ -398,6 +404,7 @@ describe('tabsContainer', () => { onDidAddPanel: jest.fn(), onDidRemovePanel: jest.fn(), options: {}, + onDidOptionsChange: jest.fn(), }); const groupPanelMock = jest.fn(() => { @@ -464,6 +471,7 @@ describe('tabsContainer', () => { element: document.createElement('div'), addFloatingGroup: jest.fn(), doSetGroupActive: jest.fn(), + onDidOptionsChange: jest.fn(), }); const groupPanelMock = jest.fn(() => { @@ -520,6 +528,7 @@ describe('tabsContainer', () => { element: document.createElement('div'), addFloatingGroup: jest.fn(), doSetGroupActive: jest.fn(), + onDidOptionsChange: jest.fn(), }); const groupPanelMock = jest.fn(() => { @@ -571,6 +580,7 @@ describe('tabsContainer', () => { element: document.createElement('div'), addFloatingGroup: jest.fn(), getGroupPanel: jest.fn(), + onDidOptionsChange: jest.fn(), }); const groupPanelMock = jest.fn(() => { @@ -627,6 +637,7 @@ describe('tabsContainer', () => { element: document.createElement('div'), addFloatingGroup: jest.fn(), getGroupPanel: jest.fn(), + onDidOptionsChange: jest.fn(), }); const groupPanelMock = jest.fn(() => { @@ -694,6 +705,7 @@ describe('tabsContainer', () => { element: document.createElement('div'), addFloatingGroup: jest.fn(), getGroupPanel: jest.fn(), + onDidOptionsChange: jest.fn(), }); const groupPanelMock = jest.fn(() => { @@ -761,6 +773,7 @@ describe('tabsContainer', () => { element: document.createElement('div'), addFloatingGroup: jest.fn(), getGroupPanel: jest.fn(), + onDidOptionsChange: jest.fn(), }); const groupPanelMock = jest.fn(() => { @@ -824,6 +837,7 @@ describe('tabsContainer', () => { const cut = new TabsContainer( fromPartial({ options: {}, + onDidOptionsChange: jest.fn(), }), fromPartial({}) ); diff --git a/packages/dockview-core/src/__tests__/dockview/dockviewGroupPanel.spec.ts b/packages/dockview-core/src/__tests__/dockview/dockviewGroupPanel.spec.ts index eb2ab5320..1a01b1d30 100644 --- a/packages/dockview-core/src/__tests__/dockview/dockviewGroupPanel.spec.ts +++ b/packages/dockview-core/src/__tests__/dockview/dockviewGroupPanel.spec.ts @@ -16,6 +16,7 @@ describe('dockviewGroupPanel', () => { onDidAddPanel: jest.fn(), onDidRemovePanel: jest.fn(), options: {}, + onDidOptionsChange: jest.fn(), }); const options = fromPartial({}); const cut = new DockviewGroupPanel(accessor, 'test_id', options); @@ -39,6 +40,7 @@ describe('dockviewGroupPanel', () => { detatch: jest.fn(), }, doSetGroupActive: jest.fn(), + onDidOptionsChange: jest.fn(), }); const options = fromPartial({}); @@ -81,6 +83,7 @@ describe('dockviewGroupPanel', () => { detatch: jest.fn(), }), options: {}, + onDidOptionsChange: jest.fn(), }); const options = fromPartial({}); const cut = new DockviewGroupPanel(accessor, 'test_id', options); diff --git a/packages/dockview-core/src/__tests__/dockview/dockviewGroupPanelModel.spec.ts b/packages/dockview-core/src/__tests__/dockview/dockviewGroupPanelModel.spec.ts index 719c26ca0..6e24cd53f 100644 --- a/packages/dockview-core/src/__tests__/dockview/dockviewGroupPanelModel.spec.ts +++ b/packages/dockview-core/src/__tests__/dockview/dockviewGroupPanelModel.spec.ts @@ -270,6 +270,7 @@ describe('dockviewGroupPanelModel', () => { document.createElement('div'), fromPartial({}) ), + onDidOptionsChange: () => ({ dispose: jest.fn() }), }); groupview = new DockviewGroupPanel(dockview, 'groupview-1', options); @@ -651,6 +652,7 @@ describe('dockviewGroupPanelModel', () => { getPanel: jest.fn(), onDidAddPanel: jest.fn(), onDidRemovePanel: jest.fn(), + onDidOptionsChange: jest.fn(), }); const groupviewMock = jest.fn, []>( @@ -713,6 +715,7 @@ describe('dockviewGroupPanelModel', () => { getPanel: jest.fn(), onDidAddPanel: jest.fn(), onDidRemovePanel: jest.fn(), + onDidOptionsChange: jest.fn(), }); const groupviewMock = jest.fn, []>( @@ -809,6 +812,7 @@ describe('dockviewGroupPanelModel', () => { document.createElement('div'), fromPartial({}) ), + onDidOptionsChange: jest.fn(), }); const groupView = fromPartial({ @@ -875,6 +879,7 @@ describe('dockviewGroupPanelModel', () => { document.createElement('div'), fromPartial({}) ), + onDidOptionsChange: jest.fn(), }); const groupviewMock = jest.fn, []>( @@ -948,6 +953,7 @@ describe('dockviewGroupPanelModel', () => { document.createElement('div'), fromPartial({}) ), + onDidOptionsChange: jest.fn(), }); const groupviewMock = jest.fn, []>( diff --git a/packages/dockview-core/src/__tests__/gridview/gridviewPanel.spec.ts b/packages/dockview-core/src/__tests__/gridview/gridviewPanel.spec.ts index cfae5ea6b..18ed7d1d6 100644 --- a/packages/dockview-core/src/__tests__/gridview/gridviewPanel.spec.ts +++ b/packages/dockview-core/src/__tests__/gridview/gridviewPanel.spec.ts @@ -8,6 +8,7 @@ describe('gridviewPanel', () => { onDidAddPanel: jest.fn(), onDidRemovePanel: jest.fn(), options: {}, + onDidOptionsChange: jest.fn(), } as any; }); diff --git a/packages/dockview-core/src/dockview/components/popupService.ts b/packages/dockview-core/src/dockview/components/popupService.ts index 1fb5a0996..58f0b9853 100644 --- a/packages/dockview-core/src/dockview/components/popupService.ts +++ b/packages/dockview-core/src/dockview/components/popupService.ts @@ -8,7 +8,7 @@ import { export class PopupService extends CompositeDisposable { private readonly _element: HTMLElement; private _active: HTMLElement | null = null; - private _activeDisposable = new MutableDisposable(); + private readonly _activeDisposable = new MutableDisposable(); constructor(private readonly root: HTMLElement) { super(); diff --git a/packages/dockview-core/src/dockview/components/titlebar/tabs.scss b/packages/dockview-core/src/dockview/components/titlebar/tabs.scss index 5b9e7487f..279659fac 100644 --- a/packages/dockview-core/src/dockview/components/titlebar/tabs.scss +++ b/packages/dockview-core/src/dockview/components/titlebar/tabs.scss @@ -1,5 +1,9 @@ .dv-tabs-container { + display: flex; + height: 100%; overflow: hidden; + scrollbar-width: thin; // firefox + &.dv-horizontal { .dv-tabs-container { .dv-tab { @@ -26,11 +30,6 @@ } } - display: flex; - height: 100%; - overflow: hidden; - scrollbar-width: thin; // firefox - &::-webkit-scrollbar { height: 3px; } @@ -57,7 +56,6 @@ } } - .dv-tabs-overflow-container { flex-direction: column; height: unset; diff --git a/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts b/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts index 6a8b803e3..e02533611 100644 --- a/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts +++ b/packages/dockview-core/src/dockview/components/titlebar/tabsContainer.ts @@ -140,11 +140,7 @@ export class TabsContainer this.preActionsContainer.className = 'dv-pre-actions-container'; this.tabs = new Tabs(group, accessor, { - showTabsOverflowControl: false, - }); - - this.tabs.onOverflowTabsChange((event) => { - this.toggleDropdown(event); + showTabsOverflowControl: !accessor.options.disableTabsOverflowList, }); this.voidContainer = new VoidContainer(this.accessor, this.group); @@ -156,6 +152,13 @@ export class TabsContainer this._element.appendChild(this.rightActionsContainer); this.addDisposables( + accessor.onDidOptionsChange(() => { + this.tabs.showTabsOverflowControl = + !accessor.options.disableTabsOverflowList; + }), + this.tabs.onOverflowTabsChange((event) => { + this.toggleDropdown(event); + }), this.tabs, this._onWillShowOverlay, this._onDrop, @@ -348,42 +351,40 @@ export class TabsContainer el.style.overflow = 'auto'; el.className = 'dv-tabs-overflow-container'; - this.tabs.tabs - .filter((tab) => this._overflowTabs.includes(tab.panel.id)) - .map((tab) => { - const panelObject = this.group.panels.find( - (panel) => panel === tab.panel - )!; + for (const tab of this.tabs.tabs.filter((tab) => + this._overflowTabs.includes(tab.panel.id) + )) { + const panelObject = this.group.panels.find( + (panel) => panel === tab.panel + )!; - const tabComponent = - panelObject.view.createTabRenderer( - 'headerOverflow' - ); + const tabComponent = + panelObject.view.createTabRenderer('headerOverflow'); - const child = tabComponent.element; + const child = tabComponent.element; - const wrapper = document.createElement('div'); - toggleClass(wrapper, 'dv-tab', true); - toggleClass( - wrapper, - 'dv-active-tab', - panelObject.api.isActive - ); - toggleClass( - wrapper, - 'dv-inactive-tab', - !panelObject.api.isActive - ); + const wrapper = document.createElement('div'); + toggleClass(wrapper, 'dv-tab', true); + toggleClass( + wrapper, + 'dv-active-tab', + panelObject.api.isActive + ); + toggleClass( + wrapper, + 'dv-inactive-tab', + !panelObject.api.isActive + ); - wrapper.addEventListener('mousedown', () => { - this.accessor.popupService.close(); - tab.element.scrollIntoView(); - tab.panel.api.setActive(); - }); - wrapper.appendChild(child); - - el.appendChild(wrapper); + wrapper.addEventListener('mousedown', () => { + this.accessor.popupService.close(); + tab.element.scrollIntoView(); + tab.panel.api.setActive(); }); + wrapper.appendChild(child); + + el.appendChild(wrapper); + } this.accessor.popupService.openPopover(el, { x: event.clientX, diff --git a/packages/dockview-core/src/dockview/dockviewComponent.ts b/packages/dockview-core/src/dockview/dockviewComponent.ts index 348e7725c..e9abdf237 100644 --- a/packages/dockview-core/src/dockview/dockviewComponent.ts +++ b/packages/dockview-core/src/dockview/dockviewComponent.ts @@ -330,6 +330,9 @@ export class DockviewComponent readonly onDidAddGroup: Event = this._onDidAddGroup.event; + private readonly _onDidOptionsChange = new Emitter(); + readonly onDidOptionsChange: Event = this._onDidOptionsChange.event; + private readonly _onDidActiveGroupChange = new Emitter< DockviewGroupPanel | undefined >(); @@ -434,6 +437,7 @@ export class DockviewComponent this._onDidActiveGroupChange, this._onUnhandledDragOverEvent, this._onDidMaximizedGroupChange, + this._onDidOptionsChange, this.onDidViewVisibilityChangeMicroTaskQueue(() => { this.updateWatermark(); }), diff --git a/packages/dockview-core/src/dockview/options.ts b/packages/dockview-core/src/dockview/options.ts index 4b096c941..bf0861006 100644 --- a/packages/dockview-core/src/dockview/options.ts +++ b/packages/dockview-core/src/dockview/options.ts @@ -72,6 +72,7 @@ export interface DockviewOptions { */ noPanelsOverlay?: 'emptyGroup' | 'watermark'; theme?: DockviewTheme; + disableTabsOverflowList?: boolean; } export interface DockviewDndOverlayEvent extends IAcceptableEvent { @@ -119,6 +120,7 @@ export const PROPERTY_KEYS_DOCKVIEW: (keyof DockviewOptions)[] = (() => { dndEdges: undefined, theme: undefined, gap: undefined, + disableTabsOverflowList: undefined, }; return Object.keys(properties) as (keyof DockviewOptions)[]; diff --git a/packages/dockview-core/src/scrollbar.ts b/packages/dockview-core/src/scrollbar.ts index 535ebd8e2..75db4e54a 100644 --- a/packages/dockview-core/src/scrollbar.ts +++ b/packages/dockview-core/src/scrollbar.ts @@ -4,11 +4,11 @@ import { CompositeDisposable } from './lifecycle'; import { clamp } from './math'; export class Scrollbar extends CompositeDisposable { - private _element: HTMLElement; - private _horizontalScrollbar: HTMLElement; + private readonly _element: HTMLElement; + private readonly _horizontalScrollbar: HTMLElement; private _scrollLeft: number = 0; private _animationTimer: any; - static MouseWheelSpeed = 1; + public static MouseWheelSpeed = 1; get element(): HTMLElement { return this._element; From c0119d65c0d7aedbe8a3b98296160ec75089d476 Mon Sep 17 00:00:00 2001 From: mathuo <6710312+mathuo@users.noreply.github.com> Date: Mon, 10 Mar 2025 21:17:21 +0000 Subject: [PATCH 09/40] chore: fixup docs --- .../dockview-core/src/api/component.api.ts | 13 ----- .../dockview/components/titlebar/tabs.scss | 29 ++++------ .../src/dockview/dockviewComponent.ts | 18 ------ .../dockview-core/src/dockview/options.ts | 5 -- packages/dockview-core/src/theme.scss | 56 +++++++++++-------- packages/docs/docs/core/panels/add.mdx | 4 ++ .../docs/docs/overview/getStarted/theme.mdx | 16 +++++- .../demo-dockview/src/gridActions.tsx | 47 +--------------- .../src/components/ui/reference/docRef.tsx | 2 + packages/docs/src/config/theme.config.ts | 16 +++--- 10 files changed, 71 insertions(+), 135 deletions(-) diff --git a/packages/dockview-core/src/api/component.api.ts b/packages/dockview-core/src/api/component.api.ts index 0b0850ed0..7f3c40299 100644 --- a/packages/dockview-core/src/api/component.api.ts +++ b/packages/dockview-core/src/api/component.api.ts @@ -629,19 +629,6 @@ export class DockviewApi implements CommonApi { return this.component.totalPanels; } - /** - * @deprecated dockview: dockviewComponent.gap has been deprecated. Use `theme` instead. This will be removed in a future version. - */ - get gap(): number { - return this.component.gap; - } - - /** - * @deprecated dockview: dockviewComponent.setGap has been deprecated. Use `theme` instead. This will be removed in a future version. - */ - setGap(gap: number | undefined): void { - this.component.updateOptions({ gap: gap }); - } /** * Invoked when the active group changes. May be undefined if no group is active. diff --git a/packages/dockview-core/src/dockview/components/titlebar/tabs.scss b/packages/dockview-core/src/dockview/components/titlebar/tabs.scss index 279659fac..df38c6741 100644 --- a/packages/dockview-core/src/dockview/components/titlebar/tabs.scss +++ b/packages/dockview-core/src/dockview/components/titlebar/tabs.scss @@ -43,17 +43,17 @@ &::-webkit-scrollbar-thumb { background: var(--dv-tabs-container-scrollbar-color); } +} - .dv-tab { - -webkit-user-drag: element; - outline: none; - padding: 0.25rem 0.5rem; - cursor: pointer; - position: relative; - box-sizing: border-box; - font-size: var(--dv-tab-font-size); - margin: var(--dv-tab-margin); - } +.dv-tab { + -webkit-user-drag: element; + outline: none; + padding: 0.25rem 0.5rem; + cursor: pointer; + position: relative; + box-sizing: border-box; + font-size: var(--dv-tab-font-size); + margin: var(--dv-tab-margin); } .dv-tabs-overflow-container { @@ -63,15 +63,6 @@ background-color: var(--dv-group-view-background-color); .dv-tab { - -webkit-user-drag: element; - outline: none; - padding: 0.25rem 0.5rem; - cursor: pointer; - position: relative; - box-sizing: border-box; - font-size: var(--dv-tab-font-size); - margin: var(--dv-tab-margin); - &:not(:last-child) { border-bottom: 1px solid var(--dv-tab-divider-color); } diff --git a/packages/dockview-core/src/dockview/dockviewComponent.ts b/packages/dockview-core/src/dockview/dockviewComponent.ts index e9abdf237..13bcb0ec2 100644 --- a/packages/dockview-core/src/dockview/dockviewComponent.ts +++ b/packages/dockview-core/src/dockview/dockviewComponent.ts @@ -195,10 +195,6 @@ export interface IDockviewComponent extends IBaseGrid { readonly totalPanels: number; readonly panels: IDockviewPanel[]; readonly orientation: Orientation; - /** - * @deprecated use `theme` instead. This will be removed in a future version - */ - readonly gap: number; readonly onDidDrop: Event; readonly onWillDrop: Event; readonly onWillShowOverlay: Event; @@ -373,13 +369,6 @@ export class DockviewComponent return this._api; } - get gap(): number { - console.warn( - 'dockview: dockviewComponent.gap has been deprecated. Use `theme` instead. This will be removed in a future version.' - ); - return this.gridview.margin; - } - get floatingGroups(): DockviewFloatingGroupPanel[] { return this._floatingGroups; } @@ -1180,13 +1169,6 @@ export class DockviewComponent override updateOptions(options: Partial): void { super.updateOptions(options); - if ('gap' in options) { - console.warn( - 'dockview: dockviewComponent.setGap has been deprecated. Use `theme` instead. This will be removed in a future version.' - ); - this.gridview.margin = options.gap ?? 0; - } - if ('floatingGroupBounds' in options) { for (const group of this._floatingGroups) { switch (options.floatingGroupBounds) { diff --git a/packages/dockview-core/src/dockview/options.ts b/packages/dockview-core/src/dockview/options.ts index bf0861006..86546934e 100644 --- a/packages/dockview-core/src/dockview/options.ts +++ b/packages/dockview-core/src/dockview/options.ts @@ -52,10 +52,6 @@ export interface DockviewOptions { }; popoutUrl?: string; defaultRenderer?: DockviewPanelRenderer; - /** - * @deprecated dockview: dockviewComponent.gap has been deprecated. Use `theme` instead. This will be removed in a future version. - */ - gap?: number; debug?: boolean; // #start dnd dndEdges?: false | DroptargetOverlayModel; @@ -119,7 +115,6 @@ export const PROPERTY_KEYS_DOCKVIEW: (keyof DockviewOptions)[] = (() => { noPanelsOverlay: undefined, dndEdges: undefined, theme: undefined, - gap: undefined, disableTabsOverflowList: undefined, }; diff --git a/packages/dockview-core/src/theme.scss b/packages/dockview-core/src/theme.scss index 0c98f152d..3b69d4c37 100644 --- a/packages/dockview-core/src/theme.scss +++ b/packages/dockview-core/src/theme.scss @@ -69,6 +69,8 @@ // --dv-separator-border: rgba(128, 128, 128, 0.35); --dv-paneview-header-border-color: rgb(51, 51, 51); + + --dv-scrollbar-background-color: rgba(0, 0, 0, 0.25); } .dockview-theme-dark { @@ -214,19 +216,21 @@ .dv-groupview { &.dv-active-group { > .dv-tabs-and-actions-container { - > .dv-tabs-container { - > .dv-tab.dv-active-tab { - position: relative; + > .dv-scrollable { + > .dv-tabs-container { + > .dv-tab.dv-active-tab { + position: relative; - &::after { - position: absolute; - left: 0px; - top: 0px; - content: ''; - width: 100%; - height: 1px; - background-color: #94527e; - z-index: 999; + &::after { + position: absolute; + left: 0px; + top: 0px; + content: ''; + width: 100%; + height: 1px; + background-color: #94527e; + z-index: 999; + } } } } @@ -234,19 +238,21 @@ } &.dv-inactive-group { > .dv-tabs-and-actions-container { - > .dv-tabs-container { - > .dv-tab.dv-active-tab { - position: relative; + > .dv-scrollable { + > .dv-tabs-container { + > .dv-tab.dv-active-tab { + position: relative; - &::after { - position: absolute; - left: 0px; - bottom: 0px; - content: ''; - width: 100%; - height: 1px; - background-color: #5e3d5a; - z-index: 999; + &::after { + position: absolute; + left: 0px; + bottom: 0px; + content: ''; + width: 100%; + height: 1px; + background-color: #5e3d5a; + z-index: 999; + } } } } @@ -444,6 +450,8 @@ padding: 10px; background-color: #f6f5f9; + --dv-scrollbar-background-color: rgba(0, 0, 0, 0.25); + .dv-resize-container { .dv-groupview { border: 2px solid rgb(255, 255, 255, 0.1); diff --git a/packages/docs/docs/core/panels/add.mdx b/packages/docs/docs/core/panels/add.mdx index cb151eb6e..aceba5e9c 100644 --- a/packages/docs/docs/core/panels/add.mdx +++ b/packages/docs/docs/core/panels/add.mdx @@ -13,6 +13,8 @@ Panels can be added through the dock api. + + ## Opening a Basic Panel To open a panel requires a unique `id` and the name of the `component` to render. @@ -95,6 +97,8 @@ See [Panel Rendering](/docs/core/panels/rendering). You can position a panel relative to an existing panel, group using `direction`. If you do not provide a reference panel or group then the panel will be positioned to the edge of the dock in the specified direction. + + #### Relative to another Panel ```ts diff --git a/packages/docs/docs/overview/getStarted/theme.mdx b/packages/docs/docs/overview/getStarted/theme.mdx index 6240340b9..131950277 100644 --- a/packages/docs/docs/overview/getStarted/theme.mdx +++ b/packages/docs/docs/overview/getStarted/theme.mdx @@ -34,8 +34,20 @@ Firstly, you should import `dockview.css`: ## Provided themes -`dockview` comes with a number of themes which are all CSS classes and can be found [here](https://github.com/mathuo/dockview/blob/master/packages/dockview-core/src/theme.scss). -To use a `dockview` theme the CSS must encapsulate the component. The current list of themes is: +`dockview` comes with a number of built-in themes. Each theme is represented as an object that can be imported. + +For dock components you should pass the theme object to the `theme` property, for other components such as split, pane and grid views you should +use set the themes associated CSS class to the `className` property. + +```tsx +import { themeAbyss } from "dockview"; + +// For dock components +theme={themeAbyss} + +// For other components +const {className} = themeAbyss; +``` diff --git a/packages/docs/sandboxes/react/dockview/demo-dockview/src/gridActions.tsx b/packages/docs/sandboxes/react/dockview/demo-dockview/src/gridActions.tsx index ec16135ed..5c13128f5 100644 --- a/packages/docs/sandboxes/react/dockview/demo-dockview/src/gridActions.tsx +++ b/packages/docs/sandboxes/react/dockview/demo-dockview/src/gridActions.tsx @@ -2,7 +2,7 @@ import { DockviewApi } from 'dockview'; import * as React from 'react'; import { defaultConfig, nextId } from './defaultLayout'; -import { createRoot, Root } from 'react-dom/client'; +import { createRoot } from 'react-dom/client'; import { PanelBuilder } from './panelBuilder'; let mount = document.querySelector('.popover-anchor') as HTMLElement | null; @@ -151,21 +151,6 @@ export const GridActions = (props: { props.api?.addGroup(); }; - // const [gap, setGap] = React.useState(undefined); - - const [overlayMode, setOverlayMode] = React.useState(false); - - // React.useEffect(() => { - // if (!props.api) { - // return; - // } - // if (typeof gap === 'number') { - // props.api.setGap(gap); - // } else { - // setGap(props.api.gap); - // } - // }, [gap, props.api]); - return (
@@ -200,23 +185,6 @@ export const GridActions = (props: { Use Custom Watermark - {/* - - */} @@ -230,19 +198,6 @@ export const GridActions = (props: { Reset - {/*
- Grid Gap - setGap(Number(event.target.value))} - /> - -
*/}
); }; diff --git a/packages/docs/src/components/ui/reference/docRef.tsx b/packages/docs/src/components/ui/reference/docRef.tsx index 8319d1721..2fccfdd9e 100644 --- a/packages/docs/src/components/ui/reference/docRef.tsx +++ b/packages/docs/src/components/ui/reference/docRef.tsx @@ -192,6 +192,8 @@ function filter(docs: TypeSystem.Type, methods: string[]) { .map((v) => filter(v, methods)) .flat(); } + + return [docs]; } if (docs.kind === 'class' || docs.kind === 'interface') { diff --git a/packages/docs/src/config/theme.config.ts b/packages/docs/src/config/theme.config.ts index 0946720df..e509c2178 100644 --- a/packages/docs/src/config/theme.config.ts +++ b/packages/docs/src/config/theme.config.ts @@ -12,43 +12,43 @@ import { export const themeConfig = [ { id: themeDark, - key: '**[dockview-theme-dark](/demo?theme=dockview-theme-dark)**', + key: '**[Dark](/demo?theme=dark)**', text: '', }, { id: themeLight, - key: '**[dockview-theme-light](/demo?theme=dockview-theme-light)**', + key: '**[Light](/demo?theme=light)**', text: '', }, { id: themeVisualStudio, - key: '**[dockview-theme-vs](/demo?theme=dockview-theme-vs)**', + key: '**[Visual Studio](/demo?theme=visualStudio)**', text: 'Based on [Visual Studio](https://visualstudio.microsoft.com)', }, { id: themeAbyss, - key: '**[dockview-theme-abyss](/demo?theme=dockview-theme-abyss)**', + key: '**[Abyss](/demo?theme=abyss)**', text: 'Based on [Visual Studio Code](https://code.visualstudio.com/docs/getstarted/themes) abyss theme', }, { id: themeDracula, - key: '**[dockview-theme-dracula](/demo?theme=dockview-theme-dracula)**', + key: '**[Dracula](/demo?theme=dracula)**', text: 'Based on [Visual Studio Code](https://code.visualstudio.com/docs/getstarted/themes) dracula theme', }, { id: themeReplit, - key: '**[dockview-theme-replit](/demo?theme=dockview-theme-replit)**', + key: '**[Replit](/demo?theme=replit)**', text: 'Based on [Replit](https://replit.com)', }, { id: themeLightSpaced, - key: '**[dockview-theme-replit](/demo?theme=dockview-theme-kraken)**', + key: '**[Light Spaced](/demo?theme=lightSpaced)**', text: '', }, { id: themeAbyssSpaced, - key: '**[dockview-theme-replit](/demo?theme=dockview-theme-kraken)**', + key: '**[Abyss Spaced](/demo?theme=abyssSpaced)**', text: '', }, ]; From e34fb439130f89c6c43f618dce1fc95ad3aee0bb Mon Sep 17 00:00:00 2001 From: mathuo <6710312+mathuo@users.noreply.github.com> Date: Mon, 10 Mar 2025 21:25:11 +0000 Subject: [PATCH 10/40] chore: theme docs --- .../docs/docs/overview/getStarted/theme.mdx | 9 +- packages/docs/src/generated/api.output.json | 961 +++++++++++++----- 2 files changed, 742 insertions(+), 228 deletions(-) diff --git a/packages/docs/docs/overview/getStarted/theme.mdx b/packages/docs/docs/overview/getStarted/theme.mdx index 131950277..dc025b57c 100644 --- a/packages/docs/docs/overview/getStarted/theme.mdx +++ b/packages/docs/docs/overview/getStarted/theme.mdx @@ -6,7 +6,7 @@ title: Theme import { CSSVariablesTable, ThemeTable } from '@site/src/components/cssVariables'; - +import { DocRef } from '@site/src/components/ui/reference/docRef'; Dockview components accept a `theme` property which is highly customizable, the theme is largly controlled through CSS however some properties can only be adjusted by direct editing variables of the `theme` object. @@ -55,6 +55,13 @@ const {className} = themeAbyss; The source code for all themes can be found [here](https://github.com/mathuo/dockview/blob/master/packages/dockview-core/src/theme.scss) and the associated CSS [here](https://github.com/mathuo/dockview/blob/master/packages/dockview-core/src/theme.scss). ::: +## Build your own theme + +You can define your own `DockviewTheme` object and pass it to the `theme` property. + + + + ## Customizing Theme The provided themes are controlled primarily through a long list of CSS variables which can be modified by the user either entirely for a new theme diff --git a/packages/docs/src/generated/api.output.json b/packages/docs/src/generated/api.output.json index d7e7ff7fd..40b17e55f 100644 --- a/packages/docs/src/generated/api.output.json +++ b/packages/docs/src/generated/api.output.json @@ -1831,20 +1831,6 @@ ] } }, - { - "name": "gap", - "code": "number", - "kind": "accessor", - "value": { - "name": "gap", - "code": "number", - "kind": "getSignature", - "returnType": { - "type": "intrinsic", - "value": "number" - } - } - }, { "name": "groups", "code": "DockviewGroupPanel[]", @@ -2509,7 +2495,7 @@ "summary": [ { "kind": "text", - "text": "Invoked before a group is dragged.\n\nCalling " + "text": "Invoked before a group is dragged.\r\n\r\nCalling " }, { "kind": "code", @@ -2540,7 +2526,7 @@ "summary": [ { "kind": "text", - "text": "Invoked before a group is dragged.\n\nCalling " + "text": "Invoked before a group is dragged.\r\n\r\nCalling " }, { "kind": "code", @@ -2563,7 +2549,7 @@ "summary": [ { "kind": "text", - "text": "Invoked before a panel is dragged.\n\nCalling " + "text": "Invoked before a panel is dragged.\r\n\r\nCalling " }, { "kind": "code", @@ -2594,7 +2580,7 @@ "summary": [ { "kind": "text", - "text": "Invoked before a panel is dragged.\n\nCalling " + "text": "Invoked before a panel is dragged.\r\n\r\nCalling " }, { "kind": "code", @@ -2617,7 +2603,7 @@ "summary": [ { "kind": "text", - "text": "Invoked when a Drag'n'Drop event occurs but before dockview handles it giving the user an opportunity to intecept and\nprevent the event from occuring using the standard " + "text": "Invoked when a Drag'n'Drop event occurs but before dockview handles it giving the user an opportunity to intecept and\r\nprevent the event from occuring using the standard " }, { "kind": "code", @@ -2625,7 +2611,7 @@ }, { "kind": "text", - "text": " syntax.\n\nPreventing certain events may causes unexpected behaviours, use carefully." + "text": " syntax.\r\n\r\nPreventing certain events may causes unexpected behaviours, use carefully." } ] }, @@ -2648,7 +2634,7 @@ "summary": [ { "kind": "text", - "text": "Invoked when a Drag'n'Drop event occurs but before dockview handles it giving the user an opportunity to intecept and\nprevent the event from occuring using the standard " + "text": "Invoked when a Drag'n'Drop event occurs but before dockview handles it giving the user an opportunity to intecept and\r\nprevent the event from occuring using the standard " }, { "kind": "code", @@ -2656,7 +2642,7 @@ }, { "kind": "text", - "text": " syntax.\n\nPreventing certain events may causes unexpected behaviours, use carefully." + "text": " syntax.\r\n\r\nPreventing certain events may causes unexpected behaviours, use carefully." } ] } @@ -2671,7 +2657,7 @@ "summary": [ { "kind": "text", - "text": "Invoked before an overlay is shown indicating a drop target.\n\nCalling " + "text": "Invoked before an overlay is shown indicating a drop target.\r\n\r\nCalling " }, { "kind": "code", @@ -2679,7 +2665,7 @@ }, { "kind": "text", - "text": " will prevent the overlay being shown and prevent\nthe any subsequent drop event." + "text": " will prevent the overlay being shown and prevent\r\nthe any subsequent drop event." } ] }, @@ -2702,7 +2688,7 @@ "summary": [ { "kind": "text", - "text": "Invoked before an overlay is shown indicating a drop target.\n\nCalling " + "text": "Invoked before an overlay is shown indicating a drop target.\r\n\r\nCalling " }, { "kind": "code", @@ -2710,7 +2696,7 @@ }, { "kind": "text", - "text": " will prevent the overlay being shown and prevent\nthe any subsequent drop event." + "text": " will prevent the overlay being shown and prevent\r\nthe any subsequent drop event." } ] } @@ -2906,7 +2892,7 @@ }, { "name": "addGroup", - "code": "(options?: GroupOptions & AddGroupOptionsWithPanel | GroupOptions & AddGroupOptionsWithGroup | GroupOptions & AbsolutePosition): DockviewGroupPanel", + "code": "(options?: GroupOptions & AbsolutePosition | GroupOptions & AddGroupOptionsWithPanel | GroupOptions & AddGroupOptionsWithGroup): DockviewGroupPanel", "kind": "method", "signature": [ { @@ -2923,10 +2909,25 @@ "parameters": [ { "name": "options", - "code": "options?: GroupOptions & AddGroupOptionsWithPanel | GroupOptions & AddGroupOptionsWithGroup | GroupOptions & AbsolutePosition", + "code": "options?: GroupOptions & AbsolutePosition | GroupOptions & AddGroupOptionsWithPanel | GroupOptions & AddGroupOptionsWithGroup", "type": { "type": "or", "values": [ + { + "type": "intersection", + "values": [ + { + "type": "reference", + "value": "GroupOptions", + "source": "dockview-core" + }, + { + "type": "reference", + "value": "AbsolutePosition", + "source": "dockview-core" + } + ] + }, { "type": "intersection", "values": [ @@ -2956,21 +2957,6 @@ "source": "dockview-core" } ] - }, - { - "type": "intersection", - "values": [ - { - "type": "reference", - "value": "GroupOptions", - "source": "dockview-core" - }, - { - "type": "reference", - "value": "AbsolutePosition", - "source": "dockview-core" - } - ] } ] }, @@ -2982,7 +2968,7 @@ "value": "DockviewGroupPanel", "source": "dockview-core" }, - "code": "(options?: GroupOptions & AddGroupOptionsWithPanel | GroupOptions & AddGroupOptionsWithGroup | GroupOptions & AbsolutePosition): DockviewGroupPanel", + "code": "(options?: GroupOptions & AbsolutePosition | GroupOptions & AddGroupOptionsWithPanel | GroupOptions & AddGroupOptionsWithGroup): DockviewGroupPanel", "kind": "callSignature" } ], @@ -3936,43 +3922,6 @@ ] } }, - { - "name": "setGap", - "code": "(gap: number | undefined): void", - "kind": "method", - "signature": [ - { - "name": "setGap", - "typeParameters": [], - "parameters": [ - { - "name": "gap", - "code": "gap: number | undefined", - "type": { - "type": "or", - "values": [ - { - "type": "intrinsic", - "value": "number" - }, - { - "type": "intrinsic", - "value": "undefined" - } - ] - }, - "kind": "parameter" - } - ], - "returnType": { - "type": "intrinsic", - "value": "void" - }, - "code": "(gap: number | undefined): void", - "kind": "callSignature" - } - ] - }, { "name": "toJSON", "code": "(): SerializedDockview", @@ -4431,6 +4380,25 @@ "isReadonly": true } }, + { + "name": "onDidOptionsChange", + "code": "Event", + "kind": "property", + "type": { + "type": "reference", + "value": "Event", + "source": "dockview-core", + "typeArguments": [ + { + "type": "intrinsic", + "value": "void" + } + ] + }, + "flags": { + "isReadonly": true + } + }, { "name": "onDidRemove", "code": "Event", @@ -4623,6 +4591,32 @@ "isReadonly": true } }, + { + "name": "popupService", + "code": "PopupService", + "kind": "property", + "type": { + "type": "reference", + "value": "PopupService", + "source": "dockview-core" + }, + "flags": { + "isReadonly": true + } + }, + { + "name": "rootDropTargetContainer", + "code": "DropTargetAnchorContainer", + "kind": "property", + "type": { + "type": "reference", + "value": "DropTargetAnchorContainer", + "source": "dockview-core" + }, + "flags": { + "isReadonly": true + } + }, { "name": "activeGroup", "code": "BaseGrid.T | undefined", @@ -4734,20 +4728,6 @@ } } }, - { - "name": "gap", - "code": "number", - "kind": "accessor", - "value": { - "name": "gap", - "code": "number", - "kind": "getSignature", - "returnType": { - "type": "intrinsic", - "value": "number" - } - } - }, { "name": "groups", "code": "BaseGrid.T[]", @@ -5067,7 +5047,7 @@ }, { "name": "addGroup", - "code": "(options?: GroupOptions & AddGroupOptionsWithPanel | GroupOptions & AddGroupOptionsWithGroup | GroupOptions & AbsolutePosition): DockviewGroupPanel", + "code": "(options?: GroupOptions & AbsolutePosition | GroupOptions & AddGroupOptionsWithPanel | GroupOptions & AddGroupOptionsWithGroup): DockviewGroupPanel", "kind": "method", "signature": [ { @@ -5076,10 +5056,25 @@ "parameters": [ { "name": "options", - "code": "options?: GroupOptions & AddGroupOptionsWithPanel | GroupOptions & AddGroupOptionsWithGroup | GroupOptions & AbsolutePosition", + "code": "options?: GroupOptions & AbsolutePosition | GroupOptions & AddGroupOptionsWithPanel | GroupOptions & AddGroupOptionsWithGroup", "type": { "type": "or", "values": [ + { + "type": "intersection", + "values": [ + { + "type": "reference", + "value": "GroupOptions", + "source": "dockview-core" + }, + { + "type": "reference", + "value": "AbsolutePosition", + "source": "dockview-core" + } + ] + }, { "type": "intersection", "values": [ @@ -5109,21 +5104,6 @@ "source": "dockview-core" } ] - }, - { - "type": "intersection", - "values": [ - { - "type": "reference", - "value": "GroupOptions", - "source": "dockview-core" - }, - { - "type": "reference", - "value": "AbsolutePosition", - "source": "dockview-core" - } - ] } ] }, @@ -5135,7 +5115,7 @@ "value": "DockviewGroupPanel", "source": "dockview-core" }, - "code": "(options?: GroupOptions & AddGroupOptionsWithPanel | GroupOptions & AddGroupOptionsWithGroup | GroupOptions & AbsolutePosition): DockviewGroupPanel", + "code": "(options?: GroupOptions & AbsolutePosition | GroupOptions & AddGroupOptionsWithPanel | GroupOptions & AddGroupOptionsWithGroup): DockviewGroupPanel", "kind": "callSignature" } ] @@ -7858,6 +7838,30 @@ } } }, + { + "name": "dropTargetContainer", + "code": "DropTargetAnchorContainer | 'null'", + "kind": "accessor", + "value": { + "name": "dropTargetContainer", + "code": "DropTargetAnchorContainer | 'null'", + "kind": "getSignature", + "returnType": { + "type": "or", + "values": [ + { + "type": "reference", + "value": "DropTargetAnchorContainer", + "source": "dockview-core" + }, + { + "type": "literal", + "value": null + } + ] + } + } + }, { "name": "element", "code": "HTMLElement", @@ -9893,7 +9897,7 @@ }, { "name": "onDidDrop", - "code": "Event", + "code": "Event", "kind": "property", "type": { "type": "reference", @@ -9902,7 +9906,7 @@ "typeArguments": [ { "type": "reference", - "value": "PaneviewDropEvent", + "value": "PaneviewDidDropEvent", "source": "dockview-core" } ] @@ -15119,7 +15123,7 @@ }, { "name": "onDidDrop", - "code": "Event", + "code": "Event", "kind": "property", "type": { "type": "reference", @@ -15128,7 +15132,7 @@ "typeArguments": [ { "type": "reference", - "value": "PaneviewDropEvent", + "value": "PaneviewDidDropEvent", "source": "dockview-core" } ] @@ -16462,7 +16466,7 @@ }, { "name": "onDidDrop", - "code": "Event", + "code": "Event", "kind": "accessor", "value": { "name": "onDidDrop", @@ -16474,7 +16478,7 @@ } ] }, - "code": "Event", + "code": "Event", "kind": "getSignature", "returnType": { "type": "reference", @@ -16483,7 +16487,7 @@ "typeArguments": [ { "type": "reference", - "value": "PaneviewDropEvent", + "value": "PaneviewDidDropEvent", "source": "dockview-core" } ] @@ -17280,7 +17284,7 @@ }, { "name": "onDidDrop", - "code": "Event", + "code": "Event", "kind": "property", "type": { "type": "reference", @@ -17289,7 +17293,7 @@ "typeArguments": [ { "type": "reference", - "value": "PaneviewDropEvent", + "value": "PaneviewDidDropEvent", "source": "dockview-core" } ] @@ -18656,6 +18660,130 @@ "BasePanelView" ] }, + "PaneviewUnhandledDragOverEvent": { + "kind": "class", + "name": "PaneviewUnhandledDragOverEvent", + "children": [ + { + "name": "constructor", + "kind": "constructor", + "code": "" + }, + { + "name": "getData", + "code": "(): PaneTransfer | undefined", + "kind": "property", + "type": { + "type": "reflection", + "value": { + "name": "__type", + "code": "(): PaneTransfer | undefined", + "kind": "typeLiteral", + "signatures": [ + { + "name": "__type", + "typeParameters": [], + "parameters": [], + "returnType": { + "type": "or", + "values": [ + { + "type": "reference", + "value": "PaneTransfer", + "source": "dockview-core" + }, + { + "type": "intrinsic", + "value": "undefined" + } + ] + }, + "code": "(): PaneTransfer | undefined", + "kind": "callSignature" + } + ] + } + }, + "flags": { + "isReadonly": true + } + }, + { + "name": "nativeEvent", + "code": "DragEvent", + "kind": "property", + "type": { + "type": "reference", + "value": "DragEvent", + "source": "typescript" + }, + "flags": { + "isReadonly": true + } + }, + { + "name": "panel", + "code": "IPaneviewPanel", + "kind": "property", + "type": { + "type": "reference", + "value": "IPaneviewPanel", + "source": "dockview-core" + }, + "flags": { + "isReadonly": true + } + }, + { + "name": "position", + "code": "Position", + "kind": "property", + "type": { + "type": "reference", + "value": "Position", + "source": "dockview-core" + }, + "flags": { + "isReadonly": true + } + }, + { + "name": "isAccepted", + "code": "boolean", + "kind": "accessor", + "value": { + "name": "isAccepted", + "code": "boolean", + "kind": "getSignature", + "returnType": { + "type": "intrinsic", + "value": "boolean" + } + } + }, + { + "name": "accept", + "code": "(): void", + "kind": "method", + "signature": [ + { + "name": "accept", + "typeParameters": [], + "parameters": [], + "returnType": { + "type": "intrinsic", + "value": "void" + }, + "code": "(): void", + "kind": "callSignature" + } + ] + } + ], + "extends": [ + "AcceptableEvent" + ] + }, "Splitview": { "kind": "class", "name": "Splitview", @@ -19571,7 +19699,7 @@ "summary": [ { "kind": "text", - "text": "Invoked whenever any aspect of the layout changes.\nIf listening to this event it may be worth debouncing ouputs." + "text": "Invoked whenever any aspect of the layout changes.\r\nIf listening to this event it may be worth debouncing ouputs." } ] }, @@ -19593,7 +19721,7 @@ "summary": [ { "kind": "text", - "text": "Invoked whenever any aspect of the layout changes.\nIf listening to this event it may be worth debouncing ouputs." + "text": "Invoked whenever any aspect of the layout changes.\r\nIf listening to this event it may be worth debouncing ouputs." } ] } @@ -20189,7 +20317,7 @@ }, { "kind": "text", - "text": " method\nfor the subsequent resize." + "text": " method\r\nfor the subsequent resize." } ] }, @@ -20251,7 +20379,7 @@ }, { "kind": "text", - "text": " method\nfor the subsequent resize." + "text": " method\r\nfor the subsequent resize." } ] } @@ -21730,26 +21858,6 @@ "kind": "constructor", "code": "" }, - { - "name": "onChanged", - "code": "Event", - "kind": "property", - "type": { - "type": "reference", - "value": "Event", - "source": "dockview-core", - "typeArguments": [ - { - "type": "reference", - "value": "MouseEvent", - "source": "typescript" - } - ] - }, - "flags": { - "isReadonly": true - } - }, { "name": "onDragStart", "code": "Event", @@ -21790,6 +21898,26 @@ "isReadonly": true } }, + { + "name": "onPointerDown", + "code": "Event", + "kind": "property", + "type": { + "type": "reference", + "value": "Event", + "source": "dockview-core", + "typeArguments": [ + { + "type": "reference", + "value": "MouseEvent", + "source": "typescript" + } + ] + }, + "flags": { + "isReadonly": true + } + }, { "name": "onWillShowOverlay", "code": "Event", @@ -24376,7 +24504,7 @@ }, { "kind": "text", - "text": ".\nCall " + "text": ".\r\nCall " }, { "kind": "code", @@ -24413,6 +24541,40 @@ "isOptional": true } }, + { + "name": "disableTabsOverflowList", + "code": "boolean", + "kind": "property", + "type": { + "type": "intrinsic", + "value": "boolean" + }, + "flags": { + "isOptional": true + } + }, + { + "name": "dndEdges", + "code": "DroptargetOverlayModel | 'false'", + "kind": "property", + "type": { + "type": "or", + "values": [ + { + "type": "reference", + "value": "DroptargetOverlayModel", + "source": "dockview-core" + }, + { + "type": "literal", + "value": false + } + ] + }, + "flags": { + "isOptional": true + } + }, { "name": "floatingGroupBounds", "code": "{ minimumHeightWithinViewport?: number, minimumWidthWithinViewport?: number } | 'boundedWithinViewport'", @@ -24464,26 +24626,6 @@ "isOptional": true } }, - { - "name": "gap", - "code": "number", - "kind": "property", - "type": { - "type": "intrinsic", - "value": "number" - }, - "flags": { - "isOptional": true - }, - "comment": { - "summary": [ - { - "kind": "text", - "text": "Pixel gap between groups" - } - ] - } - }, { "name": "hideBorders", "code": "boolean", @@ -24568,6 +24710,28 @@ }, "flags": { "isOptional": true + }, + "comment": { + "summary": [], + "blockTags": [ + { + "tag": "@deprecated", + "content": [ + { + "kind": "text", + "text": "use " + }, + { + "kind": "code", + "text": "`dndEdges`" + }, + { + "kind": "text", + "text": " instead. To be removed in a future version." + } + ] + } + ] } }, { @@ -24590,6 +24754,19 @@ "flags": { "isOptional": true } + }, + { + "name": "theme", + "code": "DockviewTheme", + "kind": "property", + "type": { + "type": "reference", + "value": "DockviewTheme", + "source": "dockview-core" + }, + "flags": { + "isOptional": true + } } ], "extends": [] @@ -25580,6 +25757,78 @@ ], "extends": [] }, + "DockviewTheme": { + "kind": "interface", + "name": "DockviewTheme", + "children": [ + { + "name": "className", + "code": "string", + "kind": "property", + "type": { + "type": "intrinsic", + "value": "string" + }, + "flags": {} + }, + { + "name": "dndOverlayMounting", + "code": "'relative' | 'absolute'", + "kind": "property", + "type": { + "type": "or", + "values": [ + { + "type": "literal", + "value": "relative" + }, + { + "type": "literal", + "value": "absolute" + } + ] + }, + "flags": { + "isOptional": true + } + }, + { + "name": "gap", + "code": "number", + "kind": "property", + "type": { + "type": "intrinsic", + "value": "number" + }, + "flags": { + "isOptional": true + } + }, + { + "name": "includeHeaderWhenHoverOverContent", + "code": "boolean", + "kind": "property", + "type": { + "type": "intrinsic", + "value": "boolean" + }, + "flags": { + "isOptional": true + } + }, + { + "name": "name", + "code": "string", + "kind": "property", + "type": { + "type": "intrinsic", + "value": "string" + }, + "flags": {} + } + ], + "extends": [] + }, "ExpansionEvent": { "kind": "interface", "name": "ExpansionEvent", @@ -28587,18 +28836,6 @@ "isReadonly": true } }, - { - "name": "gap", - "code": "number", - "kind": "property", - "type": { - "type": "intrinsic", - "value": "number" - }, - "flags": { - "isReadonly": true - } - }, { "name": "getGroupPanel", "code": "(id: string): IDockviewPanel | undefined", @@ -29264,7 +29501,7 @@ }, { "name": "addGroup", - "code": "(options?: GroupOptions & AddGroupOptionsWithPanel | GroupOptions & AddGroupOptionsWithGroup | GroupOptions & AbsolutePosition): DockviewGroupPanel", + "code": "(options?: GroupOptions & AbsolutePosition | GroupOptions & AddGroupOptionsWithPanel | GroupOptions & AddGroupOptionsWithGroup): DockviewGroupPanel", "kind": "method", "signature": [ { @@ -29273,10 +29510,25 @@ "parameters": [ { "name": "options", - "code": "options?: GroupOptions & AddGroupOptionsWithPanel | GroupOptions & AddGroupOptionsWithGroup | GroupOptions & AbsolutePosition", + "code": "options?: GroupOptions & AbsolutePosition | GroupOptions & AddGroupOptionsWithPanel | GroupOptions & AddGroupOptionsWithGroup", "type": { "type": "or", "values": [ + { + "type": "intersection", + "values": [ + { + "type": "reference", + "value": "GroupOptions", + "source": "dockview-core" + }, + { + "type": "reference", + "value": "AbsolutePosition", + "source": "dockview-core" + } + ] + }, { "type": "intersection", "values": [ @@ -29306,21 +29558,6 @@ "source": "dockview-core" } ] - }, - { - "type": "intersection", - "values": [ - { - "type": "reference", - "value": "GroupOptions", - "source": "dockview-core" - }, - { - "type": "reference", - "value": "AbsolutePosition", - "source": "dockview-core" - } - ] } ] }, @@ -29332,7 +29569,7 @@ "value": "DockviewGroupPanel", "source": "dockview-core" }, - "code": "(options?: GroupOptions & AddGroupOptionsWithPanel | GroupOptions & AddGroupOptionsWithGroup | GroupOptions & AbsolutePosition): DockviewGroupPanel", + "code": "(options?: GroupOptions & AbsolutePosition | GroupOptions & AddGroupOptionsWithPanel | GroupOptions & AddGroupOptionsWithGroup): DockviewGroupPanel", "kind": "callSignature" } ] @@ -35173,7 +35410,7 @@ }, { "name": "onDidDrop", - "code": "Event", + "code": "Event", "kind": "property", "type": { "type": "reference", @@ -35182,7 +35419,7 @@ "typeArguments": [ { "type": "reference", - "value": "PaneviewDropEvent", + "value": "PaneviewDidDropEvent", "source": "dockview-core" } ] @@ -36964,7 +37201,7 @@ }, { "name": "init", - "code": "(parameters: GroupPanelPartInitParameters): void", + "code": "(parameters: TabPartInitParameters): void", "kind": "method", "signature": [ { @@ -36973,10 +37210,10 @@ "parameters": [ { "name": "parameters", - "code": "parameters: GroupPanelPartInitParameters", + "code": "parameters: TabPartInitParameters", "type": { "type": "reference", - "value": "GroupPanelPartInitParameters", + "value": "TabPartInitParameters", "source": "dockview-core" }, "kind": "parameter" @@ -36986,7 +37223,7 @@ "type": "intrinsic", "value": "void" }, - "code": "(parameters: GroupPanelPartInitParameters): void", + "code": "(parameters: TabPartInitParameters): void", "kind": "callSignature" } ] @@ -38758,6 +38995,115 @@ "PanelInitParameters" ] }, + "PaneviewDndOverlayEvent": { + "kind": "interface", + "name": "PaneviewDndOverlayEvent", + "children": [ + { + "name": "getData", + "code": "(): PaneTransfer | undefined", + "kind": "property", + "type": { + "type": "reflection", + "value": { + "name": "__type", + "code": "(): PaneTransfer | undefined", + "kind": "typeLiteral", + "signatures": [ + { + "name": "__type", + "typeParameters": [], + "parameters": [], + "returnType": { + "type": "or", + "values": [ + { + "type": "reference", + "value": "PaneTransfer", + "source": "dockview-core" + }, + { + "type": "intrinsic", + "value": "undefined" + } + ] + }, + "code": "(): PaneTransfer | undefined", + "kind": "callSignature" + } + ] + } + }, + "flags": {} + }, + { + "name": "isAccepted", + "code": "boolean", + "kind": "property", + "type": { + "type": "intrinsic", + "value": "boolean" + }, + "flags": { + "isReadonly": true + } + }, + { + "name": "nativeEvent", + "code": "DragEvent", + "kind": "property", + "type": { + "type": "reference", + "value": "DragEvent", + "source": "typescript" + }, + "flags": {} + }, + { + "name": "panel", + "code": "IPaneviewPanel", + "kind": "property", + "type": { + "type": "reference", + "value": "IPaneviewPanel", + "source": "dockview-core" + }, + "flags": {} + }, + { + "name": "position", + "code": "Position", + "kind": "property", + "type": { + "type": "reference", + "value": "Position", + "source": "dockview-core" + }, + "flags": {} + }, + { + "name": "accept", + "code": "(): void", + "kind": "method", + "signature": [ + { + "name": "accept", + "typeParameters": [], + "parameters": [], + "returnType": { + "type": "intrinsic", + "value": "void" + }, + "code": "(): void", + "kind": "callSignature" + } + ] + } + ], + "extends": [ + "IAcceptableEvent" + ] + }, "PaneviewDropEvent": { "kind": "interface", "name": "PaneviewDropEvent", @@ -40466,14 +40812,26 @@ }, { "name": "orientation", - "code": "Orientation", + "code": "Orientation.VERTICAL | Orientation.HORIZONTAL", "kind": "property", "type": { - "type": "reference", - "value": "Orientation", - "source": "dockview-core" + "type": "or", + "values": [ + { + "type": "reference", + "value": "Orientation.VERTICAL", + "source": "dockview-core" + }, + { + "type": "reference", + "value": "Orientation.HORIZONTAL", + "source": "dockview-core" + } + ] }, - "flags": {} + "flags": { + "isOptional": true + } }, { "name": "proportionalLayout", @@ -40604,14 +40962,26 @@ }, { "name": "orientation", - "code": "Orientation", + "code": "Orientation.VERTICAL | Orientation.HORIZONTAL", "kind": "property", "type": { - "type": "reference", - "value": "Orientation", - "source": "dockview-core" + "type": "or", + "values": [ + { + "type": "reference", + "value": "Orientation.VERTICAL", + "source": "dockview-core" + }, + { + "type": "reference", + "value": "Orientation.HORIZONTAL", + "source": "dockview-core" + } + ] }, - "flags": {} + "flags": { + "isOptional": true + } }, { "name": "proportionalLayout", @@ -41168,6 +41538,69 @@ ], "extends": [] }, + "TabPartInitParameters": { + "kind": "interface", + "name": "TabPartInitParameters", + "children": [ + { + "name": "api", + "code": "DockviewPanelApi", + "kind": "property", + "type": { + "type": "reference", + "value": "DockviewPanelApi", + "source": "dockview-core" + }, + "flags": {} + }, + { + "name": "containerApi", + "code": "DockviewApi", + "kind": "property", + "type": { + "type": "reference", + "value": "DockviewApi", + "source": "dockview-core" + }, + "flags": {} + }, + { + "name": "params", + "code": "Parameters", + "kind": "property", + "type": { + "type": "reference", + "value": "Parameters", + "source": "dockview-core" + }, + "flags": {} + }, + { + "name": "tabLocation", + "code": "TabLocation", + "kind": "property", + "type": { + "type": "reference", + "value": "TabLocation", + "source": "dockview-core" + }, + "flags": {} + }, + { + "name": "title", + "code": "string", + "kind": "property", + "type": { + "type": "intrinsic", + "value": "string" + }, + "flags": {} + } + ], + "extends": [ + "GroupPanelPartInitParameters" + ] + }, "TitleEvent": { "kind": "interface", "name": "TitleEvent", @@ -41391,7 +41824,7 @@ "summary": [ { "kind": "text", - "text": "If true then add the panel without setting it as the active panel.\n\nDefaults to " + "text": "If true then add the panel without setting it as the active panel.\r\n\r\nDefaults to " }, { "kind": "code", @@ -41458,7 +41891,7 @@ "summary": [ { "kind": "text", - "text": "The rendering mode of the panel.\n\nThis dictates what happens to the HTML of the panel when it is hidden." + "text": "The rendering mode of the panel.\r\n\r\nThis dictates what happens to the HTML of the panel when it is hidden." } ] } @@ -41498,7 +41931,7 @@ "summary": [ { "kind": "text", - "text": "The title for the panel which can be accessed within both the tab and component.\n\nIf using the default tab renderer this title will be displayed in the tab." + "text": "The title for the panel which can be accessed within both the tab and component.\r\n\r\nIf using the default tab renderer this title will be displayed in the tab." } ] } @@ -41538,6 +41971,14 @@ }, "Direction": { "name": "Direction", + "comment": { + "summary": [ + { + "kind": "text", + "text": "A direction in which a panel can be moved or placed relative to another panel." + } + ] + }, "code": "'within' | 'below' | 'above' | 'right' | 'left'", "typeParameters": [], "type": { @@ -41914,18 +42355,44 @@ }, "IDockviewPanelHeaderProps": { "name": "IDockviewPanelHeaderProps", - "code": "IGroupPanelBaseProps", + "code": "{ tabLocation: TabLocation } & IGroupPanelBaseProps", "typeParameters": [], "type": { - "type": "reference", - "value": "IGroupPanelBaseProps", - "source": "dockview-core", - "typeArguments": [ + "type": "intersection", + "values": [ + { + "type": "reflection", + "value": { + "name": "__type", + "code": "{ tabLocation: TabLocation }", + "kind": "typeLiteral", + "properties": [ + { + "name": "tabLocation", + "code": "TabLocation", + "kind": "property", + "type": { + "type": "reference", + "value": "TabLocation", + "source": "dockview-core" + }, + "flags": {} + } + ] + } + }, { "type": "reference", - "value": "T", + "value": "IGroupPanelBaseProps", "source": "dockview-core", - "refersToTypeParameter": true + "typeArguments": [ + { + "type": "reference", + "value": "T", + "source": "dockview-core", + "refersToTypeParameter": true + } + ] } ] }, @@ -42181,6 +42648,46 @@ "code": "", "kind": "variable" }, + "themeAbyss": { + "name": "themeAbyss", + "code": "", + "kind": "variable" + }, + "themeAbyssSpaced": { + "name": "themeAbyssSpaced", + "code": "", + "kind": "variable" + }, + "themeDark": { + "name": "themeDark", + "code": "", + "kind": "variable" + }, + "themeDracula": { + "name": "themeDracula", + "code": "", + "kind": "variable" + }, + "themeLight": { + "name": "themeLight", + "code": "", + "kind": "variable" + }, + "themeLightSpaced": { + "name": "themeLightSpaced", + "code": "", + "kind": "variable" + }, + "themeReplit": { + "name": "themeReplit", + "code": "", + "kind": "variable" + }, + "themeVisualStudio": { + "name": "themeVisualStudio", + "code": "", + "kind": "variable" + }, "createDockview": { "name": "createDockview", "code": "(element: HTMLElement, options: DockviewComponentOptions): DockviewApi", @@ -43528,7 +44035,7 @@ }, { "name": "onDidDrop", - "code": "(event: PaneviewDropEvent): void", + "code": "(event: PaneviewDidDropEvent): void", "kind": "method", "signature": [ { @@ -43537,10 +44044,10 @@ "parameters": [ { "name": "event", - "code": "event: PaneviewDropEvent", + "code": "event: PaneviewDidDropEvent", "type": { "type": "reference", - "value": "PaneviewDropEvent", + "value": "PaneviewDidDropEvent", "source": "dockview-core" }, "kind": "parameter" @@ -43550,7 +44057,7 @@ "type": "intrinsic", "value": "void" }, - "code": "(event: PaneviewDropEvent): void", + "code": "(event: PaneviewDidDropEvent): void", "kind": "callSignature" } ] From 090f2d26f896f29c865ae0683b1e0320f9140688 Mon Sep 17 00:00:00 2001 From: mathuo <6710312+mathuo@users.noreply.github.com> Date: Mon, 10 Mar 2025 21:28:33 +0000 Subject: [PATCH 11/40] chore: docs --- packages/docs/docs/core/dnd/dragAndDrop.mdx | 4 ++-- .../docs/sandboxes/react/dockview/dnd-external/src/app.tsx | 2 +- .../docs/templates/dockview/dnd-external/react/src/app.tsx | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/docs/docs/core/dnd/dragAndDrop.mdx b/packages/docs/docs/core/dnd/dragAndDrop.mdx index 33293c4c2..bb68e83c2 100644 --- a/packages/docs/docs/core/dnd/dragAndDrop.mdx +++ b/packages/docs/docs/core/dnd/dragAndDrop.mdx @@ -24,12 +24,12 @@ The dock makes heavy use of drag and drop functionalities. # Drag And Drop -You can override the conditions of the far edge overlays through the `rootOverlayModel` prop. +You can override the conditions of the far edge overlays through the `dndEdges` prop. ```tsx { onReady={onReady} className={`${props.theme || 'dockview-theme-abyss'}`} onDidDrop={onDidDrop} - rootOverlayModel={{ + dndEdges={{ size: { value: 100, type: 'pixels' }, activationSize: { value: 5, type: 'percentage' }, }} diff --git a/packages/docs/templates/dockview/dnd-external/react/src/app.tsx b/packages/docs/templates/dockview/dnd-external/react/src/app.tsx index 19473533e..5aba3fcbc 100644 --- a/packages/docs/templates/dockview/dnd-external/react/src/app.tsx +++ b/packages/docs/templates/dockview/dnd-external/react/src/app.tsx @@ -179,7 +179,7 @@ const DndDockview = (props: { renderVisibleOnly: boolean; theme?: string }) => { onReady={onReady} className={`${props.theme || 'dockview-theme-abyss'}`} onDidDrop={onDidDrop} - rootOverlayModel={{ + dndEdges={{ size: { value: 100, type: 'pixels' }, activationSize: { value: 5, type: 'percentage' }, }} From 4eff83e9a043b5572d1f88a80c4a5c711c2fe1b8 Mon Sep 17 00:00:00 2001 From: mathuo <6710312+mathuo@users.noreply.github.com> Date: Mon, 10 Mar 2025 21:35:17 +0000 Subject: [PATCH 12/40] bug: remove element after dispose --- .../gridview/gridviewComponent.spec.ts | 60 +++++++++---------- .../paneview/paneviewComponent.spec.ts | 1 + .../splitview/splitviewComponent.spec.ts | 1 + .../src/paneview/paneviewComponent.ts | 2 + .../src/splitview/splitviewComponent.ts | 2 + 5 files changed, 36 insertions(+), 30 deletions(-) diff --git a/packages/dockview-core/src/__tests__/gridview/gridviewComponent.spec.ts b/packages/dockview-core/src/__tests__/gridview/gridviewComponent.spec.ts index 5f9febc5c..3063c692d 100644 --- a/packages/dockview-core/src/__tests__/gridview/gridviewComponent.spec.ts +++ b/packages/dockview-core/src/__tests__/gridview/gridviewComponent.spec.ts @@ -58,7 +58,7 @@ describe('gridview', () => { const gridview = new GridviewComponent(container, { proportionalLayout: false, orientation: Orientation.VERTICAL, - createComponent: (options) => { + createComponent: (options) => { switch (options.name) { case 'default': return new TestGridview(options.id, options.name); @@ -84,7 +84,7 @@ describe('gridview', () => { const gridview = new GridviewComponent(container, { proportionalLayout: false, orientation: Orientation.VERTICAL, - createComponent: (options) => { + createComponent: (options) => { switch (options.name) { case 'default': return new TestGridview(options.id, options.name); @@ -121,7 +121,7 @@ describe('gridview', () => { const gridview = new GridviewComponent(container, { proportionalLayout: false, orientation: Orientation.VERTICAL, - createComponent: (options) => { + createComponent: (options) => { switch (options.name) { case 'default': return new TestGridview(options.id, options.name); @@ -188,7 +188,7 @@ describe('gridview', () => { const gridview = new GridviewComponent(container, { proportionalLayout: false, orientation: Orientation.VERTICAL, - createComponent: (options) => { + createComponent: (options) => { switch (options.name) { case 'default': return new TestGridview(options.id, options.name); @@ -322,7 +322,7 @@ describe('gridview', () => { const gridview = new GridviewComponent(container, { proportionalLayout: false, orientation: Orientation.VERTICAL, - createComponent: (options) => { + createComponent: (options) => { switch (options.name) { case 'default': return new TestGridview(options.id, options.name); @@ -365,7 +365,7 @@ describe('gridview', () => { const gridview = new GridviewComponent(container, { proportionalLayout: false, orientation: Orientation.VERTICAL, - createComponent: (options) => { + createComponent: (options) => { switch (options.name) { case 'default': return new TestGridview(options.id, options.name); @@ -495,7 +495,7 @@ describe('gridview', () => { const gridview = new GridviewComponent(container, { proportionalLayout: false, orientation: Orientation.VERTICAL, - createComponent: (options) => { + createComponent: (options) => { switch (options.name) { case 'default': return new TestGridview(options.id, options.name); @@ -524,14 +524,14 @@ describe('gridview', () => { gridview.dispose(); - expect(container.childNodes.length).toBe(0); + expect(container.children.length).toBe(0); }); test('#1/VERTICAL', () => { const gridview = new GridviewComponent(container, { proportionalLayout: true, orientation: Orientation.VERTICAL, - createComponent: (options) => { + createComponent: (options) => { switch (options.name) { case 'default': return new TestGridview(options.id, options.name); @@ -596,7 +596,7 @@ describe('gridview', () => { const gridview = new GridviewComponent(container, { proportionalLayout: true, orientation: Orientation.HORIZONTAL, - createComponent: (options) => { + createComponent: (options) => { switch (options.name) { case 'default': return new TestGridview(options.id, options.name); @@ -661,7 +661,7 @@ describe('gridview', () => { const gridview = new GridviewComponent(container, { proportionalLayout: true, orientation: Orientation.HORIZONTAL, - createComponent: (options) => { + createComponent: (options) => { switch (options.name) { case 'default': return new TestGridview(options.id, options.name); @@ -744,7 +744,7 @@ describe('gridview', () => { const gridview = new GridviewComponent(container, { proportionalLayout: true, orientation: Orientation.HORIZONTAL, - createComponent: (options) => { + createComponent: (options) => { switch (options.name) { case 'default': return new TestGridview(options.id, options.name); @@ -845,7 +845,7 @@ describe('gridview', () => { const gridview = new GridviewComponent(container, { proportionalLayout: true, orientation: Orientation.VERTICAL, - createComponent: (options) => { + createComponent: (options) => { switch (options.name) { case 'default': return new TestGridview(options.id, options.name); @@ -946,7 +946,7 @@ describe('gridview', () => { const gridview = new GridviewComponent(container, { proportionalLayout: false, orientation: Orientation.VERTICAL, - createComponent: (options) => { + createComponent: (options) => { switch (options.name) { case 'default': return new TestGridview(options.id, options.name); @@ -1047,7 +1047,7 @@ describe('gridview', () => { const gridview = new GridviewComponent(container, { proportionalLayout: true, orientation: Orientation.VERTICAL, - createComponent: (options) => { + createComponent: (options) => { switch (options.name) { case 'default': return new TestGridview(options.id, options.name); @@ -1178,7 +1178,7 @@ describe('gridview', () => { const gridview = new GridviewComponent(container, { proportionalLayout: true, orientation: Orientation.VERTICAL, - createComponent: (options) => { + createComponent: (options) => { switch (options.name) { case 'default': return new TestGridview(options.id, options.name); @@ -1309,7 +1309,7 @@ describe('gridview', () => { const gridview = new GridviewComponent(container, { proportionalLayout: true, orientation: Orientation.VERTICAL, - createComponent: (options) => { + createComponent: (options) => { switch (options.name) { case 'default': return new TestGridview(options.id, options.name); @@ -1442,7 +1442,7 @@ describe('gridview', () => { const gridview = new GridviewComponent(container, { proportionalLayout: true, orientation: Orientation.HORIZONTAL, - createComponent: (options) => { + createComponent: (options) => { switch (options.name) { case 'default': return new TestGridview(options.id, options.name); @@ -1573,7 +1573,7 @@ describe('gridview', () => { const gridview = new GridviewComponent(container, { proportionalLayout: false, orientation: Orientation.HORIZONTAL, - createComponent: (options) => { + createComponent: (options) => { switch (options.name) { case 'default': return new TestGridview(options.id, options.name); @@ -1704,7 +1704,7 @@ describe('gridview', () => { const gridview = new GridviewComponent(container, { proportionalLayout: true, orientation: Orientation.HORIZONTAL, - createComponent: (options) => { + createComponent: (options) => { switch (options.name) { case 'default': return new TestGridview(options.id, options.name); @@ -1838,7 +1838,7 @@ describe('gridview', () => { const gridview = new GridviewComponent(container, { proportionalLayout: false, orientation: Orientation.VERTICAL, - createComponent: (options) => { + createComponent: (options) => { switch (options.name) { case 'default': return new TestGridview(options.id, options.name); @@ -1875,7 +1875,7 @@ describe('gridview', () => { const gridview = new GridviewComponent(container, { proportionalLayout: false, orientation: Orientation.VERTICAL, - createComponent: (options) => { + createComponent: (options) => { switch (options.name) { case 'default': return new TestGridview(options.id, options.name); @@ -1911,7 +1911,7 @@ describe('gridview', () => { const gridview = new GridviewComponent(container, { proportionalLayout: false, orientation: Orientation.VERTICAL, - createComponent: (options) => { + createComponent: (options) => { switch (options.name) { case 'default': return new TestGridview(options.id, options.name); @@ -1956,7 +1956,7 @@ describe('gridview', () => { const gridview = new GridviewComponent(container, { proportionalLayout: true, orientation: Orientation.HORIZONTAL, - createComponent: (options) => { + createComponent: (options) => { switch (options.name) { case 'default': return new TestGridview(options.id, options.name); @@ -2085,7 +2085,7 @@ describe('gridview', () => { const gridview = new GridviewComponent(container, { proportionalLayout: true, orientation: Orientation.VERTICAL, - createComponent: (options) => { + createComponent: (options) => { switch (options.name) { case 'default': return new TestGridview(options.id, options.name); @@ -2218,7 +2218,7 @@ describe('gridview', () => { const gridview = new GridviewComponent(container, { proportionalLayout: true, orientation: Orientation.VERTICAL, - createComponent: (options) => { + createComponent: (options) => { switch (options.name) { case 'default': return new TestGridview(options.id, options.name); @@ -2500,7 +2500,7 @@ describe('gridview', () => { const gridview = new GridviewComponent(container, { proportionalLayout: true, orientation: Orientation.VERTICAL, - createComponent: (options) => { + createComponent: (options) => { switch (options.name) { case 'default': return new TestGridview(options.id, options.name); @@ -2864,7 +2864,7 @@ describe('gridview', () => { const gridview = new GridviewComponent(container, { proportionalLayout: true, orientation: Orientation.HORIZONTAL, - createComponent: (options) => { + createComponent: (options) => { switch (options.name) { case 'default': return new TestGridview(options.id, options.name); @@ -2881,7 +2881,7 @@ describe('gridview', () => { const gridview = new GridviewComponent(container, { proportionalLayout: true, orientation: Orientation.HORIZONTAL, - createComponent: (options) => { + createComponent: (options) => { switch (options.name) { case 'default': return new TestGridview(options.id, options.name); @@ -2899,7 +2899,7 @@ describe('gridview', () => { const gridview = new GridviewComponent(container, { proportionalLayout: true, orientation: Orientation.HORIZONTAL, - createComponent: (options) => { + createComponent: (options) => { switch (options.name) { case 'default': return new TestGridview(options.id, options.name); diff --git a/packages/dockview-core/src/__tests__/paneview/paneviewComponent.spec.ts b/packages/dockview-core/src/__tests__/paneview/paneviewComponent.spec.ts index 8cfc7fc2e..a30f73fd5 100644 --- a/packages/dockview-core/src/__tests__/paneview/paneviewComponent.spec.ts +++ b/packages/dockview-core/src/__tests__/paneview/paneviewComponent.spec.ts @@ -86,6 +86,7 @@ describe('componentPaneview', () => { paneview.dispose(); expect(container.parentElement).toBe(root); + expect(container.children.length).toBe(0); }); test('vertical panels', () => { diff --git a/packages/dockview-core/src/__tests__/splitview/splitviewComponent.spec.ts b/packages/dockview-core/src/__tests__/splitview/splitviewComponent.spec.ts index 90d0b3986..6c034091c 100644 --- a/packages/dockview-core/src/__tests__/splitview/splitviewComponent.spec.ts +++ b/packages/dockview-core/src/__tests__/splitview/splitviewComponent.spec.ts @@ -46,6 +46,7 @@ describe('componentSplitview', () => { splitview.dispose(); expect(container.parentElement).toBe(root); + expect(container.children.length).toBe(0); }); test('event leakage', () => { diff --git a/packages/dockview-core/src/paneview/paneviewComponent.ts b/packages/dockview-core/src/paneview/paneviewComponent.ts index b47ca0181..4856fb1ad 100644 --- a/packages/dockview-core/src/paneview/paneviewComponent.ts +++ b/packages/dockview-core/src/paneview/paneviewComponent.ts @@ -476,6 +476,8 @@ export class PaneviewComponent extends Resizable implements IPaneviewComponent { } this._viewDisposables.clear(); + this.element.remove(); + this.paneview.dispose(); } } diff --git a/packages/dockview-core/src/splitview/splitviewComponent.ts b/packages/dockview-core/src/splitview/splitviewComponent.ts index a165ba4b3..b97855f58 100644 --- a/packages/dockview-core/src/splitview/splitviewComponent.ts +++ b/packages/dockview-core/src/splitview/splitviewComponent.ts @@ -425,6 +425,8 @@ export class SplitviewComponent view.dispose(); } + this.element.remove(); + super.dispose(); } } From 1030bdb7787ac320cd23615cea793a6753b518b3 Mon Sep 17 00:00:00 2001 From: mathuo <6710312+mathuo@users.noreply.github.com> Date: Wed, 12 Mar 2025 20:16:57 +0000 Subject: [PATCH 13/40] feat: rename theme property --- .../src/dockview/components/panel/content.ts | 2 +- packages/dockview-core/src/dockview/theme.ts | 22 ++++++++++++++++--- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/packages/dockview-core/src/dockview/components/panel/content.ts b/packages/dockview-core/src/dockview/components/panel/content.ts index 08703179d..6c37035b0 100644 --- a/packages/dockview-core/src/dockview/components/panel/content.ts +++ b/packages/dockview-core/src/dockview/components/panel/content.ts @@ -59,7 +59,7 @@ export class ContentContainer this.dropTarget = new Droptarget(this.element, { getOverlayOutline: () => { - return accessor.options.theme?.includeHeaderWhenHoverOverContent + return accessor.options.theme?.dndPanelOverlay === 'group' ? this.element.parentElement : null; }, diff --git a/packages/dockview-core/src/dockview/theme.ts b/packages/dockview-core/src/dockview/theme.ts index 4a921e2db..c010b84d0 100644 --- a/packages/dockview-core/src/dockview/theme.ts +++ b/packages/dockview-core/src/dockview/theme.ts @@ -1,9 +1,25 @@ export interface DockviewTheme { + /** + * The name of the theme + */ name: string; + /** + * The class name to apply to the theme containing the CSS variables settings. + */ className: string; + /** + * The gap between the groups + */ gap?: number; + /** + * The mouting position of the overlay shown when dragging a panel. `absolute` + * will mount the overlay to root of the dockview component whereas `relative` will mount the overlay to the group container. + */ dndOverlayMounting?: 'absolute' | 'relative'; - includeHeaderWhenHoverOverContent?: boolean; + /** + * When dragging a panel, the overlay can either encompass the panel contents or the entire group including the tab header space. + */ + dndPanelOverlay?: 'content' | 'group'; } export const themeDark: DockviewTheme = { @@ -42,7 +58,7 @@ export const themeAbyssSpaced: DockviewTheme = { className: 'dockview-theme-abyss-spaced', gap: 10, dndOverlayMounting: 'absolute', - includeHeaderWhenHoverOverContent: true, + dndPanelOverlay: 'group', }; export const themeLightSpaced: DockviewTheme = { @@ -50,5 +66,5 @@ export const themeLightSpaced: DockviewTheme = { className: 'dockview-theme-light-spaced', gap: 10, dndOverlayMounting: 'absolute', - includeHeaderWhenHoverOverContent: true, + dndPanelOverlay: 'group', }; From 21d94b654be702448450f9dba39d4faa6683aa0e Mon Sep 17 00:00:00 2001 From: mathuo <6710312+mathuo@users.noreply.github.com> Date: Wed, 12 Mar 2025 20:18:27 +0000 Subject: [PATCH 14/40] chore: generate docs --- packages/docs/src/generated/api.output.json | 91 ++++++++++++++++++--- 1 file changed, 78 insertions(+), 13 deletions(-) diff --git a/packages/docs/src/generated/api.output.json b/packages/docs/src/generated/api.output.json index 40b17e55f..f8a8a944a 100644 --- a/packages/docs/src/generated/api.output.json +++ b/packages/docs/src/generated/api.output.json @@ -25769,7 +25769,15 @@ "type": "intrinsic", "value": "string" }, - "flags": {} + "flags": {}, + "comment": { + "summary": [ + { + "kind": "text", + "text": "The class name to apply to the theme containing the CSS variables settings." + } + ] + } }, { "name": "dndOverlayMounting", @@ -25790,6 +25798,59 @@ }, "flags": { "isOptional": true + }, + "comment": { + "summary": [ + { + "kind": "text", + "text": "The mouting position of the overlay shown when dragging a panel. " + }, + { + "kind": "code", + "text": "`absolute`" + }, + { + "kind": "text", + "text": "\nwill mount the overlay to root of the dockview component whereas " + }, + { + "kind": "code", + "text": "`relative`" + }, + { + "kind": "text", + "text": " will mount the overlay to the group container." + } + ] + } + }, + { + "name": "dndPanelOverlay", + "code": "'content' | 'group'", + "kind": "property", + "type": { + "type": "or", + "values": [ + { + "type": "literal", + "value": "content" + }, + { + "type": "literal", + "value": "group" + } + ] + }, + "flags": { + "isOptional": true + }, + "comment": { + "summary": [ + { + "kind": "text", + "text": "When dragging a panel, the overlay can either encompass the panel contents or the entire group including the tab header space." + } + ] } }, { @@ -25802,18 +25863,14 @@ }, "flags": { "isOptional": true - } - }, - { - "name": "includeHeaderWhenHoverOverContent", - "code": "boolean", - "kind": "property", - "type": { - "type": "intrinsic", - "value": "boolean" }, - "flags": { - "isOptional": true + "comment": { + "summary": [ + { + "kind": "text", + "text": "The gap between the groups" + } + ] } }, { @@ -25824,7 +25881,15 @@ "type": "intrinsic", "value": "string" }, - "flags": {} + "flags": {}, + "comment": { + "summary": [ + { + "kind": "text", + "text": "The name of the theme" + } + ] + } } ], "extends": [] From ac92d59d0adc852ca5c3d52a7fa8986b0dae7464 Mon Sep 17 00:00:00 2001 From: mathuo <6710312+mathuo@users.noreply.github.com> Date: Wed, 12 Mar 2025 20:24:59 +0000 Subject: [PATCH 15/40] chore: v4.0.0 docs --- .../docs/blog/2025-03-12-dockview-4.0.0.md | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 packages/docs/blog/2025-03-12-dockview-4.0.0.md diff --git a/packages/docs/blog/2025-03-12-dockview-4.0.0.md b/packages/docs/blog/2025-03-12-dockview-4.0.0.md new file mode 100644 index 000000000..e7a759965 --- /dev/null +++ b/packages/docs/blog/2025-03-12-dockview-4.0.0.md @@ -0,0 +1,24 @@ +--- +slug: dockview-4.0.0-release +title: Dockview 4.0.0 +tags: [release] +--- + +# Release Notes + +Please reference docs @ [dockview.dev](https://dockview.dev). + +## 🚀 Features +- To control the theme of dockview you should no longer pass a `dv-theme-*` class, instead directly use the `theme` property. See [Themes](https://dockview.dev/demo) for more details. [#850](https://github.com/mathuo/dockview/pull/850) +- Introduces a new dnd overlay model with improved animations and customization options allowing themes to take more fine grained control over the dnd overlay styles. [#850](https://github.com/mathuo/dockview/pull/850) +- Custom scrollbar on tab headers for better UX [#822](https://github.com/mathuo/dockview/pull/822) +- When tabs are hidden within scrollbar hidden tabs can be selected from a dropdown that appears in header [#822](https://github.com/mathuo/dockview/pull/822) + +## 🛠 Miscs + +- Bug: Remove elements from DOM after disposable for Splitview and Paneview components [#870](https://github.com/mathuo/dockview/pull/870) + +## 🔥 Breaking changes + +- `setGap(gap: number | undefined): void` and `gap(): number` have been removed. The gap property is now controlled directly within the chosen Theme. See [Themes](https://dockview.dev) for more details. +- `DockviewDefaultTab` requires a `tabLocation` field, to mimic existing behaviour use `tabLocation="header"`. From bffd2dea89fa216bb077a3c333308d0fc2482c4e Mon Sep 17 00:00:00 2001 From: mathuo <6710312+mathuo@users.noreply.github.com> Date: Wed, 12 Mar 2025 20:25:13 +0000 Subject: [PATCH 16/40] chore(release): publish v4.0.0 --- lerna.json | 2 +- packages/dockview-angular/package.json | 4 ++-- packages/dockview-core/package.json | 2 +- packages/dockview-react/package.json | 4 ++-- packages/dockview-vue/package.json | 4 ++-- packages/dockview/package.json | 4 ++-- packages/docs/package.json | 4 ++-- 7 files changed, 12 insertions(+), 12 deletions(-) diff --git a/lerna.json b/lerna.json index 9833ce0ea..88b00f39a 100644 --- a/lerna.json +++ b/lerna.json @@ -2,7 +2,7 @@ "packages": [ "packages/*" ], - "version": "3.2.0", + "version": "4.0.0", "npmClient": "yarn", "command": { "publish": { diff --git a/packages/dockview-angular/package.json b/packages/dockview-angular/package.json index 078f3f2a2..63f6b6b58 100644 --- a/packages/dockview-angular/package.json +++ b/packages/dockview-angular/package.json @@ -1,6 +1,6 @@ { "name": "dockview-angular", - "version": "3.2.0", + "version": "4.0.0", "description": "Zero dependency layout manager supporting tabs, grids and splitviews", "keywords": [ "splitview", @@ -54,6 +54,6 @@ "test:cov": "cross-env ../../node_modules/.bin/jest --selectProjects dockview --coverage" }, "dependencies": { - "dockview-core": "^3.2.0" + "dockview-core": "^4.0.0" } } diff --git a/packages/dockview-core/package.json b/packages/dockview-core/package.json index 1d99a5495..8fab699a9 100644 --- a/packages/dockview-core/package.json +++ b/packages/dockview-core/package.json @@ -1,6 +1,6 @@ { "name": "dockview-core", - "version": "3.2.0", + "version": "4.0.0", "description": "Zero dependency layout manager supporting tabs, grids and splitviews", "keywords": [ "splitview", diff --git a/packages/dockview-react/package.json b/packages/dockview-react/package.json index ce38e89b7..6d5fa2b75 100644 --- a/packages/dockview-react/package.json +++ b/packages/dockview-react/package.json @@ -1,6 +1,6 @@ { "name": "dockview-react", - "version": "3.2.0", + "version": "4.0.0", "description": "Zero dependency layout manager supporting tabs, grids and splitviews", "keywords": [ "splitview", @@ -54,6 +54,6 @@ "test:cov": "cross-env ../../node_modules/.bin/jest --selectProjects dockview-react --coverage" }, "dependencies": { - "dockview": "^3.2.0" + "dockview": "^4.0.0" } } diff --git a/packages/dockview-vue/package.json b/packages/dockview-vue/package.json index 0eca82496..dfa4dedaf 100644 --- a/packages/dockview-vue/package.json +++ b/packages/dockview-vue/package.json @@ -1,6 +1,6 @@ { "name": "dockview-vue", - "version": "3.2.0", + "version": "4.0.0", "description": "Zero dependency layout manager supporting tabs, grids and splitviews", "keywords": [ "splitview", @@ -52,7 +52,7 @@ "test:cov": "cross-env ../../node_modules/.bin/jest --selectProjects dockview-vue --coverage" }, "dependencies": { - "dockview-core": "^3.2.0" + "dockview-core": "^4.0.0" }, "peerDependencies": { "vue": "^3.4.0" diff --git a/packages/dockview/package.json b/packages/dockview/package.json index 312cd1a43..51b0dd5b9 100644 --- a/packages/dockview/package.json +++ b/packages/dockview/package.json @@ -1,6 +1,6 @@ { "name": "dockview", - "version": "3.2.0", + "version": "4.0.0", "description": "Zero dependency layout manager supporting tabs, grids and splitviews", "keywords": [ "splitview", @@ -54,7 +54,7 @@ "test:cov": "cross-env ../../node_modules/.bin/jest --selectProjects dockview --coverage" }, "dependencies": { - "dockview-core": "^3.2.0" + "dockview-core": "^4.0.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" diff --git a/packages/docs/package.json b/packages/docs/package.json index e2ec4b807..2a0cef3d2 100644 --- a/packages/docs/package.json +++ b/packages/docs/package.json @@ -1,6 +1,6 @@ { "name": "dockview-docs", - "version": "3.2.0", + "version": "4.0.0", "private": true, "scripts": { "build": "npm run build-templates && docusaurus build", @@ -38,7 +38,7 @@ "ag-grid-react": "^31.0.2", "axios": "^1.6.3", "clsx": "^2.1.0", - "dockview": "^3.2.0", + "dockview": "^4.0.0", "prism-react-renderer": "^2.3.1", "react-dnd": "^16.0.1", "react-laag": "^2.0.5", From a306742508f2ecde8df0fb1fe6dfc74b067db3de Mon Sep 17 00:00:00 2001 From: mathuo <6710312+mathuo@users.noreply.github.com> Date: Wed, 12 Mar 2025 20:55:50 +0000 Subject: [PATCH 17/40] feat: custom paneview header height --- .../src/__tests__/paneview/paneview.spec.ts | 76 +++++++++++-------- .../paneview/paneviewComponent.spec.ts | 30 +++++--- .../src/paneview/draggablePaneviewPanel.ts | 39 +++++++--- .../src/paneview/paneviewComponent.ts | 37 ++++++--- .../src/paneview/paneviewPanel.ts | 49 +++++++----- 5 files changed, 151 insertions(+), 80 deletions(-) diff --git a/packages/dockview-core/src/__tests__/paneview/paneview.spec.ts b/packages/dockview-core/src/__tests__/paneview/paneview.spec.ts index 5bc8720d8..8818ee131 100644 --- a/packages/dockview-core/src/__tests__/paneview/paneview.spec.ts +++ b/packages/dockview-core/src/__tests__/paneview/paneview.spec.ts @@ -56,22 +56,28 @@ describe('paneview', () => { paneview.onDidRemoveView((view) => removed.push(view)) ); - const view1 = new TestPanel( - 'id', - 'component', - 'headerComponent', - Orientation.VERTICAL, - true, - true - ); - const view2 = new TestPanel( - 'id2', - 'component', - 'headerComponent', - Orientation.VERTICAL, - true, - true - ); + const view1 = new TestPanel({ + id: 'id', + component: 'component', + headerComponent: 'headerComponent', + orientation: Orientation.VERTICAL, + isExpanded: true, + isHeaderVisible: true, + headerSize: 22, + minimumBodySize: 0, + maximumBodySize: Number.MAX_SAFE_INTEGER, + }); + const view2 = new TestPanel({ + id: 'id2', + component: 'component', + headerComponent: 'headerComponent', + orientation: Orientation.VERTICAL, + isExpanded: true, + isHeaderVisible: true, + headerSize: 22, + minimumBodySize: 0, + maximumBodySize: Number.MAX_SAFE_INTEGER, + }); expect(added.length).toBe(0); expect(removed.length).toBe(0); @@ -106,22 +112,28 @@ describe('paneview', () => { orientation: Orientation.HORIZONTAL, }); - const view1 = new TestPanel( - 'id', - 'component', - 'headerComponent', - Orientation.VERTICAL, - true, - true - ); - const view2 = new TestPanel( - 'id2', - 'component', - 'headerComponent', - Orientation.VERTICAL, - true, - true - ); + const view1 = new TestPanel({ + id: 'id', + component: 'component', + headerComponent: 'headerComponent', + orientation: Orientation.VERTICAL, + isExpanded: true, + isHeaderVisible: true, + headerSize: 22, + minimumBodySize: 0, + maximumBodySize: Number.MAX_SAFE_INTEGER, + }); + const view2 = new TestPanel({ + id: 'id2', + component: 'component', + headerComponent: 'headerComponent', + orientation: Orientation.VERTICAL, + isExpanded: true, + isHeaderVisible: true, + headerSize: 22, + minimumBodySize: 0, + maximumBodySize: Number.MAX_SAFE_INTEGER, + }); paneview.addPane(view1); paneview.addPane(view2); diff --git a/packages/dockview-core/src/__tests__/paneview/paneviewComponent.spec.ts b/packages/dockview-core/src/__tests__/paneview/paneviewComponent.spec.ts index a30f73fd5..1a9fcb445 100644 --- a/packages/dockview-core/src/__tests__/paneview/paneviewComponent.spec.ts +++ b/packages/dockview-core/src/__tests__/paneview/paneviewComponent.spec.ts @@ -11,7 +11,17 @@ import { Orientation } from '../../splitview/splitview'; class TestPanel extends PaneviewPanel { constructor(id: string, component: string) { - super(id, component, 'header', Orientation.VERTICAL, false, true); + super({ + id, + component, + headerComponent: 'header', + orientation: Orientation.VERTICAL, + isExpanded: false, + isHeaderVisible: true, + headerSize: 22, + minimumBodySize: 0, + maximumBodySize: Number.MAX_SAFE_INTEGER, + }); } getHeaderComponent() { @@ -59,7 +69,7 @@ class TestPanel extends PaneviewPanel { } } -describe('componentPaneview', () => { +describe('paneviewComponent', () => { let container: HTMLElement; beforeEach(() => { @@ -255,7 +265,7 @@ describe('componentPaneview', () => { title: 'Panel 1', }, expanded: true, - minimumSize: 100, + headerSize: 22, }, { size: 22, @@ -265,7 +275,7 @@ describe('componentPaneview', () => { title: 'Panel 2', }, expanded: false, - minimumSize: 100, + headerSize: 22, }, { size: 22, @@ -275,7 +285,7 @@ describe('componentPaneview', () => { title: 'Panel 3', }, expanded: false, - minimumSize: 100, + headerSize: 22, }, ], }); @@ -450,6 +460,7 @@ describe('componentPaneview', () => { component: 'default', title: 'Panel 1', }, + minimumSize: 100, expanded: true, }, { @@ -486,26 +497,27 @@ describe('componentPaneview', () => { }, expanded: true, minimumSize: 100, + headerSize: 22, }, { - size: 122, + size: 22, data: { id: 'panel2', component: 'default', title: 'Panel 2', }, expanded: true, - minimumSize: 100, + headerSize: 22, }, { - size: 356, + size: 456, data: { id: 'panel3', component: 'default', title: 'Panel 3', }, expanded: true, - minimumSize: 100, + headerSize: 22, }, ], }); diff --git a/packages/dockview-core/src/paneview/draggablePaneviewPanel.ts b/packages/dockview-core/src/paneview/draggablePaneviewPanel.ts index 7227ea46a..6d2546dbd 100644 --- a/packages/dockview-core/src/paneview/draggablePaneviewPanel.ts +++ b/packages/dockview-core/src/paneview/draggablePaneviewPanel.ts @@ -38,20 +38,37 @@ export abstract class DraggablePaneviewPanel extends PaneviewPanel { readonly onUnhandledDragOverEvent: Event = this._onUnhandledDragOverEvent.event; - constructor( - private readonly accessor: IPaneviewComponent, - id: string, - component: string, - headerComponent: string | undefined, - orientation: Orientation, - isExpanded: boolean, - disableDnd: boolean - ) { - super(id, component, headerComponent, orientation, isExpanded, true); + readonly accessor: IPaneviewComponent; + + constructor(options: { + accessor: IPaneviewComponent; + id: string; + component: string; + headerComponent: string | undefined; + orientation: Orientation; + isExpanded: boolean; + disableDnd: boolean; + headerSize: number; + minimumBodySize: number; + maximumBodySize: number; + }) { + super({ + id: options.id, + component: options.component, + headerComponent: options.headerComponent, + orientation: options.orientation, + isExpanded: options.isExpanded, + isHeaderVisible: true, + headerSize: options.headerSize, + minimumBodySize: options.minimumBodySize, + maximumBodySize: options.maximumBodySize, + }); + + this.accessor = options.accessor; this.addDisposables(this._onDidDrop, this._onUnhandledDragOverEvent); - if (!disableDnd) { + if (!options.disableDnd) { this.initDragFeatures(); } } diff --git a/packages/dockview-core/src/paneview/paneviewComponent.ts b/packages/dockview-core/src/paneview/paneviewComponent.ts index 4856fb1ad..0239a096b 100644 --- a/packages/dockview-core/src/paneview/paneviewComponent.ts +++ b/packages/dockview-core/src/paneview/paneviewComponent.ts @@ -21,11 +21,16 @@ import { Classnames } from '../dom'; const nextLayoutId = sequentialNumberGenerator(); +const HEADER_SIZE = 22; +const MINIMUM_BODY_SIZE = 0; +const MAXIMUM_BODY_SIZE = Number.MAX_SAFE_INTEGER; + export interface SerializedPaneviewPanel { snap?: boolean; priority?: LayoutPriority; minimumSize?: number; maximumSize?: number; + headerSize?: number; data: { id: string; component: string; @@ -54,17 +59,23 @@ export class PaneFramework extends DraggablePaneviewPanel { isExpanded: boolean; disableDnd: boolean; accessor: IPaneviewComponent; + headerSize: number; + minimumBodySize: number; + maximumBodySize: number; } ) { - super( - options.accessor, - options.id, - options.component, - options.headerComponent, - options.orientation, - options.isExpanded, - options.disableDnd - ); + super({ + accessor: options.accessor, + id: options.id, + component: options.component, + headerComponent: options.headerComponent, + orientation: options.orientation, + isExpanded: options.isExpanded, + disableDnd: options.disableDnd, + headerSize: options.headerSize, + minimumBodySize: options.minimumBodySize, + maximumBodySize: options.maximumBodySize, + }); } getBodyComponent() { @@ -83,6 +94,7 @@ export interface AddPaneviewComponentOptions { params?: T; minimumBodySize?: number; maximumBodySize?: number; + headerSize?: number; isExpanded?: boolean; title: string; index?: number; @@ -277,6 +289,9 @@ export class PaneviewComponent extends Resizable implements IPaneviewComponent { isExpanded: !!options.isExpanded, disableDnd: !!this.options.disableDnd, accessor: this, + headerSize: options.headerSize ?? HEADER_SIZE, + minimumBodySize: MINIMUM_BODY_SIZE, + maximumBodySize: MAXIMUM_BODY_SIZE, }); this.doAddPanel(view); @@ -344,6 +359,7 @@ export class PaneviewComponent extends Resizable implements IPaneviewComponent { data: view.toJSON(), minimumSize: minimum(view.minimumBodySize), maximumSize: maximum(view.maximumBodySize), + headerSize: view.headerSize, expanded: view.isExpanded(), }; }); @@ -403,6 +419,9 @@ export class PaneviewComponent extends Resizable implements IPaneviewComponent { isExpanded: !!view.expanded, disableDnd: !!this.options.disableDnd, accessor: this, + headerSize: view.headerSize ?? HEADER_SIZE, + minimumBodySize: view.minimumSize ?? MINIMUM_BODY_SIZE, + maximumBodySize: view.maximumSize ?? MAXIMUM_BODY_SIZE, }); this.doAddPanel(panel); diff --git a/packages/dockview-core/src/paneview/paneviewPanel.ts b/packages/dockview-core/src/paneview/paneviewPanel.ts index 02710da90..88e724a91 100644 --- a/packages/dockview-core/src/paneview/paneviewPanel.ts +++ b/packages/dockview-core/src/paneview/paneviewPanel.ts @@ -71,22 +71,23 @@ export abstract class PaneviewPanel readonly onDidChange: Event<{ size?: number; orthogonalSize?: number }> = this._onDidChange.event; - private readonly headerSize = 22; private _orthogonalSize = 0; private _size = 0; - private _minimumBodySize = 100; - private _maximumBodySize: number = Number.POSITIVE_INFINITY; + private _minimumBodySize: number; + private _maximumBodySize: number; private _isExpanded = false; protected header?: HTMLElement; protected body?: HTMLElement; private bodyPart?: IPanePart; private headerPart?: IPanePart; - private expandedSize = 0; private animationTimer: any; private _orientation: Orientation; private _headerVisible: boolean; + readonly headerSize: number; + readonly headerComponent: string | undefined; + set orientation(value: Orientation) { this._orientation = value; } @@ -149,24 +150,37 @@ export abstract class PaneviewPanel this.header!.style.display = value ? '' : 'none'; } - constructor( - id: string, - component: string, - private readonly headerComponent: string | undefined, - orientation: Orientation, - isExpanded: boolean, - isHeaderVisible: boolean - ) { - super(id, component, new PaneviewPanelApiImpl(id, component)); + constructor(options: { + id: string; + component: string; + headerComponent: string | undefined; + orientation: Orientation; + isExpanded: boolean; + isHeaderVisible: boolean; + headerSize: number; + minimumBodySize: number; + maximumBodySize: number; + }) { + super( + options.id, + options.component, + new PaneviewPanelApiImpl(options.id, options.component) + ); this.api.pane = this; // TODO cannot use 'this' before 'super' this.api.initialize(this); - this._isExpanded = isExpanded; - this._headerVisible = isHeaderVisible; + this.headerSize = options.headerSize; + this.headerComponent = options.headerComponent; + + this._minimumBodySize = options.minimumBodySize; + this._maximumBodySize = options.maximumBodySize; + + this._isExpanded = options.isExpanded; + this._headerVisible = options.isHeaderVisible; this._onDidChangeExpansionState.fire(this.isExpanded()); // initialize value - this._orientation = orientation; + this._orientation = options.orientation; this.element.classList.add('dv-pane'); @@ -260,9 +274,6 @@ export abstract class PaneviewPanel this.orientation === Orientation.HORIZONTAL ? [size, orthogonalSize] : [orthogonalSize, size]; - if (this.isExpanded()) { - this.expandedSize = width; - } super.layout(width, height); } From 64e11a880cdf59fe7c045a13ddbf1f6996c4662c Mon Sep 17 00:00:00 2001 From: mathuo <6710312+mathuo@users.noreply.github.com> Date: Tue, 10 Dec 2024 20:57:53 +0000 Subject: [PATCH 18/40] bug: disable iframes within shadowdom during dnd --- packages/dockview-core/src/dom.ts | 32 ++++++++++--- .../react/dockview/demo-dockview/src/app.tsx | 45 ++++++++++++++++++- .../demo-dockview/src/gridActions.tsx | 23 +++++++++- .../demo-dockview/src/panelActions.tsx | 4 +- 4 files changed, 94 insertions(+), 10 deletions(-) diff --git a/packages/dockview-core/src/dom.ts b/packages/dockview-core/src/dom.ts index d61b1cacb..99e7b2bee 100644 --- a/packages/dockview-core/src/dom.ts +++ b/packages/dockview-core/src/dom.ts @@ -112,8 +112,11 @@ export function isAncestor( return false; } -export function getElementsByTagName(tag: string): HTMLElement[] { - return Array.prototype.slice.call(document.getElementsByTagName(tag), 0); +export function getElementsByTagName( + tag: string, + document: ParentNode +): HTMLElement[] { + return Array.prototype.slice.call(document.querySelectorAll(tag), 0); } export interface IFocusTracker extends IDisposable { @@ -288,10 +291,29 @@ export function addTestId(element: HTMLElement, id: string): void { element.setAttribute('data-testid', id); } -export function disableIframePointEvents() { +export function disableIframePointEvents(rootNode: ParentNode = document) { + const includeShadowDom = true; + + const shadowRoots: ShadowRoot[] = []; + + if (includeShadowDom) { + const items = rootNode.querySelectorAll('*'); + + for (let i = 0; i < items.length; i++) { + const item = items[i]; + if (item.shadowRoot) { + shadowRoots.push(item.shadowRoot); + } + } + } + const iframes: HTMLElement[] = [ - ...getElementsByTagName('iframe'), - ...getElementsByTagName('webview'), + ...getElementsByTagName('iframe', rootNode), + ...getElementsByTagName('webview', rootNode), + ...shadowRoots.flatMap((root) => [ + ...getElementsByTagName('iframe', root), + ...getElementsByTagName('webview', root), + ]), ]; const original = new WeakMap(); // don't hold onto HTMLElement references longer than required diff --git a/packages/docs/sandboxes/react/dockview/demo-dockview/src/app.tsx b/packages/docs/sandboxes/react/dockview/demo-dockview/src/app.tsx index 6595a3fce..7f1d8d8b4 100644 --- a/packages/docs/sandboxes/react/dockview/demo-dockview/src/app.tsx +++ b/packages/docs/sandboxes/react/dockview/demo-dockview/src/app.tsx @@ -7,6 +7,7 @@ import { DockviewApi, } from 'dockview'; import * as React from 'react'; +import * as ReactDOM from 'react-dom/client'; import './app.scss'; import { defaultConfig } from './defaultLayout'; import { GridActions } from './gridActions'; @@ -30,6 +31,20 @@ const Option = (props: { ); }; +const ShadowIframe = (props: IDockviewPanelProps) => { + return ( +