dockview-core
Version:
Zero dependency layout manager supporting tabs, groups, grids and splitviews for vanilla TypeScript
354 lines (353 loc) • 15.2 kB
JavaScript
import { CompositeDisposable, Disposable, MutableDisposable, } from '../../../lifecycle';
import { addDisposableListener, Emitter } from '../../../events';
import { VoidContainer } from './voidContainer';
import { addClasses, findRelativeZIndexParent, removeClasses, toggleClass, } from '../../../dom';
import { DockviewWillShowOverlayLocationEvent } from '../../events';
import { getPanelData } from '../../../dnd/dataTransfer';
import { Tabs } from './tabs';
import { createDropdownElementHandle, } from './tabOverflowControl';
import { applyTabGroupAccent } from '../../tabGroupAccent';
export class TabsContainer extends CompositeDisposable {
get onTabDragStart() {
return this.tabs.onTabDragStart;
}
get panels() {
return this.tabs.panels;
}
get size() {
return this.tabs.size;
}
get hidden() {
return this._hidden;
}
set hidden(value) {
this._hidden = value;
this.element.style.display = value ? 'none' : '';
}
get direction() {
return this._direction;
}
set direction(value) {
this._direction = value;
if (value === 'vertical') {
addClasses(this._element, 'dv-groupview-header-vertical');
addClasses(this.rightActionsContainer, 'dv-right-actions-container-vertical');
this.tabs.direction = value;
}
else {
removeClasses(this._element, 'dv-groupview-header-vertical');
removeClasses(this.rightActionsContainer, 'dv-right-actions-container-vertical');
this.tabs.direction = value;
}
}
get element() {
return this._element;
}
constructor(accessor, group) {
super();
this.accessor = accessor;
this.group = group;
this._hidden = false;
this._direction = 'horizontal';
this.dropdownPart = null;
this._overflowTabs = [];
this._overflowTabGroups = [];
this._dropdownDisposable = new MutableDisposable();
this._onDrop = new Emitter();
this.onDrop = this._onDrop.event;
this._onGroupDragStart = new Emitter();
this.onGroupDragStart = this._onGroupDragStart.event;
this._onWillShowOverlay = new Emitter();
this.onWillShowOverlay = this._onWillShowOverlay.event;
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, {
showTabsOverflowControl: !accessor.options.disableTabsOverflowList,
});
this.voidContainer = new VoidContainer(this.accessor, this.group);
this.tabs.voidContainer = this.voidContainer.element;
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.tabs.setExtendedDropZone(this._element);
this.addDisposables(this.tabs.onDrop((e) => this._onDrop.fire(e)), this.tabs.onWillShowOverlay((e) => this._onWillShowOverlay.fire(e)), accessor.onDidOptionsChange(() => {
this.tabs.showTabsOverflowControl =
!accessor.options.disableTabsOverflowList;
}), this.tabs.onOverflowTabsChange((event) => {
this.toggleDropdown(event);
}), this.tabs, this._onWillShowOverlay, this._onDrop, this._onGroupDragStart, this.voidContainer, this.voidContainer.onDragStart((event) => {
this._onGroupDragStart.fire({
nativeEvent: event,
group: this.group,
});
}), this.voidContainer.onDrop((event) => {
// If an active group drag is in progress, let Tabs handle it
if (this.tabs.handleVoidDrop()) {
return;
}
this._onDrop.fire({
event: event.nativeEvent,
index: this.tabs.size,
});
}), this.voidContainer.onWillShowOverlay((event) => {
this._onWillShowOverlay.fire(new DockviewWillShowOverlayLocationEvent(event, {
kind: 'header_space',
panel: this.group.activePanel,
api: this.accessor.api,
group: this.group,
getData: getPanelData,
}));
}), addDisposableListener(this.leftActionsContainer, 'dragleave', (event) => {
const related = event.relatedTarget;
if (!this.leftActionsContainer.contains(related) &&
!this._element.contains(related)) {
// Left the header entirely
this.tabs.clearExternalAnimState();
}
}), addDisposableListener(this.voidContainer.element, 'dragleave', (event) => {
const related = event.relatedTarget;
if (!this.voidContainer.element.contains(related)) {
if (this._element.contains(related)) {
// Moved to another part of the header — keep state
this.tabs.setExternalInsertionIndex(null);
}
else {
// Left the header entirely
this.tabs.clearExternalAnimState();
}
}
}), addDisposableListener(this.voidContainer.element, 'pointerdown', (event) => {
if (event.defaultPrevented) {
return;
}
const isFloatingGroupsEnabled = !this.accessor.options.disableFloatingGroups;
if (isFloatingGroupsEnabled &&
event.shiftKey &&
this.group.api.location.type !== 'floating' &&
this.group.api.location.type !== 'edge') {
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() {
if (!this.hidden) {
this.element.style.display = '';
}
}
hide() {
this._element.style.display = 'none';
}
setRightActionsElement(element) {
if (this.rightActions === element) {
return;
}
if (this.rightActions) {
this.rightActions.remove();
this.rightActions = undefined;
}
if (element) {
this.rightActionsContainer.appendChild(element);
this.rightActions = element;
}
}
setLeftActionsElement(element) {
if (this.leftActions === element) {
return;
}
if (this.leftActions) {
this.leftActions.remove();
this.leftActions = undefined;
}
if (element) {
this.leftActionsContainer.appendChild(element);
this.leftActions = element;
}
}
setPrefixActionsElement(element) {
if (this.preActions === element) {
return;
}
if (this.preActions) {
this.preActions.remove();
this.preActions = undefined;
}
if (element) {
this.preActionsContainer.appendChild(element);
this.preActions = element;
}
}
isActive(tab) {
return this.tabs.isActive(tab);
}
indexOf(id) {
return this.tabs.indexOf(id);
}
setActive(_isGroupActive) {
// noop
}
delete(id) {
this.tabs.delete(id);
this.updateClassnames();
}
setActivePanel(panel) {
this.tabs.setActivePanel(panel);
}
openPanel(panel, index = this.tabs.size) {
this.tabs.openPanel(panel, index);
this.updateClassnames();
}
closePanel(panel) {
this.delete(panel.id);
}
updateClassnames() {
toggleClass(this._element, 'dv-single-tab', this.size === 1);
}
toggleDropdown(options) {
const tabs = options.reset ? [] : options.tabs;
const tabGroups = options.reset ? [] : options.tabGroups;
this._overflowTabs = tabs;
this._overflowTabGroups = tabGroups;
const totalCount = this._overflowTabs.length;
if (totalCount > 0 && this.dropdownPart) {
this.dropdownPart.update({ tabs: totalCount });
return;
}
if (totalCount === 0) {
this._dropdownDisposable.dispose();
return;
}
const root = document.createElement('div');
root.className = 'dv-tabs-overflow-dropdown-root';
const part = createDropdownElementHandle();
part.update({ tabs: totalCount });
this.dropdownPart = part;
root.appendChild(part.element);
this.rightActionsContainer.prepend(root);
this._dropdownDisposable.value = new CompositeDisposable(Disposable.from(() => {
var _a, _b;
root.remove();
(_b = (_a = this.dropdownPart) === null || _a === void 0 ? void 0 : _a.dispose) === null || _b === void 0 ? void 0 : _b.call(_a);
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';
// Build lookup: panelId → tabGroup for overflow groups
const overflowGroupSet = new Set(this._overflowTabGroups);
const allTabGroups = this.group.model.getTabGroups();
const panelToGroup = new Map();
for (const tg of allTabGroups) {
if (overflowGroupSet.has(tg.id)) {
for (const pid of tg.panelIds) {
panelToGroup.set(pid, tg);
}
}
}
// Track which groups have already been rendered
const renderedGroups = new Set();
for (const tab of this.tabs.tabs.filter((tab) => this._overflowTabs.includes(tab.panel.id))) {
const tg = panelToGroup.get(tab.panel.id);
// If this tab belongs to an overflow group, render the
// group header before its first member tab.
if (tg && !renderedGroups.has(tg.id)) {
renderedGroups.add(tg.id);
const groupHeader = document.createElement('div');
groupHeader.className = 'dv-tabs-overflow-group-header';
const colorDot = document.createElement('span');
colorDot.className = 'dv-tabs-overflow-group-color';
applyTabGroupAccent(colorDot, tg.color, this.accessor.tabGroupColorPalette);
groupHeader.appendChild(colorDot);
const labelSpan = document.createElement('span');
labelSpan.className = 'dv-tabs-overflow-group-label';
labelSpan.textContent = tg.label || tg.id;
groupHeader.appendChild(labelSpan);
if (tg.collapsed) {
const badge = document.createElement('span');
badge.className =
'dv-tabs-overflow-group-collapsed-badge';
badge.textContent = `${tg.panelIds.length}`;
groupHeader.appendChild(badge);
}
groupHeader.addEventListener('click', () => {
this.accessor
.getPopupServiceForGroup(this.group)
.close();
if (tg.collapsed) {
tg.expand();
}
// Activate the first panel in the group
const firstPanelId = tg.panelIds[0];
if (firstPanelId) {
const panel = this.group.panels.find((p) => p.id === firstPanelId);
panel === null || panel === void 0 ? void 0 : panel.api.setActive();
}
});
el.appendChild(groupHeader);
}
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);
if (tg) {
toggleClass(wrapper, 'dv-tab--grouped', true);
}
wrapper.addEventListener('click', (event) => {
this.accessor
.getPopupServiceForGroup(this.group)
.close();
if (event.defaultPrevented) {
return;
}
if (tg === null || tg === void 0 ? void 0 : tg.collapsed) {
tg.expand();
}
tab.element.scrollIntoView();
tab.panel.api.setActive();
});
wrapper.appendChild(child);
el.appendChild(wrapper);
}
const relativeParent = findRelativeZIndexParent(root);
this.accessor
.getPopupServiceForGroup(this.group)
.openPopover(el, {
x: event.clientX,
y: event.clientY,
zIndex: (relativeParent === null || relativeParent === void 0 ? void 0 : relativeParent.style.zIndex)
? `calc(${relativeParent.style.zIndex} * 2)`
: undefined,
});
}));
}
updateDragAndDropState() {
this.tabs.updateDragAndDropState();
this.voidContainer.updateDragAndDropState();
}
updateTabGroups() {
this.tabs.updateTabGroups();
}
refreshTabGroupAccent() {
this.tabs.refreshTabGroupAccent();
}
}