dockview-core
Version:
Zero dependency layout manager supporting tabs, groups, grids and splitviews for vanilla TypeScript
1,101 lines • 88.8 kB
JavaScript
"use strict";
var __extends = (this && this.__extends) || (function () {
var extendStatics = function (d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
return extendStatics(d, b);
};
return function (d, b) {
if (typeof b !== "function" && b !== null)
throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
var __values = (this && this.__values) || function(o) {
var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0;
if (m) return m.call(o);
if (o && typeof o.length === "number") return {
next: function () {
if (o && i >= o.length) o = void 0;
return { value: o && o[i++], done: !o };
}
};
throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined.");
};
var __read = (this && this.__read) || function (o, n) {
var m = typeof Symbol === "function" && o[Symbol.iterator];
if (!m) return o;
var i = m.call(o), r, ar = [], e;
try {
while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);
}
catch (error) { e = { error: error }; }
finally {
try {
if (r && !r.done && (m = i["return"])) m.call(i);
}
finally { if (e) throw e.error; }
}
return ar;
};
var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
if (ar || !(i in from)) {
if (!ar) ar = Array.prototype.slice.call(from, 0, i);
ar[i] = from[i];
}
}
return to.concat(ar || Array.prototype.slice.call(from));
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Tabs = void 0;
var dataTransfer_1 = require("../../../dnd/dataTransfer");
var dom_1 = require("../../../dom");
var events_1 = require("../../../events");
var lifecycle_1 = require("../../../lifecycle");
var scrollbar_1 = require("../../../scrollbar");
var pointerDragController_1 = require("../../../dnd/pointer/pointerDragController");
var events_2 = require("../../events");
var tab_1 = require("../tab/tab");
var tabGroups_1 = require("./tabGroups");
var Tabs = /** @class */ (function (_super) {
__extends(Tabs, _super);
function Tabs(group, accessor, options) {
var _this = _super.call(this) || this;
_this.group = group;
_this.accessor = accessor;
_this._observerDisposable = new lifecycle_1.MutableDisposable();
_this._scrollbar = null;
_this._tabs = [];
_this._tabMap = new Map();
_this.selectedIndex = -1;
_this._showTabsOverflowControl = false;
_this._direction = 'horizontal';
_this._animState = null;
_this._pendingMarginCleanups = new Map();
_this._pendingCollapse = false;
_this._flipTransitionCleanup = null;
_this._voidContainer = null;
_this._voidContainerListeners = null;
_this._extendedDropZone = null;
_this._pointerInsideTabsList = false;
_this._onTabDragStart = new events_1.Emitter();
_this.onTabDragStart = _this._onTabDragStart.event;
_this._onDrop = new events_1.Emitter();
_this.onDrop = _this._onDrop.event;
_this._onWillShowOverlay = new events_1.Emitter();
_this.onWillShowOverlay = _this._onWillShowOverlay.event;
_this._onOverflowTabsChange = new events_1.Emitter();
_this.onOverflowTabsChange = _this._onOverflowTabsChange.event;
_this._tabsList = document.createElement('div');
_this._tabsList.className = 'dv-tabs-container';
_this.showTabsOverflowControl = options.showTabsOverflowControl;
if (accessor.options.scrollbars === 'native') {
_this._element = _this._tabsList;
}
else {
_this._scrollbar = new scrollbar_1.Scrollbar(_this._tabsList);
_this._scrollbar.orientation = _this.direction;
_this._element = _this._scrollbar.element;
_this.addDisposables(_this._scrollbar);
}
_this._tabGroupManager = new tabGroups_1.TabGroupManager({
group: _this.group,
accessor: _this.accessor,
tabsList: _this._tabsList,
getTabs: function () { return _this._tabs; },
getTabMap: function () { return _this._tabMap; },
getDirection: function () { return _this._direction; },
}, {
onChipContextMenu: function (tabGroup, event) {
_this.accessor.contextMenuController.showForChip(tabGroup, _this.group, event);
},
onChipDragStart: function (tabGroup, chip, event) {
_this._handleChipDragStart(tabGroup, chip, event);
},
onChipDragEnd: function () {
// HTML5 chip dragend (incl. cancels). The Html5DragSource
// owns the listener on the chip element, so this fires
// even if the chip was detached cross-group — the
// element keeps its listeners until the source is
// disposed. resetDragAnimation is a no-op after a
// successful drop (anim state already null) thanks to
// the gating inside it.
_this.resetDragAnimation();
},
onChipDrop: function (tabGroup, event) {
_this._handleChipDrop(tabGroup, event);
},
});
_this.addDisposables(_this._onOverflowTabsChange, _this._observerDisposable, _this._onWillShowOverlay, _this._onDrop, _this._onTabDragStart, {
dispose: function () {
var _a;
(_a = _this._flipTransitionCleanup) === null || _a === void 0 ? void 0 : _a.call(_this);
},
},
// Pointer-side cleanup: when any pointer drag ends, tear
// down smooth-reorder anim state the dragover bridge may
// have installed. The chip's pointer drag source handles
// its own transfer payload + iframe-shield cleanup.
pointerDragController_1.PointerDragController.getInstance().onDragEnd(function () {
_this._pointerInsideTabsList = false;
_this.resetDragAnimation();
}),
// Pointer-event mirror of the HTML5 dragover / dragleave handlers
// below. Drives smooth-reorder for `dndStrategy: 'pointer'` and
// for touch drags in `'auto'`.
pointerDragController_1.PointerDragController.getInstance().onDragMove(function (e) {
_this._handlePointerDragMove(e.clientX, e.clientY);
}), (0, events_1.addDisposableListener)(_this.element, 'pointerdown', function (event) {
if (event.defaultPrevented) {
return;
}
var isLeftClick = event.button === 0;
if (isLeftClick) {
_this.accessor.doSetGroupActive(_this.group);
}
}),
// Trackpad / wheel forwarding. The strip scrolls along its own
// axis (x for horizontal headers, y for vertical), so deltaY
// from a plain mouse wheel maps onto the strip's axis too —
// this gives the VS Code-style "scroll over tab bar to page
// through tabs" feel. We only consume the event when the strip
// is actually overflowing in the direction the user wheeled in,
// so a wheel at the edge of a non-overflowing strip still
// bubbles up and scrolls the page. `{ passive: false }` is
// required because we call preventDefault().
(0, events_1.addDisposableListener)(_this._tabsList, 'wheel', function (event) {
var isVertical = _this._direction === 'vertical';
var primary = isVertical
? event.deltaY || event.deltaX
: event.deltaX || event.deltaY;
if (primary === 0) {
return;
}
var max = isVertical
? _this._tabsList.scrollHeight -
_this._tabsList.clientHeight
: _this._tabsList.scrollWidth -
_this._tabsList.clientWidth;
if (max <= 0) {
return;
}
var current = isVertical
? _this._tabsList.scrollTop
: _this._tabsList.scrollLeft;
// At the edge in the wheel direction: let the page
// scroll instead of trapping the gesture.
if ((primary < 0 && current <= 0) ||
(primary > 0 && current >= max)) {
return;
}
event.preventDefault();
// Custom-scrollbar mode wraps the tabs list and installs
// its own wheel listener that rewrites scrollLeft from a
// deltaY-only tracker. Without stopPropagation that
// handler would clobber our deltaX-aware update.
event.stopPropagation();
if (isVertical) {
_this._tabsList.scrollTop = current + primary;
}
else {
_this._tabsList.scrollLeft = current + primary;
}
}, { passive: false }), (0, events_1.addDisposableListener)(_this._tabsList, 'dragover', function (event) {
if (_this._processDragOver(event.clientX)) {
// Allow `drop` to fire on the tabs list container.
event.preventDefault();
}
}, true), (0, events_1.addDisposableListener)(_this._tabsList, 'dragleave', function (event) {
_this._processDragLeave(event.relatedTarget);
}, true), (0, events_1.addDisposableListener)(_this._tabsList, 'dragend', function () {
_this.resetDragAnimation();
}), (0, events_1.addDisposableListener)(_this._tabsList, 'drop', function (event) {
var _a, _b, _c;
if (!_this._animState ||
_this._animState.currentInsertionIndex === null) {
return;
}
// In non-smooth mode only handle group drags here;
// individual tab drops are handled by tab Droptargets.
if (((_a = _this.accessor.options.theme) === null || _a === void 0 ? void 0 : _a.tabAnimation) !==
'smooth' &&
!_this._animState.sourceTabGroupId) {
return;
}
event.stopPropagation();
event.preventDefault();
// The capturing stopPropagation above prevents the
// individual tab's Droptarget.onDrop from firing, so
// the anchor overlay won't be cleared by that path.
// Clear it explicitly here before processing the drop.
(_c = (_b = _this.group.model.dropTargetContainer) === null || _b === void 0 ? void 0 : _b.model) === null || _c === void 0 ? void 0 : _c.clear();
var animState = _this._animState;
_this._animState = null;
_this._pendingCollapse = false;
// Handle group drag (entire group repositioned)
if (animState.sourceTabGroupId) {
_this._commitGroupMove(animState.sourceTabGroupId, animState.currentInsertionIndex);
return;
}
var insertionIndex = animState.currentInsertionIndex;
var sourceIndex = animState.sourceIndex;
var adjustedIndex = insertionIndex -
(sourceIndex !== -1 && sourceIndex < insertionIndex
? 1
: 0);
var sourceCurrentGroup = _this.group.model.getTabGroupForPanel(animState.sourceTabId);
if (adjustedIndex === sourceIndex &&
!animState.targetTabGroupId &&
!sourceCurrentGroup) {
_this._uncollapsSourceTab(animState.sourceTabId);
_this.resetTabTransforms();
return;
}
_this._uncollapsSourceTab(animState.sourceTabId);
var firstPositions = _this.snapshotTabPositions();
_this.resetTabTransforms();
_this._onDrop.fire({
event: event,
index: adjustedIndex,
targetTabGroupId: animState.targetTabGroupId,
});
_this.runFlipAnimation(firstPositions, animState.sourceTabId, animState.sourceIndex === -1, {
from: Math.min(sourceIndex, adjustedIndex),
to: Math.max(sourceIndex, adjustedIndex),
});
}, true), lifecycle_1.Disposable.from(function () {
var e_1, _a;
var _b;
(_b = _this._voidContainerListeners) === null || _b === void 0 ? void 0 : _b.dispose();
_this.resetDragAnimation();
_this._tabGroupManager.disposeAll();
try {
for (var _c = __values(_this._tabs), _d = _c.next(); !_d.done; _d = _c.next()) {
var _e = _d.value, value = _e.value, disposable = _e.disposable;
disposable.dispose();
value.dispose();
}
}
catch (e_1_1) { e_1 = { error: e_1_1 }; }
finally {
try {
if (_d && !_d.done && (_a = _c.return)) _a.call(_c);
}
finally { if (e_1) throw e_1.error; }
}
_this._tabs = [];
_this._tabMap.clear();
}));
return _this;
}
Object.defineProperty(Tabs.prototype, "showTabsOverflowControl", {
get: function () {
return this._showTabsOverflowControl;
},
set: function (value) {
var _this = this;
if (this._showTabsOverflowControl == value) {
return;
}
this._showTabsOverflowControl = value;
if (value) {
var observer = new dom_1.OverflowObserver(this._tabsList);
this._observerDisposable.value = new lifecycle_1.CompositeDisposable(observer, observer.onDidChange(function (event) {
var hasOverflow = event.hasScrollX || event.hasScrollY;
_this.toggleDropdown({ reset: !hasOverflow });
if (_this._tabGroupManager.groupUnderlines.size > 0) {
_this._tabGroupManager.positionUnderlines();
}
}), (0, events_1.addDisposableListener)(this._tabsList, 'scroll', function () {
_this.toggleDropdown({ reset: false });
if (_this._tabGroupManager.groupUnderlines.size > 0) {
_this._tabGroupManager.positionUnderlines();
}
}));
}
},
enumerable: false,
configurable: true
});
Object.defineProperty(Tabs.prototype, "element", {
get: function () {
return this._element;
},
enumerable: false,
configurable: true
});
Object.defineProperty(Tabs.prototype, "voidContainer", {
set: function (el) {
var _this = this;
var _a;
(_a = this._voidContainerListeners) === null || _a === void 0 ? void 0 : _a.dispose();
this._voidContainerListeners = null;
this._voidContainer = el;
if (el) {
this._voidContainerListeners = new lifecycle_1.CompositeDisposable((0, events_1.addDisposableListener)(el, 'dragover', function (event) {
if (_this._animState) {
event.preventDefault();
}
}), (0, events_1.addDisposableListener)(el, 'drop', function (event) {
var _a;
if (((_a = _this._animState) === null || _a === void 0 ? void 0 : _a.sourceTabGroupId) &&
_this._animState.currentInsertionIndex !== null) {
event.preventDefault();
event.stopPropagation();
_this.handleVoidDrop();
}
}));
}
},
enumerable: false,
configurable: true
});
/**
* Handle a drop that occurred on the void container (empty header
* space to the right of the tabs). Returns `true` if the drop was
* consumed by an active group drag, `false` otherwise.
*/
Tabs.prototype.handleVoidDrop = function () {
var _a, _b;
if (!((_a = this._animState) === null || _a === void 0 ? void 0 : _a.sourceTabGroupId)) {
return false;
}
var sourceTabGroupId = this._animState.sourceTabGroupId;
var insertionIndex = (_b = this._animState.currentInsertionIndex) !== null && _b !== void 0 ? _b : this._tabs.length;
this._animState = null;
this._commitGroupMove(sourceTabGroupId, insertionIndex);
return true;
};
Object.defineProperty(Tabs.prototype, "panels", {
get: function () {
return this._tabs.map(function (_) { return _.value.panel.id; });
},
enumerable: false,
configurable: true
});
Object.defineProperty(Tabs.prototype, "size", {
get: function () {
return this._tabs.length;
},
enumerable: false,
configurable: true
});
Object.defineProperty(Tabs.prototype, "tabs", {
get: function () {
return this._tabs.map(function (_) { return _.value; });
},
enumerable: false,
configurable: true
});
Object.defineProperty(Tabs.prototype, "direction", {
get: function () {
return this._direction;
},
set: function (value) {
var e_2, _a;
if (this._direction === value) {
return;
}
this._direction = value;
if (this._scrollbar) {
this._scrollbar.orientation = value;
}
(0, dom_1.removeClasses)(this._tabsList, 'dv-horizontal', 'dv-vertical');
if (value === 'vertical') {
(0, dom_1.addClasses)(this._tabsList, 'dv-tabs-container-vertical', 'dv-vertical');
}
else {
(0, dom_1.removeClasses)(this._tabsList, 'dv-tabs-container-vertical');
(0, dom_1.addClasses)(this._tabsList, 'dv-horizontal');
}
try {
for (var _b = __values(this._tabs), _c = _b.next(); !_c.done; _c = _b.next()) {
var tab = _c.value;
tab.value.setDirection(value);
}
}
catch (e_2_1) { e_2 = { error: e_2_1 }; }
finally {
try {
if (_c && !_c.done && (_a = _b.return)) _a.call(_b);
}
finally { if (e_2) throw e_2.error; }
}
this._tabGroupManager.updateDirection();
},
enumerable: false,
configurable: true
});
Tabs.prototype.indexOf = function (id) {
return this._tabs.findIndex(function (tab) { return tab.value.panel.id === id; });
};
Tabs.prototype.isActive = function (tab) {
return (this.selectedIndex > -1 &&
this._tabs[this.selectedIndex].value === tab);
};
Tabs.prototype.setActivePanel = function (panel) {
var e_3, _a;
var isVertical = this._direction === 'vertical';
var running = 0;
try {
for (var _b = __values(this._tabs), _c = _b.next(); !_c.done; _c = _b.next()) {
var tab = _c.value;
var isActivePanel = panel.id === tab.value.panel.id;
tab.value.setActive(isActivePanel);
if (isActivePanel) {
var element = tab.value.element;
var parentElement = element.parentElement;
if (isVertical) {
if (running < parentElement.scrollTop ||
running + element.clientHeight >
parentElement.scrollTop + parentElement.clientHeight) {
parentElement.scrollTop = running;
}
}
else {
if (running < parentElement.scrollLeft ||
running + element.clientWidth >
parentElement.scrollLeft + parentElement.clientWidth) {
parentElement.scrollLeft = running;
}
}
}
running += isVertical
? tab.value.element.clientHeight
: tab.value.element.clientWidth;
}
}
catch (e_3_1) { e_3 = { error: e_3_1 }; }
finally {
try {
if (_c && !_c.done && (_a = _b.return)) _a.call(_b);
}
finally { if (e_3) throw e_3.error; }
}
// Reposition underlines so the wrap-around follows the new active tab
if (this._tabGroupManager.groupUnderlines.size > 0) {
this._tabGroupManager.positionUnderlines();
}
};
Tabs.prototype.openPanel = function (panel, index) {
var _this = this;
if (index === void 0) { index = this._tabs.length; }
if (this._tabMap.has(panel.id)) {
return;
}
var tab = new tab_1.Tab(panel, this.accessor, this.group);
tab.setContent(panel.view.tab);
if (this._direction !== 'horizontal') {
tab.setDirection(this._direction);
}
var disposable = new lifecycle_1.CompositeDisposable(tab.onDragStart(function (event) {
var _a;
_this._onTabDragStart.fire({ nativeEvent: event, panel: panel });
// Both HTML5 and pointer drags initialize _animState. Cleanup
// is wired in both paths: HTML5 via dragend/drop on _tabsList,
// pointer via PointerDragController.onDragEnd subscriptions.
if (((_a = _this.accessor.options.theme) === null || _a === void 0 ? void 0 : _a.tabAnimation) === 'smooth') {
var tabWidth = tab.element.getBoundingClientRect().width;
var sourceIndex_1 = _this._tabs.findIndex(function (x) { return x.value === tab; });
_this._animState = {
sourceTabId: panel.id,
sourceIndex: sourceIndex_1,
tabPositions: _this.snapshotTabPositions(),
chipPositions: _this._tabGroupManager.snapshotChipWidths(),
currentInsertionIndex: null,
targetTabGroupId: null,
sourceTabGroupId: null,
sourceGroupPanelIds: null,
sourceChipWidth: 0,
cursorOffsetFromDragLeft: tabWidth / 2,
sourceGapWidth: tabWidth,
containerLeft: _this._tabsList.getBoundingClientRect().left,
};
// Collapse the source tab after the browser captures the
// drag image, then open the gap at the source position in
// the same paint frame — no visual jump.
// Both collapse and gap must be instant (no transition).
_this._pendingCollapse = true;
requestAnimationFrame(function () {
var _a;
var _b;
_this._pendingCollapse = false;
if (!_this._animState) {
return;
}
// Collapse source tab instantly (no transition)
tab.element.style.transition = 'none';
(0, dom_1.toggleClass)(tab.element, 'dv-tab--dragging', true);
void tab.element.offsetHeight; // force reflow
(_a = (_b = _this._animState).currentInsertionIndex) !== null && _a !== void 0 ? _a : (_b.currentInsertionIndex = sourceIndex_1);
// Apply gap with transitions disabled on the target
_this.applyDragOverTransforms(true);
// Re-enable transitions for subsequent moves
tab.element.style.removeProperty('transition');
});
}
}), tab.onTabClick(function (event) {
if (event.defaultPrevented) {
return;
}
if (_this.group.api.location.type !== 'edge') {
return;
}
if (_this.group.activePanel === panel) {
// Clicking the active tab toggles expansion
if (_this.group.api.isCollapsed()) {
_this.group.api.expand();
}
else {
_this.group.api.collapse();
}
}
else {
// Clicking a non-active tab switches the active tab.
// If the group is collapsed, also expand it.
_this.group.model.openPanel(panel);
if (_this.group.api.isCollapsed()) {
_this.group.api.expand();
}
}
}), tab.onPointerDown(function (event) {
if (event.defaultPrevented) {
return;
}
var isFloatingGroupsEnabled = !_this.accessor.options.disableFloatingGroups;
var isFloatingWithOnePanel = _this.group.api.location.type === 'floating' &&
_this.size === 1;
if (isFloatingGroupsEnabled &&
!isFloatingWithOnePanel &&
event.shiftKey) {
event.preventDefault();
var panel_1 = _this.accessor.getGroupPanel(tab.panel.id);
var _a = tab.element.getBoundingClientRect(), top_1 = _a.top, left = _a.left;
var _b = _this.accessor.element.getBoundingClientRect(), rootTop = _b.top, rootLeft = _b.left;
_this.accessor.addFloatingGroup(panel_1, {
x: left - rootLeft,
y: top_1 - rootTop,
inDragMode: true,
});
return;
}
switch (event.button) {
case 0:
if (_this.group.api.location.type === 'edge') {
// All tab interaction for edge groups is handled by
// onTabClick to avoid race conditions with active panel state
}
else {
if (_this.group.activePanel !== panel) {
_this.group.model.openPanel(panel);
}
}
break;
}
}), tab.onDrop(function (event) {
var _a, _b, _c, _d;
var animState = _this._animState;
_this._animState = null;
_this._pendingCollapse = false;
var tabIndex = _this._tabs.findIndex(function (x) { return x.value === tab; });
if (animState) {
var dropIndex = event.position === 'right' ? tabIndex + 1 : tabIndex;
if (animState.sourceTabGroupId) {
_this._commitGroupMove(animState.sourceTabGroupId, (_a = animState.currentInsertionIndex) !== null && _a !== void 0 ? _a : dropIndex);
return;
}
_this._uncollapsSourceTab(animState.sourceTabId);
var firstPositions = _this.snapshotTabPositions();
_this.resetTabTransforms();
_this._onDrop.fire({
event: event.nativeEvent,
index: dropIndex,
targetTabGroupId: animState.targetTabGroupId,
});
if (((_b = _this.accessor.options.theme) === null || _b === void 0 ? void 0 : _b.tabAnimation) === 'smooth') {
_this.runFlipAnimation(firstPositions, animState.sourceTabId, animState.sourceIndex === -1, animState.sourceIndex !== -1
? {
from: Math.min(animState.sourceIndex, dropIndex),
to: Math.max(animState.sourceIndex, dropIndex),
}
: undefined);
}
}
else {
// Compute insertion index based on which half of the tab
// the pointer is over, then adjust for same-group removal:
// when the source tab sits before the insertion point,
// removing it shifts all subsequent indices down by one.
var afterPosition = _this._direction === 'vertical' ? 'bottom' : 'right';
var insertionIndex = event.position === afterPosition
? tabIndex + 1
: tabIndex;
var data_1 = (0, dataTransfer_1.getPanelData)();
var sourceIndex = data_1
? _this._tabs.findIndex(function (x) { return x.value.panel.id === data_1.panelId; })
: -1;
var adjustedIndex = insertionIndex -
(sourceIndex !== -1 && sourceIndex < insertionIndex
? 1
: 0);
var targetTabGroupId = (_d = (_c = _this.group.model.getTabGroupForPanel(tab.panel.id)) === null || _c === void 0 ? void 0 : _c.id) !== null && _d !== void 0 ? _d : null;
_this._onDrop.fire({
event: event.nativeEvent,
index: adjustedIndex,
targetTabGroupId: targetTabGroupId,
});
}
}), tab.onWillShowOverlay(function (event) {
_this._onWillShowOverlay.fire(new events_2.DockviewWillShowOverlayLocationEvent(event, {
kind: 'tab',
panel: _this.group.activePanel,
api: _this.accessor.api,
group: _this.group,
getData: dataTransfer_1.getPanelData,
}));
}));
var value = { value: tab, disposable: disposable };
this.addTab(value, index);
// A new tab may have been inserted between a chip and its
// group's first tab — reposition all chips to stay correct.
this._tabGroupManager.positionAllChips();
// If a tab was added during active drag, refresh positions
if (this._animState) {
this._animState.tabPositions = this.snapshotTabPositions();
this._animState.chipPositions =
this._tabGroupManager.snapshotChipWidths();
this.applyDragOverTransforms();
}
};
Tabs.prototype.delete = function (id) {
var _a;
if (((_a = this._animState) === null || _a === void 0 ? void 0 : _a.sourceTabId) === id) {
this.resetTabTransforms();
this._animState = null;
}
// Force-clean any pending transitionend listener
this._tabGroupManager.cleanupTransition(id);
var index = this.indexOf(id);
var tabToRemove = this._tabs.splice(index, 1)[0];
this._tabMap.delete(id);
if (tabToRemove) {
var value = tabToRemove.value, disposable = tabToRemove.disposable;
disposable.dispose();
value.dispose();
value.element.remove();
}
// If a non-source tab was removed during active drag, refresh positions
if (this._animState) {
this._animState.tabPositions = this.snapshotTabPositions();
this._animState.chipPositions =
this._tabGroupManager.snapshotChipWidths();
this.applyDragOverTransforms();
}
};
Tabs.prototype.addTab = function (tab, index) {
if (index === void 0) { index = this._tabs.length; }
if (index < 0 || index > this._tabs.length) {
throw new Error('invalid location');
}
// Use the tab element at `index` as the reference node rather than
// `children[index]`, because `_tabsList` may contain non-tab children
// (e.g. group chips, underlines) that shift the DOM indices.
var refNode = index < this._tabs.length ? this._tabs[index].value.element : null;
this._tabsList.insertBefore(tab.value.element, refNode);
this._tabs = __spreadArray(__spreadArray(__spreadArray([], __read(this._tabs.slice(0, index)), false), [
tab
], false), __read(this._tabs.slice(index)), false);
this._tabMap.set(tab.value.panel.id, tab);
if (this.selectedIndex < 0) {
this.selectedIndex = index;
}
};
Tabs.prototype.toggleDropdown = function (options) {
var e_4, _a, e_5, _b;
var _this = this;
if (options.reset) {
this._onOverflowTabsChange.fire({
tabs: [],
tabGroups: [],
reset: true,
});
return;
}
var tabs = this._tabs
.filter(function (tab) {
return !(0, dom_1.isChildEntirelyVisibleWithinParent)(tab.value.element, _this._tabsList);
})
.map(function (x) { return x.value.panel.id; });
// Detect tab groups whose chip is clipped or whose tabs are all
// in the overflow set (e.g. collapsed groups scrolled out of view).
var overflowTabSet = new Set(tabs);
var tabGroups = [];
try {
for (var _c = __values(this.group.model.getTabGroups()), _d = _c.next(); !_d.done; _d = _c.next()) {
var tg = _d.value;
var chipEntry = this._tabGroupManager.chipRenderers.get(tg.id);
var chipClipped = chipEntry &&
!(0, dom_1.isChildEntirelyVisibleWithinParent)(chipEntry.chip.element, this._tabsList);
// A group is in overflow if its chip is clipped OR all its
// visible tabs are in the overflow set.
var allTabsOverflow = tg.panelIds.length > 0 &&
tg.panelIds.every(function (pid) { return overflowTabSet.has(pid); });
if (chipClipped || allTabsOverflow) {
tabGroups.push(tg.id);
// For collapsed groups whose chip is clipped, ensure all
// member tabs are included in the overflow list so they
// appear in the dropdown.
if (tg.collapsed) {
try {
for (var _e = (e_5 = void 0, __values(tg.panelIds)), _f = _e.next(); !_f.done; _f = _e.next()) {
var pid = _f.value;
if (!overflowTabSet.has(pid)) {
overflowTabSet.add(pid);
tabs.push(pid);
}
}
}
catch (e_5_1) { e_5 = { error: e_5_1 }; }
finally {
try {
if (_f && !_f.done && (_b = _e.return)) _b.call(_e);
}
finally { if (e_5) throw e_5.error; }
}
}
}
}
}
catch (e_4_1) { e_4 = { error: e_4_1 }; }
finally {
try {
if (_d && !_d.done && (_a = _c.return)) _a.call(_c);
}
finally { if (e_4) throw e_4.error; }
}
this._onOverflowTabsChange.fire({ tabs: tabs, tabGroups: tabGroups, reset: false });
};
Tabs.prototype.updateDragAndDropState = function () {
var e_6, _a;
try {
for (var _b = __values(this._tabs), _c = _b.next(); !_c.done; _c = _b.next()) {
var tab = _c.value;
tab.value.updateDragAndDropState();
}
}
catch (e_6_1) { e_6 = { error: e_6_1 }; }
finally {
try {
if (_c && !_c.done && (_a = _b.return)) _a.call(_b);
}
finally { if (e_6) throw e_6.error; }
}
this._tabGroupManager.updateDragAndDropState();
};
/**
* Synchronize chip elements and CSS classes for all tab groups
* in the parent group model. Call after any tab group mutation.
*/
Tabs.prototype.updateTabGroups = function () {
this._tabGroupManager.update();
};
Tabs.prototype.refreshTabGroupAccent = function () {
this._tabGroupManager.refreshAccents();
};
/**
* Tabs-list-specific side effects of a chip drag start. The chip's
* drag sources (constructed by `TabGroupManager`) own the transfer
* payload, iframe shielding, dataTransfer setup, and the HTML5 drag
* image. This method just sets up the smooth-reorder anim state and
* collapses the source-group tabs in the tabs list.
*/
Tabs.prototype._handleChipDragStart = function (tabGroup, chip, event) {
var e_7, _a;
var _this = this;
var _b;
var firstPanelId = tabGroup.panelIds[0];
var firstIdx = firstPanelId
? this._tabs.findIndex(function (t) { return t.value.panel.id === firstPanelId; })
: -1;
var chipRect = chip.element.getBoundingClientRect();
// Compute total group width (chip + all tabs)
var groupGapWidth = chipRect.width;
try {
for (var _c = __values(tabGroup.panelIds), _d = _c.next(); !_d.done; _d = _c.next()) {
var pid = _d.value;
var tabEntry = this._tabMap.get(pid);
if (tabEntry) {
groupGapWidth +=
tabEntry.value.element.getBoundingClientRect().width;
}
}
}
catch (e_7_1) { e_7 = { error: e_7_1 }; }
finally {
try {
if (_d && !_d.done && (_a = _c.return)) _a.call(_c);
}
finally { if (e_7) throw e_7.error; }
}
this._animState = {
sourceTabId: '',
sourceIndex: firstIdx,
tabPositions: this.snapshotTabPositions(),
chipPositions: this._tabGroupManager.snapshotChipWidths(),
currentInsertionIndex: null,
targetTabGroupId: null,
sourceTabGroupId: tabGroup.id,
sourceGroupPanelIds: new Set(tabGroup.panelIds),
sourceChipWidth: chipRect.width,
cursorOffsetFromDragLeft: event.clientX - chipRect.left,
sourceGapWidth: groupGapWidth,
containerLeft: this._tabsList.getBoundingClientRect().left,
};
if (((_b = this.accessor.options.theme) === null || _b === void 0 ? void 0 : _b.tabAnimation) !== 'smooth') {
return;
}
// Collapse group tabs + chip after the browser captures the drag
// image, then open the gap at the source position — all instant
// (no transitions).
var groupPanelIds = new Set(tabGroup.panelIds);
this._pendingCollapse = true;
requestAnimationFrame(function () {
var e_8, _a, e_9, _b;
var _c;
var _d;
_this._pendingCollapse = false;
if (!_this._animState) {
return;
}
try {
// Collapse all group tabs instantly
for (var _e = __values(_this._tabs), _f = _e.next(); !_f.done; _f = _e.next()) {
var t = _f.value;
if (groupPanelIds.has(t.value.panel.id)) {
t.value.element.style.transition = 'none';
(0, dom_1.toggleClass)(t.value.element, 'dv-tab--dragging', true);
}
}
}
catch (e_8_1) { e_8 = { error: e_8_1 }; }
finally {
try {
if (_f && !_f.done && (_a = _e.return)) _a.call(_e);
}
finally { if (e_8) throw e_8.error; }
}
// Collapse the group chip instantly
var chipEntry = _this._tabGroupManager.chipRenderers.get(tabGroup.id);
if (chipEntry) {
chipEntry.chip.element.style.transition = 'none';
(0, dom_1.toggleClass)(chipEntry.chip.element, 'dv-tab-group-chip--dragging', true);
}
// Single reflow for the entire batch
void _this._tabsList.offsetHeight;
var underline = _this._tabGroupManager.groupUnderlines.get(tabGroup.id);
if (underline) {
underline.style.display = 'none';
}
(_c = (_d = _this._animState).currentInsertionIndex) !== null && _c !== void 0 ? _c : (_d.currentInsertionIndex = firstIdx);
_this.applyDragOverTransforms(true);
try {
for (var _g = __values(_this._tabs), _h = _g.next(); !_h.done; _h = _g.next()) {
var t = _h.value;
if (groupPanelIds.has(t.value.panel.id)) {
t.value.element.style.removeProperty('transition');
}
}
}
catch (e_9_1) { e_9 = { error: e_9_1 }; }
finally {
try {
if (_h && !_h.done && (_b = _g.return)) _b.call(_g);
}
finally { if (e_9) throw e_9.error; }
}
if (chipEntry) {
chipEntry.chip.element.style.removeProperty('transition');
}
});
};
/**
* A drop on a tab group chip means "insert before this group". Resolve to
* the index of the group's first tab, adjusting for same-group removal
* (when the source tab is currently to the left of the target slot, its
* removal shifts the insertion index down by one). Always clears
* `targetTabGroupId` so the dropped tab lands outside the group.
*/
Tabs.prototype._handleChipDrop = function (tabGroup, event) {
var firstPanelId = tabGroup.panelIds[0];
if (!firstPanelId) {
return;
}
var insertionIndex = this._tabs.findIndex(function (x) { return x.value.panel.id === firstPanelId; });
if (insertionIndex === -1) {
return;
}
var data = (0, dataTransfer_1.getPanelData)();
var sourceIndex = data && data.groupId === this.group.id && data.panelId
? this._tabs.findIndex(function (x) { return x.value.panel.id === data.panelId; })
: -1;
var adjustedIndex = insertionIndex -
(sourceIndex !== -1 && sourceIndex < insertionIndex ? 1 : 0);
this._onDrop.fire({
event: event.nativeEvent,
index: adjustedIndex,
targetTabGroupId: null,
});
};
/**
* Sets the broader container that is part of the same logical drop surface
* as this tab list (e.g. the full header element). When a dragleave from
* the tabs list lands inside this container, `_animState` is preserved so
* that external dragover listeners can continue the animation.
*/
Tabs.prototype.setExtendedDropZone = function (el) {
this._extendedDropZone = el;
};
/**
* Allows external elements (e.g. void container, left actions) to push an
* insertion index into the animation while the cursor is outside the tabs
* list itself. Pass `null` to clear the indicator.
*/
Tabs.prototype.setExternalInsertionIndex = function (index) {
if (!this._animState) {
return;
}
if (index === this._animState.currentInsertionIndex) {
return;
}
this._animState.currentInsertionIndex = index;
this.applyDragOverTransforms();
};
/**
* Called when the drag cursor leaves the entire header area (not just the
* tabs list). Clears animation state for cross-group drags, which never
* receive a `dragend` event on this tab list.
*/
Tabs.prototype.clearExternalAnimState = function () {
if (!this._animState) {
return;
}
this.resetTabTransforms();
if (this._animState.sourceIndex === -1) {
this._animState = null;
}
else {
this._animState.currentInsertionIndex = null;
}
};
Tabs.prototype.snapshotTabPositions = function () {
var e_10, _a;
var positions = new Map();
try {
for (var _b = __values(this._tabs), _c = _b.next(); !_c.done; _c = _b.next()) {
var tab = _c.value;
positions.set(tab.value.panel.id, tab.value.element.getBoundingClientRect());
}
}
catch (e_10_1) { e_10 = { error: e_10_1 }; }
finally {
try {
if (_c && !_c.done && (_a = _b.return)) _a.call(_b);
}
finally { if (e_10) throw e_10.error; }
}
return positions;
};
Tabs.prototype.getAverageTabWidth = function () {
var e_11, _a;
if (this._tabs.length === 0) {
return 0;
}
var isVertical = this._direction === 'vertical';
var total = 0;
try {
for (var _b = __values(this._tabs), _c = _b.next(); !_c.done; _c = _b.next()) {
var tab = _c.value;
var rect = tab.value.element.getBoundingClientRect();
total += isVertical ? rect.height : rect.width;
}
}
catch (e_11_1) { e_11 = { error: e_11_1 }; }
finally {
try {
if (_c && !_c.done && (_a = _b.return)) _a.call(_b);
}
finally { if (e_11) throw e_11.error; }
}
return total / this._tabs.length;
};
/**
* Pointer-event entry point. The HTML5 path enters via the per-element
* `dragover` listener; this one hit-tests the global pointer-drag
* position against the tabs list and routes through the same shared
* `_processDragOver` / `_processDragLeave` helpers.
*/
Tabs.prototype._handlePointerDragMove = function (clientX, clientY) {
var _a;
var sourceDoc = (_a = this._tabsList.ownerDocument) !== null && _a !== void 0 ? _a : document;
var elAtPoint = sourceDoc.elementFromPoint(clientX, clientY);
var inside = !!elAtPoint &&
(this._tabsList.contains(elAtPoint) ||
(!!this._extendedDropZone &&
this._extendedDropZone.contains(elAtPoint)));
if (!inside) {
if (this._pointerInsideTabsList) {
this._pointerInsideTabsList = false;
this._processDragLeave(elAtPoint);
}
return;
}
this._pointerInsideTabsList = true;
this._processDragOver(clientX);
};
/**
* Shared body of the dragover entry point. Refreshes stale anim state
* for a changed drag identity, initializes anim state for incoming
* cross-group drags, and dispatches to the gap-following math in
* `handleDragOver`. Returns true when this tabs list has taken
* ownership of the drag — HTML5 callers use this to gate
* `event.preventDefault()`.
*/
Tabs.prototype._processDragOver = function (clientX) {
var _a, _b, _c, _d;
if (this.accessor.options.disableDnd) {
return false;
}
// Stale-state guard: if a previous drag's anim state is still here
// but the current drag is a different identity, drop the stale one
// so the new drag starts from a clean slate.
if (this._animState) {
var data = (0, dataTransfer_1.getPanelData)();
if ((data === null || data === void 0 ? void 0 : data.tabGroupId) &&
data.groupId !== this.group.id &&
this._animState.sourceTabGroupId !== data.tabGroupId) {
this._animState = null;
}
}
if (!this._animState) {
var data_2 = (0, dataTransfer_1.getPanelData)();
// In default animation mode, individual tab drops are handled
// by per-tab Droptargets; only chip drags need tabs-list-level
// handling so drops on void space still work.
if (((_a = this.accessor.options.theme) === null || _a === void 0 ? void 0 : _a.tabAnimation) === 'default' &&
!(data_2 === null || data_2 === void 0 ? void 0 : data_2.tabGroupId)) {
return false;
}
if (data_2 &&
(data_2.panelId || data_2.tabGroupId) &&
data_2.groupId !== this.group.id) {
var avgWidth = this.getAverageTabWidth();
if (data_2.tabGroupId) {
// External group drag — look up the source group to
// size the gap.
var sourceGroup = this.accessor.getPanel(data_2.groupId);
var sourceTg = sourceGroup === null || sourceGroup === void 0 ? void 0 : sourceGroup.model.getTabGroups().find(function (tg) { return tg.id === data_2.tabGroupId; });
var panelCount = (_b = sourceTg === null || sourceTg === void 0 ? void 0 : so