mirror of
https://github.com/mathuo/dockview
synced 2025-03-09 23:42:05 +00:00
feat: scrollbars
This commit is contained in:
parent
7a6b2cb26d
commit
cfe37766a9
@ -18,7 +18,7 @@ export class DockviewPanelModelMock implements IDockviewPanelModel {
|
||||
//
|
||||
}
|
||||
|
||||
copyTabComponent(tabLocation: TabLocation): ITabRenderer {
|
||||
createTabRenderer(tabLocation: TabLocation): ITabRenderer {
|
||||
return this.tab;
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
});
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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}`;
|
||||
},
|
||||
};
|
||||
}
|
@ -1,6 +1,5 @@
|
||||
.dv-tabs-panel {
|
||||
.dv-tabs-container {
|
||||
overflow: hidden;
|
||||
|
||||
&.dv-horizontal {
|
||||
.dv-tabs-container {
|
||||
.dv-tab {
|
||||
@ -27,8 +26,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
.dv-tabs-container {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
scrollbar-width: thin; // firefox
|
||||
|
||||
@ -58,30 +57,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
|
@ -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<Tab>[] = [];
|
||||
private _tabs: IValueDisposable<Tab>[] = [];
|
||||
private selectedIndex = -1;
|
||||
|
||||
private readonly _dropdownDisposable = new MutableDisposable();
|
||||
private _showTabsOverflowControl = false;
|
||||
|
||||
private readonly _onTabDragStart = new Emitter<TabDragEvent>();
|
||||
readonly onTabDragStart: Event<TabDragEvent> = this._onTabDragStart.event;
|
||||
@ -63,42 +38,27 @@ export class Tabs extends CompositeDisposable {
|
||||
readonly onWillShowOverlay: Event<WillShowOverlayLocationEvent> =
|
||||
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 element(): HTMLElement {
|
||||
return this._element;
|
||||
get showTabsOverflowControl(): boolean {
|
||||
return this._showTabsOverflowControl;
|
||||
}
|
||||
|
||||
get panels(): string[] {
|
||||
return this.tabs.map((_) => _.value.panel.id);
|
||||
set showTabsOverflowControl(value: boolean) {
|
||||
if (this._showTabsOverflowControl == value) {
|
||||
return;
|
||||
}
|
||||
|
||||
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 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._showTabsOverflowControl = value;
|
||||
|
||||
if (value) {
|
||||
const observer = new OverflowObserver(this._tabsList);
|
||||
|
||||
this.addDisposables(
|
||||
this._dropdownDisposable,
|
||||
this._onWillShowOverlay,
|
||||
this._onDrop,
|
||||
this._onTabDragStart,
|
||||
this._observerDisposable.value = new CompositeDisposable(
|
||||
observer,
|
||||
observer.onDidChange((event) => {
|
||||
const hasOverflow = event.hasScrollX || event.hasScrollY;
|
||||
@ -106,7 +66,51 @@ export class Tabs extends CompositeDisposable {
|
||||
}),
|
||||
addDisposableListener(this._tabsList, 'scroll', () => {
|
||||
this.toggleDropdown({ reset: false });
|
||||
}),
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
get element(): HTMLElement {
|
||||
return this._element;
|
||||
}
|
||||
|
||||
get panels(): string[] {
|
||||
return this._tabs.map((_) => _.value.panel.id);
|
||||
}
|
||||
|
||||
get size(): number {
|
||||
return this._tabs.length;
|
||||
}
|
||||
|
||||
get tabs(): Tab[] {
|
||||
return this._tabs.map((_) => _.value);
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly group: DockviewGroupPanel,
|
||||
private readonly accessor: DockviewComponent,
|
||||
options: {
|
||||
showTabsOverflowControl: boolean;
|
||||
}
|
||||
) {
|
||||
super();
|
||||
|
||||
this._tabsList = document.createElement('div');
|
||||
this._tabsList.className = 'dv-tabs-container dv-horizontal';
|
||||
|
||||
this.showTabsOverflowControl = options.showTabsOverflowControl;
|
||||
|
||||
const scrollbar = new Scrollbar(this._tabsList);
|
||||
this._element = scrollbar.element;
|
||||
|
||||
this.addDisposables(
|
||||
this._onOverflowTabsChange,
|
||||
this._observerDisposable,
|
||||
scrollbar,
|
||||
this._onWillShowOverlay,
|
||||
this._onDrop,
|
||||
this._onTabDragStart,
|
||||
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<Tab>,
|
||||
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 });
|
||||
}
|
||||
}
|
||||
|
@ -25,4 +25,8 @@
|
||||
flex-grow: 1;
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.dv-right-actions-container {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
@ -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<TabDropIndexEvent>();
|
||||
readonly onDrop: Event<TabDropIndexEvent> = 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,
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
28
packages/dockview-core/src/scrollbar.scss
Normal file
28
packages/dockview-core/src/scrollbar.scss
Normal file
@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
131
packages/dockview-core/src/scrollbar.ts
Normal file
131
packages/dockview-core/src/scrollbar.ts
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -19,6 +19,7 @@ describe('defaultTab', () => {
|
||||
|
||||
render(
|
||||
<DockviewDefaultTab
|
||||
tabLocation="header"
|
||||
api={api}
|
||||
containerApi={containerApi}
|
||||
params={params}
|
||||
@ -41,6 +42,7 @@ describe('defaultTab', () => {
|
||||
|
||||
render(
|
||||
<DockviewDefaultTab
|
||||
tabLocation="header"
|
||||
api={api}
|
||||
containerApi={containerApi}
|
||||
params={params}
|
||||
@ -65,6 +67,7 @@ describe('defaultTab', () => {
|
||||
|
||||
render(
|
||||
<DockviewDefaultTab
|
||||
tabLocation="header"
|
||||
api={api}
|
||||
containerApi={containerApi}
|
||||
params={params}
|
||||
@ -97,6 +100,7 @@ describe('defaultTab', () => {
|
||||
|
||||
render(
|
||||
<DockviewDefaultTab
|
||||
tabLocation="header"
|
||||
api={api}
|
||||
containerApi={containerApi}
|
||||
params={params}
|
||||
@ -122,6 +126,7 @@ describe('defaultTab', () => {
|
||||
|
||||
render(
|
||||
<DockviewDefaultTab
|
||||
tabLocation="header"
|
||||
api={api}
|
||||
containerApi={containerApi}
|
||||
params={params}
|
||||
@ -151,6 +156,7 @@ describe('defaultTab', () => {
|
||||
|
||||
render(
|
||||
<DockviewDefaultTab
|
||||
tabLocation="header"
|
||||
api={api}
|
||||
containerApi={containerApi}
|
||||
params={params}
|
||||
@ -177,6 +183,7 @@ describe('defaultTab', () => {
|
||||
|
||||
render(
|
||||
<DockviewDefaultTab
|
||||
tabLocation="header"
|
||||
api={api}
|
||||
containerApi={containerApi}
|
||||
params={params}
|
||||
|
Loading…
Reference in New Issue
Block a user