dockview-core 5.2.0 → 6.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/dist/cjs/api/component.api.d.ts +93 -1
- package/dist/cjs/api/component.api.js +146 -0
- package/dist/cjs/api/dockviewGroupPanelApi.d.ts +26 -0
- package/dist/cjs/api/dockviewGroupPanelApi.js +21 -1
- package/dist/cjs/api/entryPoints.js +4 -5
- package/dist/cjs/array.js +7 -8
- package/dist/cjs/dnd/dataTransfer.d.ts +2 -1
- package/dist/cjs/dnd/dataTransfer.js +5 -4
- package/dist/cjs/dnd/droptarget.d.ts +12 -0
- package/dist/cjs/dnd/droptarget.js +38 -10
- package/dist/cjs/dnd/ghost.js +1 -2
- package/dist/cjs/dockview/components/panel/content.js +5 -1
- package/dist/cjs/dockview/components/popupService.d.ts +9 -2
- package/dist/cjs/dockview/components/popupService.js +24 -9
- package/dist/cjs/dockview/components/tab/tab.d.ts +6 -1
- package/dist/cjs/dockview/components/tab/tab.js +81 -9
- package/dist/cjs/dockview/components/titlebar/tabGroupChip.d.ts +30 -0
- package/dist/cjs/dockview/components/titlebar/tabGroupChip.js +95 -0
- package/dist/cjs/dockview/components/titlebar/tabGroupIndicator.d.ts +71 -0
- package/dist/cjs/dockview/components/titlebar/tabGroupIndicator.js +471 -0
- package/dist/cjs/dockview/components/titlebar/tabGroups.d.ts +57 -0
- package/dist/cjs/dockview/components/titlebar/tabGroups.js +612 -0
- package/dist/cjs/dockview/components/titlebar/tabOverflowControl.js +1 -2
- package/dist/cjs/dockview/components/titlebar/tabs.d.ts +59 -0
- package/dist/cjs/dockview/components/titlebar/tabs.js +1227 -144
- package/dist/cjs/dockview/components/titlebar/tabsContainer.d.ts +6 -0
- package/dist/cjs/dockview/components/titlebar/tabsContainer.js +132 -14
- package/dist/cjs/dockview/contextMenu.d.ts +10 -0
- package/dist/cjs/dockview/contextMenu.js +298 -0
- package/dist/cjs/dockview/dockviewComponent.d.ts +60 -3
- package/dist/cjs/dockview/dockviewComponent.js +712 -126
- package/dist/cjs/dockview/dockviewGroupPanelModel.d.ts +83 -0
- package/dist/cjs/dockview/dockviewGroupPanelModel.js +619 -27
- package/dist/cjs/dockview/dockviewShell.d.ts +128 -0
- package/dist/cjs/dockview/dockviewShell.js +681 -0
- package/dist/cjs/dockview/events.d.ts +9 -0
- package/dist/cjs/dockview/framework.d.ts +14 -0
- package/dist/cjs/dockview/options.d.ts +92 -10
- package/dist/cjs/dockview/options.js +10 -7
- package/dist/cjs/dockview/tabGroup.d.ts +99 -0
- package/dist/cjs/dockview/tabGroup.js +219 -0
- package/dist/cjs/dockview/tabGroupAccent.d.ts +65 -0
- package/dist/cjs/dockview/tabGroupAccent.js +128 -0
- package/dist/cjs/dockview/theme.d.ts +56 -1
- package/dist/cjs/dockview/theme.js +103 -6
- package/dist/cjs/dockview/types.d.ts +2 -0
- package/dist/cjs/dom.js +19 -19
- package/dist/cjs/events.js +2 -2
- package/dist/cjs/gridview/baseComponentGridview.d.ts +1 -0
- package/dist/cjs/gridview/baseComponentGridview.js +6 -3
- package/dist/cjs/gridview/gridview.js +7 -7
- package/dist/cjs/index.d.ts +8 -5
- package/dist/cjs/index.js +6 -1
- package/dist/cjs/popoutWindow.js +3 -3
- package/dist/cjs/splitview/splitviewPanel.d.ts +1 -1
- package/dist/dockview-core.js +6942 -2777
- package/dist/dockview-core.min.js +2 -2
- package/dist/dockview-core.min.js.map +1 -1
- package/dist/dockview-core.min.noStyle.js +2 -2
- package/dist/dockview-core.min.noStyle.js.map +1 -1
- package/dist/dockview-core.noStyle.js +6940 -2775
- package/dist/esm/api/component.api.d.ts +93 -1
- package/dist/esm/api/component.api.js +118 -0
- package/dist/esm/api/dockviewGroupPanelApi.d.ts +26 -0
- package/dist/esm/api/dockviewGroupPanelApi.js +21 -1
- package/dist/esm/dnd/dataTransfer.d.ts +2 -1
- package/dist/esm/dnd/dataTransfer.js +2 -1
- package/dist/esm/dnd/droptarget.d.ts +12 -0
- package/dist/esm/dnd/droptarget.js +33 -5
- package/dist/esm/dockview/components/panel/content.js +5 -1
- package/dist/esm/dockview/components/popupService.d.ts +9 -2
- package/dist/esm/dockview/components/popupService.js +23 -9
- package/dist/esm/dockview/components/tab/tab.d.ts +6 -1
- package/dist/esm/dockview/components/tab/tab.js +83 -9
- package/dist/esm/dockview/components/titlebar/tabGroupChip.d.ts +30 -0
- package/dist/esm/dockview/components/titlebar/tabGroupChip.js +68 -0
- package/dist/esm/dockview/components/titlebar/tabGroupIndicator.d.ts +71 -0
- package/dist/esm/dockview/components/titlebar/tabGroupIndicator.js +354 -0
- package/dist/esm/dockview/components/titlebar/tabGroups.d.ts +57 -0
- package/dist/esm/dockview/components/titlebar/tabGroups.js +406 -0
- package/dist/esm/dockview/components/titlebar/tabs.d.ts +59 -0
- package/dist/esm/dockview/components/titlebar/tabs.js +1011 -99
- package/dist/esm/dockview/components/titlebar/tabsContainer.d.ts +6 -0
- package/dist/esm/dockview/components/titlebar/tabsContainer.js +105 -7
- package/dist/esm/dockview/contextMenu.d.ts +10 -0
- package/dist/esm/dockview/contextMenu.js +213 -0
- package/dist/esm/dockview/dockviewComponent.d.ts +60 -3
- package/dist/esm/dockview/dockviewComponent.js +460 -35
- package/dist/esm/dockview/dockviewGroupPanelModel.d.ts +83 -0
- package/dist/esm/dockview/dockviewGroupPanelModel.js +460 -4
- package/dist/esm/dockview/dockviewShell.d.ts +128 -0
- package/dist/esm/dockview/dockviewShell.js +621 -0
- package/dist/esm/dockview/events.d.ts +9 -0
- package/dist/esm/dockview/framework.d.ts +14 -0
- package/dist/esm/dockview/options.d.ts +92 -10
- package/dist/esm/dockview/options.js +5 -2
- package/dist/esm/dockview/tabGroup.d.ts +99 -0
- package/dist/esm/dockview/tabGroup.js +144 -0
- package/dist/esm/dockview/tabGroupAccent.d.ts +65 -0
- package/dist/esm/dockview/tabGroupAccent.js +116 -0
- package/dist/esm/dockview/theme.d.ts +56 -1
- package/dist/esm/dockview/theme.js +102 -5
- package/dist/esm/dockview/types.d.ts +2 -0
- package/dist/esm/dom.js +1 -1
- package/dist/esm/gridview/baseComponentGridview.d.ts +1 -0
- package/dist/esm/gridview/baseComponentGridview.js +4 -1
- package/dist/esm/index.d.ts +8 -5
- package/dist/esm/index.js +2 -1
- package/dist/esm/popoutWindow.js +1 -1
- package/dist/esm/splitview/splitviewPanel.d.ts +1 -1
- package/dist/package/main.cjs.js +6936 -2801
- package/dist/package/main.cjs.min.js +2 -2
- package/dist/package/main.esm.min.mjs +2 -2
- package/dist/package/main.esm.mjs +6922 -2800
- package/dist/styles/dockview.css +1945 -196
- package/package.json +5 -1
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { getPanelData } from '../../../dnd/dataTransfer';
|
|
2
|
-
import { addClasses, isChildEntirelyVisibleWithinParent, OverflowObserver, removeClasses, toggleClass, } from '../../../dom';
|
|
1
|
+
import { getPanelData, LocalSelectionTransfer, PanelTransfer, } from '../../../dnd/dataTransfer';
|
|
2
|
+
import { addClasses, disableIframePointEvents, isChildEntirelyVisibleWithinParent, OverflowObserver, removeClasses, toggleClass, } from '../../../dom';
|
|
3
3
|
import { addDisposableListener, Emitter } from '../../../events';
|
|
4
4
|
import { CompositeDisposable, Disposable, MutableDisposable, } from '../../../lifecycle';
|
|
5
5
|
import { Scrollbar } from '../../../scrollbar';
|
|
6
6
|
import { DockviewWillShowOverlayLocationEvent } from '../../events';
|
|
7
7
|
import { Tab } from '../tab/tab';
|
|
8
|
+
import { TabGroupManager } from './tabGroups';
|
|
8
9
|
export class Tabs extends CompositeDisposable {
|
|
9
10
|
get showTabsOverflowControl() {
|
|
10
11
|
return this._showTabsOverflowControl;
|
|
@@ -19,14 +20,57 @@ export class Tabs extends CompositeDisposable {
|
|
|
19
20
|
this._observerDisposable.value = new CompositeDisposable(observer, observer.onDidChange((event) => {
|
|
20
21
|
const hasOverflow = event.hasScrollX || event.hasScrollY;
|
|
21
22
|
this.toggleDropdown({ reset: !hasOverflow });
|
|
23
|
+
if (this._tabGroupManager.groupUnderlines.size > 0) {
|
|
24
|
+
this._tabGroupManager.positionUnderlines();
|
|
25
|
+
}
|
|
22
26
|
}), addDisposableListener(this._tabsList, 'scroll', () => {
|
|
23
27
|
this.toggleDropdown({ reset: false });
|
|
28
|
+
if (this._tabGroupManager.groupUnderlines.size > 0) {
|
|
29
|
+
this._tabGroupManager.positionUnderlines();
|
|
30
|
+
}
|
|
24
31
|
}));
|
|
25
32
|
}
|
|
26
33
|
}
|
|
27
34
|
get element() {
|
|
28
35
|
return this._element;
|
|
29
36
|
}
|
|
37
|
+
set voidContainer(el) {
|
|
38
|
+
var _a;
|
|
39
|
+
(_a = this._voidContainerListeners) === null || _a === void 0 ? void 0 : _a.dispose();
|
|
40
|
+
this._voidContainerListeners = null;
|
|
41
|
+
this._voidContainer = el;
|
|
42
|
+
if (el) {
|
|
43
|
+
this._voidContainerListeners = new CompositeDisposable(addDisposableListener(el, 'dragover', (event) => {
|
|
44
|
+
if (this._animState) {
|
|
45
|
+
event.preventDefault();
|
|
46
|
+
}
|
|
47
|
+
}), addDisposableListener(el, 'drop', (event) => {
|
|
48
|
+
var _a;
|
|
49
|
+
if (((_a = this._animState) === null || _a === void 0 ? void 0 : _a.sourceTabGroupId) &&
|
|
50
|
+
this._animState.currentInsertionIndex !== null) {
|
|
51
|
+
event.preventDefault();
|
|
52
|
+
event.stopPropagation();
|
|
53
|
+
this.handleVoidDrop();
|
|
54
|
+
}
|
|
55
|
+
}));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Handle a drop that occurred on the void container (empty header
|
|
60
|
+
* space to the right of the tabs). Returns `true` if the drop was
|
|
61
|
+
* consumed by an active group drag, `false` otherwise.
|
|
62
|
+
*/
|
|
63
|
+
handleVoidDrop() {
|
|
64
|
+
var _a, _b;
|
|
65
|
+
if (!((_a = this._animState) === null || _a === void 0 ? void 0 : _a.sourceTabGroupId)) {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
const sourceTabGroupId = this._animState.sourceTabGroupId;
|
|
69
|
+
const insertionIndex = (_b = this._animState.currentInsertionIndex) !== null && _b !== void 0 ? _b : this._tabs.length;
|
|
70
|
+
this._animState = null;
|
|
71
|
+
this._commitGroupMove(sourceTabGroupId, insertionIndex);
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
30
74
|
get panels() {
|
|
31
75
|
return this._tabs.map((_) => _.value.panel.id);
|
|
32
76
|
}
|
|
@@ -55,6 +99,9 @@ export class Tabs extends CompositeDisposable {
|
|
|
55
99
|
removeClasses(this._tabsList, 'dv-tabs-container-vertical');
|
|
56
100
|
addClasses(this._tabsList, 'dv-horizontal');
|
|
57
101
|
}
|
|
102
|
+
for (const tab of this._tabs) {
|
|
103
|
+
tab.value.setDirection(value);
|
|
104
|
+
}
|
|
58
105
|
}
|
|
59
106
|
constructor(group, accessor, options) {
|
|
60
107
|
super();
|
|
@@ -63,10 +110,18 @@ export class Tabs extends CompositeDisposable {
|
|
|
63
110
|
this._observerDisposable = new MutableDisposable();
|
|
64
111
|
this._scrollbar = null;
|
|
65
112
|
this._tabs = [];
|
|
113
|
+
this._tabMap = new Map();
|
|
66
114
|
this.selectedIndex = -1;
|
|
67
115
|
this._showTabsOverflowControl = false;
|
|
68
116
|
this._direction = 'horizontal';
|
|
69
117
|
this._animState = null;
|
|
118
|
+
this._pendingMarginCleanups = new Map();
|
|
119
|
+
this._pendingCollapse = false;
|
|
120
|
+
this._flipTransitionCleanup = null;
|
|
121
|
+
this._voidContainer = null;
|
|
122
|
+
this._voidContainerListeners = null;
|
|
123
|
+
this._extendedDropZone = null;
|
|
124
|
+
this._chipDragCleanup = null;
|
|
70
125
|
this._onTabDragStart = new Emitter();
|
|
71
126
|
this.onTabDragStart = this._onTabDragStart.event;
|
|
72
127
|
this._onDrop = new Emitter();
|
|
@@ -87,7 +142,27 @@ export class Tabs extends CompositeDisposable {
|
|
|
87
142
|
this._element = this._scrollbar.element;
|
|
88
143
|
this.addDisposables(this._scrollbar);
|
|
89
144
|
}
|
|
90
|
-
this.
|
|
145
|
+
this._tabGroupManager = new TabGroupManager({
|
|
146
|
+
group: this.group,
|
|
147
|
+
accessor: this.accessor,
|
|
148
|
+
tabsList: this._tabsList,
|
|
149
|
+
getTabs: () => this._tabs,
|
|
150
|
+
getTabMap: () => this._tabMap,
|
|
151
|
+
getDirection: () => this._direction,
|
|
152
|
+
}, {
|
|
153
|
+
onChipContextMenu: (tabGroup, event) => {
|
|
154
|
+
this.accessor.contextMenuController.showForChip(tabGroup, this.group, event);
|
|
155
|
+
},
|
|
156
|
+
onChipDragStart: (tabGroup, chip, event) => {
|
|
157
|
+
this._handleChipDragStart(tabGroup, chip, event);
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
this.addDisposables(this._onOverflowTabsChange, this._observerDisposable, this._onWillShowOverlay, this._onDrop, this._onTabDragStart, {
|
|
161
|
+
dispose: () => {
|
|
162
|
+
var _a;
|
|
163
|
+
(_a = this._flipTransitionCleanup) === null || _a === void 0 ? void 0 : _a.call(this);
|
|
164
|
+
},
|
|
165
|
+
}, addDisposableListener(this.element, 'pointerdown', (event) => {
|
|
91
166
|
if (event.defaultPrevented) {
|
|
92
167
|
return;
|
|
93
168
|
}
|
|
@@ -96,42 +171,128 @@ export class Tabs extends CompositeDisposable {
|
|
|
96
171
|
this.accessor.doSetGroupActive(this.group);
|
|
97
172
|
}
|
|
98
173
|
}), addDisposableListener(this._tabsList, 'dragover', (event) => {
|
|
174
|
+
var _a, _b, _c, _d;
|
|
175
|
+
if (this.accessor.options.disableDnd) {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
// If _animState exists but belongs to a different
|
|
179
|
+
// drag (stale from a previous operation), replace it
|
|
180
|
+
// so the current drag is handled correctly.
|
|
181
|
+
if (this._animState) {
|
|
182
|
+
const data = getPanelData();
|
|
183
|
+
if ((data === null || data === void 0 ? void 0 : data.tabGroupId) &&
|
|
184
|
+
data.groupId !== this.group.id &&
|
|
185
|
+
this._animState.sourceTabGroupId !== data.tabGroupId) {
|
|
186
|
+
this._animState = null;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
99
189
|
if (!this._animState) {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
190
|
+
const data = getPanelData();
|
|
191
|
+
// In default animation mode, individual tab drops
|
|
192
|
+
// are handled by per-tab Droptargets. But tab group
|
|
193
|
+
// chip drags still need tab-list-level handling so
|
|
194
|
+
// that drops on gaps / void space work.
|
|
195
|
+
if (((_a = this.accessor.options.theme) === null || _a === void 0 ? void 0 : _a.tabAnimation) ===
|
|
196
|
+
'default' &&
|
|
197
|
+
!(data === null || data === void 0 ? void 0 : data.tabGroupId)) {
|
|
103
198
|
return;
|
|
104
199
|
}
|
|
105
|
-
const data = getPanelData();
|
|
106
200
|
if (data &&
|
|
107
|
-
data.panelId &&
|
|
201
|
+
(data.panelId || data.tabGroupId) &&
|
|
108
202
|
data.groupId !== this.group.id) {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
203
|
+
const avgWidth = this.getAverageTabWidth();
|
|
204
|
+
if (data.tabGroupId) {
|
|
205
|
+
// External group drag — look up the
|
|
206
|
+
// source tab group to size the gap
|
|
207
|
+
const sourceGroup = this.accessor.getPanel(data.groupId);
|
|
208
|
+
const sourceTg = sourceGroup === null || sourceGroup === void 0 ? void 0 : sourceGroup.model.getTabGroups().find((tg) => tg.id === data.tabGroupId);
|
|
209
|
+
const panelCount = (_b = sourceTg === null || sourceTg === void 0 ? void 0 : sourceTg.panelIds.length) !== null && _b !== void 0 ? _b : 1;
|
|
210
|
+
const groupGapWidth = avgWidth * panelCount + avgWidth;
|
|
211
|
+
this._animState = {
|
|
212
|
+
sourceTabId: '',
|
|
213
|
+
sourceIndex: -1,
|
|
214
|
+
tabPositions: this.snapshotTabPositions(),
|
|
215
|
+
chipPositions: this._tabGroupManager.snapshotChipWidths(),
|
|
216
|
+
currentInsertionIndex: null,
|
|
217
|
+
targetTabGroupId: null,
|
|
218
|
+
sourceTabGroupId: data.tabGroupId,
|
|
219
|
+
sourceGroupPanelIds: sourceTg
|
|
220
|
+
? new Set(sourceTg.panelIds)
|
|
221
|
+
: new Set(),
|
|
222
|
+
sourceChipWidth: avgWidth,
|
|
223
|
+
cursorOffsetFromDragLeft: groupGapWidth / 2,
|
|
224
|
+
sourceGapWidth: groupGapWidth,
|
|
225
|
+
containerLeft: this._tabsList.getBoundingClientRect()
|
|
226
|
+
.left,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
this._animState = {
|
|
231
|
+
sourceTabId: data.panelId,
|
|
232
|
+
sourceIndex: -1,
|
|
233
|
+
tabPositions: this.snapshotTabPositions(),
|
|
234
|
+
chipPositions: this._tabGroupManager.snapshotChipWidths(),
|
|
235
|
+
currentInsertionIndex: null,
|
|
236
|
+
targetTabGroupId: null,
|
|
237
|
+
sourceTabGroupId: null,
|
|
238
|
+
sourceGroupPanelIds: null,
|
|
239
|
+
sourceChipWidth: 0,
|
|
240
|
+
cursorOffsetFromDragLeft: avgWidth / 2,
|
|
241
|
+
sourceGapWidth: avgWidth,
|
|
242
|
+
containerLeft: this._tabsList.getBoundingClientRect()
|
|
243
|
+
.left,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
115
246
|
}
|
|
116
247
|
else {
|
|
117
248
|
return;
|
|
118
249
|
}
|
|
119
250
|
}
|
|
251
|
+
event.preventDefault(); // allow drop to fire on the container
|
|
252
|
+
// For intra-group drag (sourceIndex >= 0) the gap
|
|
253
|
+
// animation is the sole visual indicator — clear any
|
|
254
|
+
// stale anchor overlay that may have been set while the
|
|
255
|
+
// cursor was over the panel content area or another zone.
|
|
256
|
+
// External drags (sourceIndex === -1) leave the overlay
|
|
257
|
+
// to the individual tab Droptargets so cross-group
|
|
258
|
+
// animation is not disrupted.
|
|
259
|
+
if (this._animState.sourceIndex !== -1) {
|
|
260
|
+
(_d = (_c = this.group.model.dropTargetContainer) === null || _c === void 0 ? void 0 : _c.model) === null || _d === void 0 ? void 0 : _d.clear();
|
|
261
|
+
}
|
|
120
262
|
this.handleDragOver(event);
|
|
121
263
|
}, true), addDisposableListener(this._tabsList, 'dragleave', (event) => {
|
|
264
|
+
var _a, _b, _c;
|
|
122
265
|
if (!this._animState) {
|
|
123
266
|
return;
|
|
124
267
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
268
|
+
const related = event.relatedTarget;
|
|
269
|
+
// Ignore moves between children of the tabs list
|
|
270
|
+
if (related && this._tabsList.contains(related)) {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
// If moving into the broader drop zone (e.g. void container,
|
|
274
|
+
// left actions), keep _animState alive so the external
|
|
275
|
+
// dragover listeners can continue the gap animation.
|
|
276
|
+
if (related && ((_a = this._extendedDropZone) === null || _a === void 0 ? void 0 : _a.contains(related))) {
|
|
277
|
+
this.resetTabTransforms();
|
|
278
|
+
this._animState.currentInsertionIndex = null;
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
// When leaving toward the void container (empty header space
|
|
282
|
+
// to the right), keep the animation state so the drop can
|
|
283
|
+
// still land at the end position.
|
|
284
|
+
const rt = event.relatedTarget;
|
|
285
|
+
const isVoid = this._voidContainer &&
|
|
286
|
+
rt &&
|
|
287
|
+
(rt === this._voidContainer ||
|
|
288
|
+
this._voidContainer.contains(rt));
|
|
289
|
+
if (isVoid) {
|
|
128
290
|
return;
|
|
129
291
|
}
|
|
130
292
|
this.resetTabTransforms();
|
|
131
293
|
if (this._animState) {
|
|
132
294
|
if (this._animState.sourceIndex === -1) {
|
|
133
|
-
|
|
134
|
-
// (no dragend will fire on this tab list)
|
|
295
|
+
(_c = (_b = this.group.model.dropTargetContainer) === null || _b === void 0 ? void 0 : _b.model) === null || _c === void 0 ? void 0 : _c.clear();
|
|
135
296
|
this._animState = null;
|
|
136
297
|
}
|
|
137
298
|
else {
|
|
@@ -139,49 +300,72 @@ export class Tabs extends CompositeDisposable {
|
|
|
139
300
|
}
|
|
140
301
|
}
|
|
141
302
|
}, true), addDisposableListener(this._tabsList, 'dragend', () => {
|
|
142
|
-
// Only fires for cancel (not after successful drop, since
|
|
143
|
-
// source tab is removed from DOM and doesn't bubble)
|
|
144
303
|
this.resetDragAnimation();
|
|
145
304
|
}), addDisposableListener(this._tabsList, 'drop', (event) => {
|
|
146
|
-
|
|
147
|
-
|
|
305
|
+
var _a, _b, _c;
|
|
306
|
+
if (!this._animState ||
|
|
148
307
|
this._animState.currentInsertionIndex === null) {
|
|
149
308
|
return;
|
|
150
309
|
}
|
|
310
|
+
// In non-smooth mode only handle group drags here;
|
|
311
|
+
// individual tab drops are handled by tab Droptargets.
|
|
312
|
+
if (((_a = this.accessor.options.theme) === null || _a === void 0 ? void 0 : _a.tabAnimation) !==
|
|
313
|
+
'smooth' &&
|
|
314
|
+
!this._animState.sourceTabGroupId) {
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
151
317
|
event.stopPropagation();
|
|
152
318
|
event.preventDefault();
|
|
319
|
+
// The capturing stopPropagation above prevents the
|
|
320
|
+
// individual tab's Droptarget.onDrop from firing, so
|
|
321
|
+
// the anchor overlay won't be cleared by that path.
|
|
322
|
+
// Clear it explicitly here before processing the drop.
|
|
323
|
+
(_c = (_b = this.group.model.dropTargetContainer) === null || _b === void 0 ? void 0 : _b.model) === null || _c === void 0 ? void 0 : _c.clear();
|
|
153
324
|
const animState = this._animState;
|
|
154
325
|
this._animState = null;
|
|
326
|
+
this._pendingCollapse = false;
|
|
327
|
+
// Handle group drag (entire group repositioned)
|
|
328
|
+
if (animState.sourceTabGroupId) {
|
|
329
|
+
this._commitGroupMove(animState.sourceTabGroupId, animState.currentInsertionIndex);
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
155
332
|
const insertionIndex = animState.currentInsertionIndex;
|
|
156
333
|
const sourceIndex = animState.sourceIndex;
|
|
157
|
-
// After the source tab is removed, indices after it shift
|
|
158
|
-
// down by one, so adjust the target index accordingly.
|
|
159
334
|
const adjustedIndex = insertionIndex -
|
|
160
335
|
(sourceIndex !== -1 && sourceIndex < insertionIndex
|
|
161
336
|
? 1
|
|
162
337
|
: 0);
|
|
163
|
-
|
|
164
|
-
if (adjustedIndex === sourceIndex
|
|
338
|
+
const sourceCurrentGroup = this.group.model.getTabGroupForPanel(animState.sourceTabId);
|
|
339
|
+
if (adjustedIndex === sourceIndex &&
|
|
340
|
+
!animState.targetTabGroupId &&
|
|
341
|
+
!sourceCurrentGroup) {
|
|
342
|
+
this._uncollapsSourceTab(animState.sourceTabId);
|
|
165
343
|
this.resetTabTransforms();
|
|
166
344
|
return;
|
|
167
345
|
}
|
|
168
|
-
|
|
169
|
-
// before resetting transforms, so FLIP starts from what the
|
|
170
|
-
// user currently sees — not from a teleported state.
|
|
346
|
+
this._uncollapsSourceTab(animState.sourceTabId);
|
|
171
347
|
const firstPositions = this.snapshotTabPositions();
|
|
172
348
|
this.resetTabTransforms();
|
|
173
|
-
this._onDrop.fire({
|
|
349
|
+
this._onDrop.fire({
|
|
350
|
+
event,
|
|
351
|
+
index: adjustedIndex,
|
|
352
|
+
targetTabGroupId: animState.targetTabGroupId,
|
|
353
|
+
});
|
|
174
354
|
this.runFlipAnimation(firstPositions, animState.sourceTabId, animState.sourceIndex === -1, {
|
|
175
355
|
from: Math.min(sourceIndex, adjustedIndex),
|
|
176
356
|
to: Math.max(sourceIndex, adjustedIndex),
|
|
177
357
|
});
|
|
178
358
|
}, true), Disposable.from(() => {
|
|
359
|
+
var _a;
|
|
360
|
+
(_a = this._voidContainerListeners) === null || _a === void 0 ? void 0 : _a.dispose();
|
|
179
361
|
this.resetDragAnimation();
|
|
362
|
+
this._tabGroupManager.disposeAll();
|
|
180
363
|
for (const { value, disposable } of this._tabs) {
|
|
181
364
|
disposable.dispose();
|
|
182
365
|
value.dispose();
|
|
183
366
|
}
|
|
184
367
|
this._tabs = [];
|
|
368
|
+
this._tabMap.clear();
|
|
185
369
|
}));
|
|
186
370
|
}
|
|
187
371
|
indexOf(id) {
|
|
@@ -192,37 +376,113 @@ export class Tabs extends CompositeDisposable {
|
|
|
192
376
|
this._tabs[this.selectedIndex].value === tab);
|
|
193
377
|
}
|
|
194
378
|
setActivePanel(panel) {
|
|
195
|
-
|
|
379
|
+
const isVertical = this._direction === 'vertical';
|
|
380
|
+
let running = 0;
|
|
196
381
|
for (const tab of this._tabs) {
|
|
197
382
|
const isActivePanel = panel.id === tab.value.panel.id;
|
|
198
383
|
tab.value.setActive(isActivePanel);
|
|
199
384
|
if (isActivePanel) {
|
|
200
385
|
const element = tab.value.element;
|
|
201
386
|
const parentElement = element.parentElement;
|
|
202
|
-
if (
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
387
|
+
if (isVertical) {
|
|
388
|
+
if (running < parentElement.scrollTop ||
|
|
389
|
+
running + element.clientHeight >
|
|
390
|
+
parentElement.scrollTop + parentElement.clientHeight) {
|
|
391
|
+
parentElement.scrollTop = running;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
else {
|
|
395
|
+
if (running < parentElement.scrollLeft ||
|
|
396
|
+
running + element.clientWidth >
|
|
397
|
+
parentElement.scrollLeft + parentElement.clientWidth) {
|
|
398
|
+
parentElement.scrollLeft = running;
|
|
399
|
+
}
|
|
206
400
|
}
|
|
207
401
|
}
|
|
208
|
-
|
|
402
|
+
running += isVertical
|
|
403
|
+
? tab.value.element.clientHeight
|
|
404
|
+
: tab.value.element.clientWidth;
|
|
405
|
+
}
|
|
406
|
+
// Reposition underlines so the wrap-around follows the new active tab
|
|
407
|
+
if (this._tabGroupManager.groupUnderlines.size > 0) {
|
|
408
|
+
this._tabGroupManager.positionUnderlines();
|
|
209
409
|
}
|
|
210
410
|
}
|
|
211
411
|
openPanel(panel, index = this._tabs.length) {
|
|
212
|
-
if (this.
|
|
412
|
+
if (this._tabMap.has(panel.id)) {
|
|
213
413
|
return;
|
|
214
414
|
}
|
|
215
415
|
const tab = new Tab(panel, this.accessor, this.group);
|
|
216
416
|
tab.setContent(panel.view.tab);
|
|
417
|
+
if (this._direction !== 'horizontal') {
|
|
418
|
+
tab.setDirection(this._direction);
|
|
419
|
+
}
|
|
217
420
|
const disposable = new CompositeDisposable(tab.onDragStart((event) => {
|
|
421
|
+
var _a;
|
|
218
422
|
this._onTabDragStart.fire({ nativeEvent: event, panel });
|
|
219
|
-
if (this.accessor.options.tabAnimation === 'smooth') {
|
|
423
|
+
if (((_a = this.accessor.options.theme) === null || _a === void 0 ? void 0 : _a.tabAnimation) === 'smooth') {
|
|
424
|
+
const tabWidth = tab.element.getBoundingClientRect().width;
|
|
425
|
+
const sourceIndex = this._tabs.findIndex((x) => x.value === tab);
|
|
220
426
|
this._animState = {
|
|
221
427
|
sourceTabId: panel.id,
|
|
222
|
-
sourceIndex
|
|
428
|
+
sourceIndex,
|
|
223
429
|
tabPositions: this.snapshotTabPositions(),
|
|
430
|
+
chipPositions: this._tabGroupManager.snapshotChipWidths(),
|
|
224
431
|
currentInsertionIndex: null,
|
|
432
|
+
targetTabGroupId: null,
|
|
433
|
+
sourceTabGroupId: null,
|
|
434
|
+
sourceGroupPanelIds: null,
|
|
435
|
+
sourceChipWidth: 0,
|
|
436
|
+
cursorOffsetFromDragLeft: tabWidth / 2,
|
|
437
|
+
sourceGapWidth: tabWidth,
|
|
438
|
+
containerLeft: this._tabsList.getBoundingClientRect().left,
|
|
225
439
|
};
|
|
440
|
+
// Collapse the source tab after the browser captures the
|
|
441
|
+
// drag image, then open the gap at the source position in
|
|
442
|
+
// the same paint frame — no visual jump.
|
|
443
|
+
// Both collapse and gap must be instant (no transition).
|
|
444
|
+
this._pendingCollapse = true;
|
|
445
|
+
requestAnimationFrame(() => {
|
|
446
|
+
var _a;
|
|
447
|
+
var _b;
|
|
448
|
+
this._pendingCollapse = false;
|
|
449
|
+
if (!this._animState) {
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
// Collapse source tab instantly (no transition)
|
|
453
|
+
tab.element.style.transition = 'none';
|
|
454
|
+
toggleClass(tab.element, 'dv-tab--dragging', true);
|
|
455
|
+
void tab.element.offsetHeight; // force reflow
|
|
456
|
+
(_a = (_b = this._animState).currentInsertionIndex) !== null && _a !== void 0 ? _a : (_b.currentInsertionIndex = sourceIndex);
|
|
457
|
+
// Apply gap with transitions disabled on the target
|
|
458
|
+
this.applyDragOverTransforms(true);
|
|
459
|
+
// Re-enable transitions for subsequent moves
|
|
460
|
+
tab.element.style.removeProperty('transition');
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
}), tab.onTabClick((event) => {
|
|
464
|
+
if (event.defaultPrevented) {
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
if (this.group.api.location.type !== 'edge') {
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
if (this.group.activePanel === panel) {
|
|
471
|
+
// Clicking the active tab toggles expansion
|
|
472
|
+
if (this.group.api.isCollapsed()) {
|
|
473
|
+
this.group.api.expand();
|
|
474
|
+
}
|
|
475
|
+
else {
|
|
476
|
+
this.group.api.collapse();
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
else {
|
|
480
|
+
// Clicking a non-active tab switches the active tab.
|
|
481
|
+
// If the group is collapsed, also expand it.
|
|
482
|
+
this.group.model.openPanel(panel);
|
|
483
|
+
if (this.group.api.isCollapsed()) {
|
|
484
|
+
this.group.api.expand();
|
|
485
|
+
}
|
|
226
486
|
}
|
|
227
487
|
}), tab.onPointerDown((event) => {
|
|
228
488
|
if (event.defaultPrevented) {
|
|
@@ -246,34 +506,69 @@ export class Tabs extends CompositeDisposable {
|
|
|
246
506
|
return;
|
|
247
507
|
}
|
|
248
508
|
switch (event.button) {
|
|
249
|
-
case 0:
|
|
250
|
-
if (this.group.
|
|
251
|
-
|
|
509
|
+
case 0:
|
|
510
|
+
if (this.group.api.location.type === 'edge') {
|
|
511
|
+
// All tab interaction for edge groups is handled by
|
|
512
|
+
// onTabClick to avoid race conditions with active panel state
|
|
513
|
+
}
|
|
514
|
+
else {
|
|
515
|
+
if (this.group.activePanel !== panel) {
|
|
516
|
+
this.group.model.openPanel(panel);
|
|
517
|
+
}
|
|
252
518
|
}
|
|
253
519
|
break;
|
|
254
520
|
}
|
|
255
521
|
}), tab.onDrop((event) => {
|
|
522
|
+
var _a, _b, _c, _d;
|
|
256
523
|
const animState = this._animState;
|
|
257
524
|
this._animState = null;
|
|
258
|
-
|
|
525
|
+
this._pendingCollapse = false;
|
|
526
|
+
const tabIndex = this._tabs.findIndex((x) => x.value === tab);
|
|
259
527
|
if (animState) {
|
|
528
|
+
const dropIndex = event.position === 'right' ? tabIndex + 1 : tabIndex;
|
|
529
|
+
if (animState.sourceTabGroupId) {
|
|
530
|
+
this._commitGroupMove(animState.sourceTabGroupId, (_a = animState.currentInsertionIndex) !== null && _a !== void 0 ? _a : dropIndex);
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
this._uncollapsSourceTab(animState.sourceTabId);
|
|
260
534
|
const firstPositions = this.snapshotTabPositions();
|
|
261
535
|
this.resetTabTransforms();
|
|
262
536
|
this._onDrop.fire({
|
|
263
537
|
event: event.nativeEvent,
|
|
264
538
|
index: dropIndex,
|
|
539
|
+
targetTabGroupId: animState.targetTabGroupId,
|
|
265
540
|
});
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
541
|
+
if (((_b = this.accessor.options.theme) === null || _b === void 0 ? void 0 : _b.tabAnimation) === 'smooth') {
|
|
542
|
+
this.runFlipAnimation(firstPositions, animState.sourceTabId, animState.sourceIndex === -1, animState.sourceIndex !== -1
|
|
543
|
+
? {
|
|
544
|
+
from: Math.min(animState.sourceIndex, dropIndex),
|
|
545
|
+
to: Math.max(animState.sourceIndex, dropIndex),
|
|
546
|
+
}
|
|
547
|
+
: undefined);
|
|
548
|
+
}
|
|
272
549
|
}
|
|
273
550
|
else {
|
|
551
|
+
// Compute insertion index based on which half of the tab
|
|
552
|
+
// the pointer is over, then adjust for same-group removal:
|
|
553
|
+
// when the source tab sits before the insertion point,
|
|
554
|
+
// removing it shifts all subsequent indices down by one.
|
|
555
|
+
const afterPosition = this._direction === 'vertical' ? 'bottom' : 'right';
|
|
556
|
+
const insertionIndex = event.position === afterPosition
|
|
557
|
+
? tabIndex + 1
|
|
558
|
+
: tabIndex;
|
|
559
|
+
const data = getPanelData();
|
|
560
|
+
const sourceIndex = data
|
|
561
|
+
? this._tabs.findIndex((x) => x.value.panel.id === data.panelId)
|
|
562
|
+
: -1;
|
|
563
|
+
const adjustedIndex = insertionIndex -
|
|
564
|
+
(sourceIndex !== -1 && sourceIndex < insertionIndex
|
|
565
|
+
? 1
|
|
566
|
+
: 0);
|
|
567
|
+
const targetTabGroupId = (_d = (_c = this.group.model.getTabGroupForPanel(tab.panel.id)) === null || _c === void 0 ? void 0 : _c.id) !== null && _d !== void 0 ? _d : null;
|
|
274
568
|
this._onDrop.fire({
|
|
275
569
|
event: event.nativeEvent,
|
|
276
|
-
index:
|
|
570
|
+
index: adjustedIndex,
|
|
571
|
+
targetTabGroupId,
|
|
277
572
|
});
|
|
278
573
|
}
|
|
279
574
|
}), tab.onWillShowOverlay((event) => {
|
|
@@ -287,9 +582,14 @@ export class Tabs extends CompositeDisposable {
|
|
|
287
582
|
}));
|
|
288
583
|
const value = { value: tab, disposable };
|
|
289
584
|
this.addTab(value, index);
|
|
585
|
+
// A new tab may have been inserted between a chip and its
|
|
586
|
+
// group's first tab — reposition all chips to stay correct.
|
|
587
|
+
this._tabGroupManager.positionAllChips();
|
|
290
588
|
// If a tab was added during active drag, refresh positions
|
|
291
589
|
if (this._animState) {
|
|
292
590
|
this._animState.tabPositions = this.snapshotTabPositions();
|
|
591
|
+
this._animState.chipPositions =
|
|
592
|
+
this._tabGroupManager.snapshotChipWidths();
|
|
293
593
|
this.applyDragOverTransforms();
|
|
294
594
|
}
|
|
295
595
|
}
|
|
@@ -299,8 +599,11 @@ export class Tabs extends CompositeDisposable {
|
|
|
299
599
|
this.resetTabTransforms();
|
|
300
600
|
this._animState = null;
|
|
301
601
|
}
|
|
602
|
+
// Force-clean any pending transitionend listener
|
|
603
|
+
this._tabGroupManager.cleanupTransition(id);
|
|
302
604
|
const index = this.indexOf(id);
|
|
303
605
|
const tabToRemove = this._tabs.splice(index, 1)[0];
|
|
606
|
+
this._tabMap.delete(id);
|
|
304
607
|
const { value, disposable } = tabToRemove;
|
|
305
608
|
disposable.dispose();
|
|
306
609
|
value.dispose();
|
|
@@ -308,6 +611,8 @@ export class Tabs extends CompositeDisposable {
|
|
|
308
611
|
// If a non-source tab was removed during active drag, refresh positions
|
|
309
612
|
if (this._animState) {
|
|
310
613
|
this._animState.tabPositions = this.snapshotTabPositions();
|
|
614
|
+
this._animState.chipPositions =
|
|
615
|
+
this._tabGroupManager.snapshotChipWidths();
|
|
311
616
|
this.applyDragOverTransforms();
|
|
312
617
|
}
|
|
313
618
|
}
|
|
@@ -315,29 +620,217 @@ export class Tabs extends CompositeDisposable {
|
|
|
315
620
|
if (index < 0 || index > this._tabs.length) {
|
|
316
621
|
throw new Error('invalid location');
|
|
317
622
|
}
|
|
318
|
-
|
|
623
|
+
// Use the tab element at `index` as the reference node rather than
|
|
624
|
+
// `children[index]`, because `_tabsList` may contain non-tab children
|
|
625
|
+
// (e.g. group chips, underlines) that shift the DOM indices.
|
|
626
|
+
const refNode = index < this._tabs.length ? this._tabs[index].value.element : null;
|
|
627
|
+
this._tabsList.insertBefore(tab.value.element, refNode);
|
|
319
628
|
this._tabs = [
|
|
320
629
|
...this._tabs.slice(0, index),
|
|
321
630
|
tab,
|
|
322
631
|
...this._tabs.slice(index),
|
|
323
632
|
];
|
|
633
|
+
this._tabMap.set(tab.value.panel.id, tab);
|
|
324
634
|
if (this.selectedIndex < 0) {
|
|
325
635
|
this.selectedIndex = index;
|
|
326
636
|
}
|
|
327
637
|
}
|
|
328
638
|
toggleDropdown(options) {
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
639
|
+
if (options.reset) {
|
|
640
|
+
this._onOverflowTabsChange.fire({
|
|
641
|
+
tabs: [],
|
|
642
|
+
tabGroups: [],
|
|
643
|
+
reset: true,
|
|
644
|
+
});
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
const tabs = this._tabs
|
|
648
|
+
.filter((tab) => !isChildEntirelyVisibleWithinParent(tab.value.element, this._tabsList))
|
|
649
|
+
.map((x) => x.value.panel.id);
|
|
650
|
+
// Detect tab groups whose chip is clipped or whose tabs are all
|
|
651
|
+
// in the overflow set (e.g. collapsed groups scrolled out of view).
|
|
652
|
+
const overflowTabSet = new Set(tabs);
|
|
653
|
+
const tabGroups = [];
|
|
654
|
+
for (const tg of this.group.model.getTabGroups()) {
|
|
655
|
+
const chipEntry = this._tabGroupManager.chipRenderers.get(tg.id);
|
|
656
|
+
const chipClipped = chipEntry &&
|
|
657
|
+
!isChildEntirelyVisibleWithinParent(chipEntry.chip.element, this._tabsList);
|
|
658
|
+
// A group is in overflow if its chip is clipped OR all its
|
|
659
|
+
// visible tabs are in the overflow set.
|
|
660
|
+
const allTabsOverflow = tg.panelIds.length > 0 &&
|
|
661
|
+
tg.panelIds.every((pid) => overflowTabSet.has(pid));
|
|
662
|
+
if (chipClipped || allTabsOverflow) {
|
|
663
|
+
tabGroups.push(tg.id);
|
|
664
|
+
// For collapsed groups whose chip is clipped, ensure all
|
|
665
|
+
// member tabs are included in the overflow list so they
|
|
666
|
+
// appear in the dropdown.
|
|
667
|
+
if (tg.collapsed) {
|
|
668
|
+
for (const pid of tg.panelIds) {
|
|
669
|
+
if (!overflowTabSet.has(pid)) {
|
|
670
|
+
overflowTabSet.add(pid);
|
|
671
|
+
tabs.push(pid);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
this._onOverflowTabsChange.fire({ tabs, tabGroups, reset: false });
|
|
335
678
|
}
|
|
336
679
|
updateDragAndDropState() {
|
|
337
680
|
for (const tab of this._tabs) {
|
|
338
681
|
tab.value.updateDragAndDropState();
|
|
339
682
|
}
|
|
340
683
|
}
|
|
684
|
+
/**
|
|
685
|
+
* Synchronize chip elements and CSS classes for all tab groups
|
|
686
|
+
* in the parent group model. Call after any tab group mutation.
|
|
687
|
+
*/
|
|
688
|
+
updateTabGroups() {
|
|
689
|
+
this._tabGroupManager.update();
|
|
690
|
+
}
|
|
691
|
+
refreshTabGroupAccent() {
|
|
692
|
+
this._tabGroupManager.refreshAccents();
|
|
693
|
+
}
|
|
694
|
+
_handleChipDragStart(tabGroup, chip, event) {
|
|
695
|
+
var _a;
|
|
696
|
+
const firstPanelId = tabGroup.panelIds[0];
|
|
697
|
+
const firstIdx = firstPanelId
|
|
698
|
+
? this._tabs.findIndex((t) => t.value.panel.id === firstPanelId)
|
|
699
|
+
: -1;
|
|
700
|
+
const chipRect = chip.element.getBoundingClientRect();
|
|
701
|
+
// Compute total group width (chip + all tabs)
|
|
702
|
+
let groupGapWidth = chipRect.width;
|
|
703
|
+
for (const pid of tabGroup.panelIds) {
|
|
704
|
+
const tabEntry = this._tabMap.get(pid);
|
|
705
|
+
if (tabEntry) {
|
|
706
|
+
groupGapWidth +=
|
|
707
|
+
tabEntry.value.element.getBoundingClientRect().width;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
this._animState = {
|
|
711
|
+
sourceTabId: '',
|
|
712
|
+
sourceIndex: firstIdx,
|
|
713
|
+
tabPositions: this.snapshotTabPositions(),
|
|
714
|
+
chipPositions: this._tabGroupManager.snapshotChipWidths(),
|
|
715
|
+
currentInsertionIndex: null,
|
|
716
|
+
targetTabGroupId: null,
|
|
717
|
+
sourceTabGroupId: tabGroup.id,
|
|
718
|
+
sourceGroupPanelIds: new Set(tabGroup.panelIds),
|
|
719
|
+
sourceChipWidth: chipRect.width,
|
|
720
|
+
cursorOffsetFromDragLeft: event.clientX - chipRect.left,
|
|
721
|
+
sourceGapWidth: groupGapWidth,
|
|
722
|
+
containerLeft: this._tabsList.getBoundingClientRect().left,
|
|
723
|
+
};
|
|
724
|
+
// Set LocalSelectionTransfer so drop targets recognise this as
|
|
725
|
+
// an internal dockview drag. panelId is null (group-level),
|
|
726
|
+
// tabGroupId identifies which tab group is being dragged.
|
|
727
|
+
const panelTransfer = LocalSelectionTransfer.getInstance();
|
|
728
|
+
panelTransfer.setData([
|
|
729
|
+
new PanelTransfer(this.accessor.id, this.group.id, null, tabGroup.id),
|
|
730
|
+
], PanelTransfer.prototype);
|
|
731
|
+
const iframes = disableIframePointEvents();
|
|
732
|
+
this._chipDragCleanup = {
|
|
733
|
+
dispose: () => {
|
|
734
|
+
panelTransfer.clearData(PanelTransfer.prototype);
|
|
735
|
+
iframes.release();
|
|
736
|
+
},
|
|
737
|
+
};
|
|
738
|
+
if (event.dataTransfer) {
|
|
739
|
+
event.dataTransfer.effectAllowed = 'move';
|
|
740
|
+
if (event.dataTransfer.items.length === 0) {
|
|
741
|
+
event.dataTransfer.setData('text/plain', '');
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
if (((_a = this.accessor.options.theme) === null || _a === void 0 ? void 0 : _a.tabAnimation) === 'smooth') {
|
|
745
|
+
// Collapse group tabs + chip after the browser
|
|
746
|
+
// captures the drag image, then open the gap at the
|
|
747
|
+
// source position — all instant (no transitions).
|
|
748
|
+
const groupPanelIds = new Set(tabGroup.panelIds);
|
|
749
|
+
this._pendingCollapse = true;
|
|
750
|
+
requestAnimationFrame(() => {
|
|
751
|
+
var _a;
|
|
752
|
+
var _b;
|
|
753
|
+
this._pendingCollapse = false;
|
|
754
|
+
if (!this._animState) {
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
// Collapse all group tabs instantly
|
|
758
|
+
for (const t of this._tabs) {
|
|
759
|
+
if (groupPanelIds.has(t.value.panel.id)) {
|
|
760
|
+
t.value.element.style.transition = 'none';
|
|
761
|
+
toggleClass(t.value.element, 'dv-tab--dragging', true);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
// Collapse the group chip instantly
|
|
765
|
+
const chipEntry = this._tabGroupManager.chipRenderers.get(tabGroup.id);
|
|
766
|
+
if (chipEntry) {
|
|
767
|
+
chipEntry.chip.element.style.transition = 'none';
|
|
768
|
+
toggleClass(chipEntry.chip.element, 'dv-tab-group-chip--dragging', true);
|
|
769
|
+
}
|
|
770
|
+
// Single reflow for the entire batch
|
|
771
|
+
void this._tabsList.offsetHeight;
|
|
772
|
+
const underline = this._tabGroupManager.groupUnderlines.get(tabGroup.id);
|
|
773
|
+
if (underline) {
|
|
774
|
+
underline.style.display = 'none';
|
|
775
|
+
}
|
|
776
|
+
(_a = (_b = this._animState).currentInsertionIndex) !== null && _a !== void 0 ? _a : (_b.currentInsertionIndex = firstIdx);
|
|
777
|
+
// Apply gap with transitions disabled
|
|
778
|
+
this.applyDragOverTransforms(true);
|
|
779
|
+
// Re-enable transitions for subsequent moves
|
|
780
|
+
for (const t of this._tabs) {
|
|
781
|
+
if (groupPanelIds.has(t.value.panel.id)) {
|
|
782
|
+
t.value.element.style.removeProperty('transition');
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
if (chipEntry) {
|
|
786
|
+
chipEntry.chip.element.style.removeProperty('transition');
|
|
787
|
+
}
|
|
788
|
+
});
|
|
789
|
+
}
|
|
790
|
+
// Build a composite drag image showing chip + group tabs
|
|
791
|
+
this._tabGroupManager.setGroupDragImage(event, tabGroup, chip.element);
|
|
792
|
+
}
|
|
793
|
+
/**
|
|
794
|
+
* Sets the broader container that is part of the same logical drop surface
|
|
795
|
+
* as this tab list (e.g. the full header element). When a dragleave from
|
|
796
|
+
* the tabs list lands inside this container, `_animState` is preserved so
|
|
797
|
+
* that external dragover listeners can continue the animation.
|
|
798
|
+
*/
|
|
799
|
+
setExtendedDropZone(el) {
|
|
800
|
+
this._extendedDropZone = el;
|
|
801
|
+
}
|
|
802
|
+
/**
|
|
803
|
+
* Allows external elements (e.g. void container, left actions) to push an
|
|
804
|
+
* insertion index into the animation while the cursor is outside the tabs
|
|
805
|
+
* list itself. Pass `null` to clear the indicator.
|
|
806
|
+
*/
|
|
807
|
+
setExternalInsertionIndex(index) {
|
|
808
|
+
if (!this._animState) {
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
if (index === this._animState.currentInsertionIndex) {
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
this._animState.currentInsertionIndex = index;
|
|
815
|
+
this.applyDragOverTransforms();
|
|
816
|
+
}
|
|
817
|
+
/**
|
|
818
|
+
* Called when the drag cursor leaves the entire header area (not just the
|
|
819
|
+
* tabs list). Clears animation state for cross-group drags, which never
|
|
820
|
+
* receive a `dragend` event on this tab list.
|
|
821
|
+
*/
|
|
822
|
+
clearExternalAnimState() {
|
|
823
|
+
if (!this._animState) {
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
this.resetTabTransforms();
|
|
827
|
+
if (this._animState.sourceIndex === -1) {
|
|
828
|
+
this._animState = null;
|
|
829
|
+
}
|
|
830
|
+
else {
|
|
831
|
+
this._animState.currentInsertionIndex = null;
|
|
832
|
+
}
|
|
833
|
+
}
|
|
341
834
|
snapshotTabPositions() {
|
|
342
835
|
const positions = new Map();
|
|
343
836
|
for (const tab of this._tabs) {
|
|
@@ -349,103 +842,505 @@ export class Tabs extends CompositeDisposable {
|
|
|
349
842
|
if (this._tabs.length === 0) {
|
|
350
843
|
return 0;
|
|
351
844
|
}
|
|
352
|
-
|
|
845
|
+
const isVertical = this._direction === 'vertical';
|
|
846
|
+
let total = 0;
|
|
353
847
|
for (const tab of this._tabs) {
|
|
354
|
-
|
|
848
|
+
const rect = tab.value.element.getBoundingClientRect();
|
|
849
|
+
total += isVertical ? rect.height : rect.width;
|
|
355
850
|
}
|
|
356
|
-
return
|
|
851
|
+
return total / this._tabs.length;
|
|
357
852
|
}
|
|
358
853
|
handleDragOver(event) {
|
|
854
|
+
var _a, _b, _c, _d, _e;
|
|
359
855
|
if (!this._animState) {
|
|
360
856
|
return;
|
|
361
857
|
}
|
|
362
858
|
const mouseX = event.clientX;
|
|
363
859
|
let insertionIndex = null;
|
|
860
|
+
let targetTabGroupId = null;
|
|
861
|
+
const sourceGroupPanelIds = this._animState.sourceGroupPanelIds;
|
|
862
|
+
// Accumulation approach: compute where the drag image's left edge
|
|
863
|
+
// would be, then walk tabs left-to-right using their original widths.
|
|
864
|
+
// A tab fits to the left of the gap if the cumulative width of all
|
|
865
|
+
// preceding non-source tabs <= available space.
|
|
866
|
+
const dragLeftEdge = mouseX - this._animState.cursorOffsetFromDragLeft;
|
|
867
|
+
const availableSpace = dragLeftEdge - this._animState.containerLeft;
|
|
868
|
+
let accWidth = 0;
|
|
869
|
+
// Build lookup: first panel ID of each non-source group → group ID
|
|
870
|
+
// so we can add chip widths when we encounter a group's first tab.
|
|
871
|
+
const firstPanelToGroup = new Map();
|
|
872
|
+
if (this._tabGroupManager.chipRenderers.size > 0) {
|
|
873
|
+
const tabGroups = this.group.model.getTabGroups();
|
|
874
|
+
for (const tg of tabGroups) {
|
|
875
|
+
if (tg.id === this._animState.sourceTabGroupId) {
|
|
876
|
+
continue;
|
|
877
|
+
}
|
|
878
|
+
if (tg.panelIds.length > 0) {
|
|
879
|
+
firstPanelToGroup.set(tg.panelIds[0], tg.id);
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
}
|
|
364
883
|
for (let i = 0; i < this._tabs.length; i++) {
|
|
365
884
|
const tab = this._tabs[i].value;
|
|
366
885
|
if (tab.panel.id === this._animState.sourceTabId) {
|
|
367
886
|
continue;
|
|
368
887
|
}
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
888
|
+
if (sourceGroupPanelIds === null || sourceGroupPanelIds === void 0 ? void 0 : sourceGroupPanelIds.has(tab.panel.id)) {
|
|
889
|
+
continue;
|
|
890
|
+
}
|
|
891
|
+
// If this tab is the first of a non-source group, include
|
|
892
|
+
// the chip width (which sits before it in the DOM).
|
|
893
|
+
const groupId = firstPanelToGroup.get(tab.panel.id);
|
|
894
|
+
if (groupId) {
|
|
895
|
+
const chipWidth = (_a = this._animState.chipPositions.get(groupId)) !== null && _a !== void 0 ? _a : 0;
|
|
896
|
+
if (accWidth + chipWidth > availableSpace) {
|
|
897
|
+
// Chip alone overflows — gap goes before this group
|
|
898
|
+
insertionIndex !== null && insertionIndex !== void 0 ? insertionIndex : (insertionIndex = i);
|
|
899
|
+
break;
|
|
900
|
+
}
|
|
901
|
+
accWidth += chipWidth;
|
|
902
|
+
}
|
|
903
|
+
// Use original width (before collapse/transforms)
|
|
904
|
+
const origRect = this._animState.tabPositions.get(tab.panel.id);
|
|
905
|
+
const tabWidth = origRect
|
|
906
|
+
? origRect.width
|
|
907
|
+
: tab.element.getBoundingClientRect().width;
|
|
908
|
+
// Shift at the midpoint: a tab moves left once the drag image
|
|
909
|
+
// covers half of it (like Chrome's tab drag behavior).
|
|
910
|
+
if (accWidth + tabWidth / 2 <= availableSpace) {
|
|
911
|
+
accWidth += tabWidth;
|
|
912
|
+
insertionIndex = i + 1;
|
|
913
|
+
}
|
|
914
|
+
else {
|
|
915
|
+
insertionIndex !== null && insertionIndex !== void 0 ? insertionIndex : (insertionIndex = i);
|
|
373
916
|
break;
|
|
374
917
|
}
|
|
375
|
-
insertionIndex = i + 1;
|
|
376
918
|
}
|
|
377
|
-
|
|
919
|
+
// Determine which tab group (if any) the insertion index falls within.
|
|
920
|
+
//
|
|
921
|
+
// We use snapshot-based positions (accWidth from the accumulation loop
|
|
922
|
+
// above) to compute original chip boundaries. This avoids reading
|
|
923
|
+
// getBoundingClientRect() on chips whose live position is shifted by
|
|
924
|
+
// the drag gap margin, which caused oscillation / visual jumps.
|
|
925
|
+
if (insertionIndex !== null &&
|
|
926
|
+
this._tabGroupManager.chipRenderers.size > 0) {
|
|
927
|
+
const isGroupDrag = !!this._animState.sourceTabGroupId;
|
|
928
|
+
const tabGroups = this.group.model.getTabGroups();
|
|
929
|
+
// Rebuild the accumulated width up to insertionIndex so we know
|
|
930
|
+
// the original right edge of the chip (if any) that precedes it.
|
|
931
|
+
// We walk exactly the same way as the accumulation loop above.
|
|
932
|
+
let accUpTo = 0;
|
|
933
|
+
for (let i = 0; i < this._tabs.length; i++) {
|
|
934
|
+
const tab = this._tabs[i].value;
|
|
935
|
+
if (tab.panel.id === this._animState.sourceTabId) {
|
|
936
|
+
continue;
|
|
937
|
+
}
|
|
938
|
+
if (sourceGroupPanelIds === null || sourceGroupPanelIds === void 0 ? void 0 : sourceGroupPanelIds.has(tab.panel.id)) {
|
|
939
|
+
continue;
|
|
940
|
+
}
|
|
941
|
+
if (i >= insertionIndex) {
|
|
942
|
+
break;
|
|
943
|
+
}
|
|
944
|
+
const gid = firstPanelToGroup.get(tab.panel.id);
|
|
945
|
+
if (gid) {
|
|
946
|
+
accUpTo += (_b = this._animState.chipPositions.get(gid)) !== null && _b !== void 0 ? _b : 0;
|
|
947
|
+
}
|
|
948
|
+
const origRect = this._animState.tabPositions.get(tab.panel.id);
|
|
949
|
+
accUpTo += origRect
|
|
950
|
+
? origRect.width
|
|
951
|
+
: tab.element.getBoundingClientRect().width;
|
|
952
|
+
}
|
|
953
|
+
for (const tg of tabGroups) {
|
|
954
|
+
// Build effective panel list: exclude the source tab
|
|
955
|
+
// so that dragging a tab out of its own group doesn't
|
|
956
|
+
// inflate the group's index range.
|
|
957
|
+
const effectivePanelIds = tg.panelIds.filter((pid) => pid !== this._animState.sourceTabId &&
|
|
958
|
+
!(sourceGroupPanelIds === null || sourceGroupPanelIds === void 0 ? void 0 : sourceGroupPanelIds.has(pid)));
|
|
959
|
+
if (effectivePanelIds.length === 0) {
|
|
960
|
+
continue;
|
|
961
|
+
}
|
|
962
|
+
const firstIdx = this._tabs.findIndex((t) => t.value.panel.id === effectivePanelIds[0]);
|
|
963
|
+
const lastIdx = this._tabs.findIndex((t) => t.value.panel.id ===
|
|
964
|
+
effectivePanelIds[effectivePanelIds.length - 1]);
|
|
965
|
+
if (firstIdx === -1 || lastIdx === -1) {
|
|
966
|
+
continue;
|
|
967
|
+
}
|
|
968
|
+
const isInsideRange = insertionIndex >= firstIdx && insertionIndex <= lastIdx;
|
|
969
|
+
const isJustBeforeGroup = !isInsideRange && insertionIndex === firstIdx - 1;
|
|
970
|
+
if (!isInsideRange && !isJustBeforeGroup) {
|
|
971
|
+
continue;
|
|
972
|
+
}
|
|
973
|
+
if (isGroupDrag) {
|
|
974
|
+
// A group cannot be dropped inside another group.
|
|
975
|
+
// Snap the insertion index to just before or just
|
|
976
|
+
// after this group based on cursor position relative
|
|
977
|
+
// to the group's midpoint.
|
|
978
|
+
const groupMid = (firstIdx + lastIdx + 1) / 2;
|
|
979
|
+
if (insertionIndex < groupMid) {
|
|
980
|
+
insertionIndex = firstIdx;
|
|
981
|
+
}
|
|
982
|
+
else {
|
|
983
|
+
insertionIndex = lastIdx + 1;
|
|
984
|
+
}
|
|
985
|
+
// targetTabGroupId stays null
|
|
986
|
+
break;
|
|
987
|
+
}
|
|
988
|
+
if (isJustBeforeGroup) {
|
|
989
|
+
// Check whether only the source tab (or source group
|
|
990
|
+
// tabs) sits between insertionIndex and firstIdx.
|
|
991
|
+
// If so, the source is being dragged away from that
|
|
992
|
+
// slot, so we ARE effectively "just before" the group
|
|
993
|
+
// and should still allow dropping into position 0.
|
|
994
|
+
let allInBetweenAreSource = true;
|
|
995
|
+
for (let j = insertionIndex; j < firstIdx; j++) {
|
|
996
|
+
const pid = this._tabs[j].value.panel.id;
|
|
997
|
+
if (pid !== this._animState.sourceTabId &&
|
|
998
|
+
!(sourceGroupPanelIds === null || sourceGroupPanelIds === void 0 ? void 0 : sourceGroupPanelIds.has(pid))) {
|
|
999
|
+
allInBetweenAreSource = false;
|
|
1000
|
+
break;
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
if (!allInBetweenAreSource) {
|
|
1004
|
+
continue;
|
|
1005
|
+
}
|
|
1006
|
+
const chipWidth = (_c = this._animState.chipPositions.get(tg.id)) !== null && _c !== void 0 ? _c : 0;
|
|
1007
|
+
const threshold = tg.collapsed
|
|
1008
|
+
? this._animState.containerLeft +
|
|
1009
|
+
accUpTo +
|
|
1010
|
+
chipWidth / 2
|
|
1011
|
+
: this._animState.containerLeft + accUpTo + chipWidth;
|
|
1012
|
+
if (mouseX >= threshold) {
|
|
1013
|
+
insertionIndex = firstIdx;
|
|
1014
|
+
targetTabGroupId = tg.id;
|
|
1015
|
+
}
|
|
1016
|
+
break;
|
|
1017
|
+
}
|
|
1018
|
+
if (isInsideRange) {
|
|
1019
|
+
const chipWidth = (_d = this._animState.chipPositions.get(tg.id)) !== null && _d !== void 0 ? _d : 0;
|
|
1020
|
+
const chipOriginalRight = this._animState.containerLeft + accUpTo + chipWidth;
|
|
1021
|
+
if (insertionIndex === firstIdx) {
|
|
1022
|
+
if (mouseX >= chipOriginalRight) {
|
|
1023
|
+
targetTabGroupId = tg.id;
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
else {
|
|
1027
|
+
targetTabGroupId = tg.id;
|
|
1028
|
+
}
|
|
1029
|
+
break;
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
if (insertionIndex === this._animState.currentInsertionIndex &&
|
|
1034
|
+
targetTabGroupId === this._animState.targetTabGroupId) {
|
|
378
1035
|
return;
|
|
379
1036
|
}
|
|
380
1037
|
this._animState.currentInsertionIndex = insertionIndex;
|
|
381
|
-
this.
|
|
1038
|
+
this._animState.targetTabGroupId = targetTabGroupId;
|
|
1039
|
+
if (((_e = this.accessor.options.theme) === null || _e === void 0 ? void 0 : _e.tabAnimation) === 'smooth') {
|
|
1040
|
+
this.applyDragOverTransforms();
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
/**
|
|
1044
|
+
* Batch-remove a CSS class from multiple elements instantly,
|
|
1045
|
+
* forcing only a single reflow for the entire batch.
|
|
1046
|
+
*/
|
|
1047
|
+
_removeClassInstantlyBatch(elements, cls) {
|
|
1048
|
+
const affected = [];
|
|
1049
|
+
for (const el of elements) {
|
|
1050
|
+
if (el.classList.contains(cls)) {
|
|
1051
|
+
el.style.transition = 'none';
|
|
1052
|
+
toggleClass(el, cls, false);
|
|
1053
|
+
affected.push(el);
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
if (affected.length > 0) {
|
|
1057
|
+
void affected[0].offsetHeight; // single reflow for entire batch
|
|
1058
|
+
for (const el of affected) {
|
|
1059
|
+
el.style.removeProperty('transition');
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
/**
|
|
1064
|
+
* Remove `dv-tab--dragging` from the source tab instantly so it
|
|
1065
|
+
* regains its real width before FLIP snapshots.
|
|
1066
|
+
*/
|
|
1067
|
+
_uncollapsSourceTab(sourceTabId) {
|
|
1068
|
+
const entry = this._tabMap.get(sourceTabId);
|
|
1069
|
+
if (entry) {
|
|
1070
|
+
this._removeClassInstantlyBatch([entry.value.element], 'dv-tab--dragging');
|
|
1071
|
+
}
|
|
382
1072
|
}
|
|
383
|
-
applyDragOverTransforms() {
|
|
1073
|
+
applyDragOverTransforms(skipTransition = false) {
|
|
384
1074
|
if (!this._animState ||
|
|
385
1075
|
this._animState.currentInsertionIndex === null) {
|
|
386
1076
|
this.resetTabTransforms();
|
|
387
1077
|
return;
|
|
388
1078
|
}
|
|
1079
|
+
// Don't apply transforms until the source tab has been collapsed
|
|
1080
|
+
// in the rAF callback — otherwise the gap + visible source = jump.
|
|
1081
|
+
if (this._pendingCollapse) {
|
|
1082
|
+
return;
|
|
1083
|
+
}
|
|
389
1084
|
const insertionIndex = this._animState.currentInsertionIndex;
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
1085
|
+
// For group drags, gap = sum of all group member widths
|
|
1086
|
+
let gapWidth;
|
|
1087
|
+
const sourceGroupPanelIds = this._animState.sourceGroupPanelIds;
|
|
1088
|
+
if (this._animState.sourceTabGroupId && sourceGroupPanelIds) {
|
|
1089
|
+
gapWidth = this._animState.sourceGapWidth;
|
|
1090
|
+
}
|
|
1091
|
+
else {
|
|
1092
|
+
const sourceRect = this._animState.tabPositions.get(this._animState.sourceTabId);
|
|
1093
|
+
gapWidth = sourceRect
|
|
1094
|
+
? sourceRect.width
|
|
1095
|
+
: this.getAverageTabWidth();
|
|
1096
|
+
}
|
|
1097
|
+
// When the insertion lands at or before a group's first tab, shift
|
|
1098
|
+
// the chip so the gap appears before the entire group.
|
|
1099
|
+
//
|
|
1100
|
+
// Two cases:
|
|
1101
|
+
// 1. targetTabGroupId is null (standalone drop) — always shift chip.
|
|
1102
|
+
// 2. targetTabGroupId is set AND the group is collapsed — shift chip
|
|
1103
|
+
// because the collapsed tabs are invisible, so putting the gap on
|
|
1104
|
+
// them has no visual effect.
|
|
1105
|
+
let chipToShift = null;
|
|
1106
|
+
if (this._tabGroupManager.chipRenderers.size > 0) {
|
|
1107
|
+
const tabGroups = this.group.model.getTabGroups();
|
|
1108
|
+
for (const tg of tabGroups) {
|
|
1109
|
+
if (tg.id === this._animState.sourceTabGroupId)
|
|
1110
|
+
continue;
|
|
1111
|
+
// Skip the group that the dragged tab belongs to — the
|
|
1112
|
+
// gap should appear after the chip (where the tab was),
|
|
1113
|
+
// not before it.
|
|
1114
|
+
if (tg.panelIds.includes(this._animState.sourceTabId))
|
|
1115
|
+
continue;
|
|
1116
|
+
const effectivePids = tg.panelIds.filter((pid) => pid !== this._animState.sourceTabId &&
|
|
1117
|
+
!(sourceGroupPanelIds === null || sourceGroupPanelIds === void 0 ? void 0 : sourceGroupPanelIds.has(pid)));
|
|
1118
|
+
if (effectivePids.length === 0)
|
|
1119
|
+
continue;
|
|
1120
|
+
const firstIdx = this._tabs.findIndex((t) => t.value.panel.id === effectivePids[0]);
|
|
1121
|
+
// Only consider chip-shifting when dropping outside the
|
|
1122
|
+
// group, or when dropping inside a collapsed group (whose
|
|
1123
|
+
// tabs are invisible).
|
|
1124
|
+
const shouldShiftChip = !this._animState.targetTabGroupId ||
|
|
1125
|
+
(this._animState.targetTabGroupId === tg.id &&
|
|
1126
|
+
tg.collapsed);
|
|
1127
|
+
if (!shouldShiftChip)
|
|
1128
|
+
continue;
|
|
1129
|
+
if (firstIdx >= insertionIndex) {
|
|
1130
|
+
let hasTabs = false;
|
|
1131
|
+
for (let j = insertionIndex; j < firstIdx; j++) {
|
|
1132
|
+
const pid = this._tabs[j].value.panel.id;
|
|
1133
|
+
if (pid === this._animState.sourceTabId)
|
|
1134
|
+
continue;
|
|
1135
|
+
if (sourceGroupPanelIds === null || sourceGroupPanelIds === void 0 ? void 0 : sourceGroupPanelIds.has(pid))
|
|
1136
|
+
continue;
|
|
1137
|
+
hasTabs = true;
|
|
1138
|
+
break;
|
|
1139
|
+
}
|
|
1140
|
+
if (!hasTabs) {
|
|
1141
|
+
const chipEntry = this._tabGroupManager.chipRenderers.get(tg.id);
|
|
1142
|
+
if (chipEntry) {
|
|
1143
|
+
chipToShift = chipEntry.chip.element;
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
break;
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
// Helper: pick the correct shifting class for tabs vs chips.
|
|
1151
|
+
const shiftingClass = (el) => el.classList.contains('dv-tab-group-chip')
|
|
1152
|
+
? 'dv-tab-group-chip--shifting'
|
|
1153
|
+
: 'dv-tab--shifting';
|
|
1154
|
+
// Helper: apply a margin-left value to an element, optionally
|
|
1155
|
+
// bypassing CSS transitions for instant positioning.
|
|
1156
|
+
const setMargin = (el, value) => {
|
|
1157
|
+
if (skipTransition) {
|
|
1158
|
+
el.style.transition = 'none';
|
|
1159
|
+
el.style.marginLeft = value;
|
|
1160
|
+
void el.offsetHeight;
|
|
1161
|
+
el.style.removeProperty('transition');
|
|
1162
|
+
}
|
|
1163
|
+
else {
|
|
1164
|
+
el.style.marginLeft = value;
|
|
1165
|
+
}
|
|
1166
|
+
toggleClass(el, shiftingClass(el), true);
|
|
1167
|
+
};
|
|
1168
|
+
const clearMargin = (el) => {
|
|
1169
|
+
const cls = shiftingClass(el);
|
|
1170
|
+
// Remove any previous pending listener for this element
|
|
1171
|
+
const prev = this._pendingMarginCleanups.get(el);
|
|
1172
|
+
if (prev) {
|
|
1173
|
+
prev();
|
|
1174
|
+
}
|
|
1175
|
+
if (skipTransition || !el.style.marginLeft) {
|
|
1176
|
+
el.style.removeProperty('margin-left');
|
|
1177
|
+
toggleClass(el, cls, false);
|
|
1178
|
+
}
|
|
1179
|
+
else {
|
|
1180
|
+
el.style.marginLeft = '0px';
|
|
1181
|
+
toggleClass(el, cls, true);
|
|
1182
|
+
const onEnd = () => {
|
|
1183
|
+
el.style.removeProperty('margin-left');
|
|
1184
|
+
toggleClass(el, cls, false);
|
|
1185
|
+
el.removeEventListener('transitionend', onEnd);
|
|
1186
|
+
clearTimeout(fallbackTimer);
|
|
1187
|
+
this._pendingMarginCleanups.delete(el);
|
|
1188
|
+
};
|
|
1189
|
+
// Fallback in case transitionend never fires
|
|
1190
|
+
// (e.g. element removed from DOM mid-transition)
|
|
1191
|
+
const fallbackTimer = setTimeout(onEnd, 300);
|
|
1192
|
+
this._pendingMarginCleanups.set(el, onEnd);
|
|
1193
|
+
el.addEventListener('transitionend', onEnd);
|
|
1194
|
+
}
|
|
1195
|
+
};
|
|
395
1196
|
let gapApplied = false;
|
|
1197
|
+
// Reset all non-source chip margins first
|
|
1198
|
+
for (const [groupId, entry] of this._tabGroupManager.chipRenderers) {
|
|
1199
|
+
if (groupId === this._animState.sourceTabGroupId)
|
|
1200
|
+
continue;
|
|
1201
|
+
clearMargin(entry.chip.element);
|
|
1202
|
+
}
|
|
1203
|
+
// Apply gap to chip if insertion is before a group
|
|
1204
|
+
if (chipToShift) {
|
|
1205
|
+
setMargin(chipToShift, `${gapWidth}px`);
|
|
1206
|
+
gapApplied = true;
|
|
1207
|
+
}
|
|
396
1208
|
for (let i = 0; i < this._tabs.length; i++) {
|
|
397
1209
|
const tab = this._tabs[i].value;
|
|
398
1210
|
if (tab.panel.id === this._animState.sourceTabId) {
|
|
399
1211
|
continue;
|
|
400
1212
|
}
|
|
1213
|
+
if (sourceGroupPanelIds === null || sourceGroupPanelIds === void 0 ? void 0 : sourceGroupPanelIds.has(tab.panel.id)) {
|
|
1214
|
+
continue;
|
|
1215
|
+
}
|
|
401
1216
|
if (!gapApplied && i >= insertionIndex) {
|
|
402
|
-
tab.element
|
|
403
|
-
toggleClass(tab.element, 'dv-tab--shifting', true);
|
|
1217
|
+
setMargin(tab.element, `${gapWidth}px`);
|
|
404
1218
|
gapApplied = true;
|
|
405
1219
|
}
|
|
406
1220
|
else {
|
|
407
|
-
|
|
408
|
-
// then remove both once the transition ends
|
|
409
|
-
if (tab.element.style.marginLeft) {
|
|
410
|
-
tab.element.style.marginLeft = '0px';
|
|
411
|
-
toggleClass(tab.element, 'dv-tab--shifting', true);
|
|
412
|
-
const onEnd = () => {
|
|
413
|
-
tab.element.style.removeProperty('margin-left');
|
|
414
|
-
toggleClass(tab.element, 'dv-tab--shifting', false);
|
|
415
|
-
tab.element.removeEventListener('transitionend', onEnd);
|
|
416
|
-
};
|
|
417
|
-
tab.element.addEventListener('transitionend', onEnd);
|
|
418
|
-
}
|
|
419
|
-
else {
|
|
420
|
-
toggleClass(tab.element, 'dv-tab--shifting', false);
|
|
421
|
-
}
|
|
1221
|
+
clearMargin(tab.element);
|
|
422
1222
|
}
|
|
423
1223
|
}
|
|
1224
|
+
// Reposition underlines to follow shifted chips/tabs
|
|
1225
|
+
this._tabGroupManager.trackUnderlines();
|
|
424
1226
|
}
|
|
425
1227
|
resetTabTransforms() {
|
|
1228
|
+
// Cancel any pending margin transitionend listeners
|
|
1229
|
+
for (const [, cleanup] of this._pendingMarginCleanups) {
|
|
1230
|
+
cleanup();
|
|
1231
|
+
}
|
|
1232
|
+
this._pendingMarginCleanups.clear();
|
|
426
1233
|
for (const tab of this._tabs) {
|
|
427
1234
|
tab.value.element.style.removeProperty('margin-left');
|
|
1235
|
+
tab.value.element.style.removeProperty('margin-right');
|
|
1236
|
+
tab.value.element.style.removeProperty('margin-top');
|
|
1237
|
+
tab.value.element.style.removeProperty('margin-bottom');
|
|
428
1238
|
tab.value.element.style.removeProperty('transform');
|
|
429
1239
|
toggleClass(tab.value.element, 'dv-tab--shifting', false);
|
|
430
1240
|
}
|
|
1241
|
+
for (const [, entry] of this._tabGroupManager.chipRenderers) {
|
|
1242
|
+
entry.chip.element.style.removeProperty('margin-left');
|
|
1243
|
+
toggleClass(entry.chip.element, 'dv-tab-group-chip--shifting', false);
|
|
1244
|
+
}
|
|
1245
|
+
this._tabGroupManager.positionUnderlines();
|
|
1246
|
+
}
|
|
1247
|
+
/**
|
|
1248
|
+
* Commit a group-drag drop: clear drag classes, move the group
|
|
1249
|
+
* in the model, and run a FLIP animation.
|
|
1250
|
+
*/
|
|
1251
|
+
_commitGroupMove(sourceTabGroupId, insertionIndex) {
|
|
1252
|
+
var _a, _b, _c;
|
|
1253
|
+
// Read transfer data BEFORE disposing cleanup — disposing
|
|
1254
|
+
// _chipDragCleanup clears the global LocalSelectionTransfer
|
|
1255
|
+
// singleton which getPanelData() reads from.
|
|
1256
|
+
const data = getPanelData();
|
|
1257
|
+
(_a = this._chipDragCleanup) === null || _a === void 0 ? void 0 : _a.dispose();
|
|
1258
|
+
this._chipDragCleanup = null;
|
|
1259
|
+
// Check if the tab group exists in this group (within-group reorder)
|
|
1260
|
+
// or in another group (cross-group move).
|
|
1261
|
+
const isLocal = this.group.model
|
|
1262
|
+
.getTabGroups()
|
|
1263
|
+
.some((tg) => tg.id === sourceTabGroupId);
|
|
1264
|
+
if (isLocal) {
|
|
1265
|
+
if (((_b = this.accessor.options.theme) === null || _b === void 0 ? void 0 : _b.tabAnimation) === 'smooth') {
|
|
1266
|
+
this._clearGroupDragClasses(sourceTabGroupId);
|
|
1267
|
+
const firstPositions = this.snapshotTabPositions();
|
|
1268
|
+
this.resetTabTransforms();
|
|
1269
|
+
this.group.model.moveTabGroup(sourceTabGroupId, insertionIndex);
|
|
1270
|
+
this.runFlipAnimation(firstPositions, '', false);
|
|
1271
|
+
}
|
|
1272
|
+
else {
|
|
1273
|
+
this._tabGroupManager.skipNextCollapseAnimation = true;
|
|
1274
|
+
this.group.model.moveTabGroup(sourceTabGroupId, insertionIndex);
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
else if (data) {
|
|
1278
|
+
// Cross-group: delegate to the component-level move which
|
|
1279
|
+
// handles panel transfer and tab group recreation.
|
|
1280
|
+
// Use the REAL tab group ID from transfer data, not the
|
|
1281
|
+
// potentially stale one from _animState.
|
|
1282
|
+
this.accessor.moveGroupOrPanel({
|
|
1283
|
+
from: {
|
|
1284
|
+
groupId: data.groupId,
|
|
1285
|
+
tabGroupId: (_c = data.tabGroupId) !== null && _c !== void 0 ? _c : sourceTabGroupId,
|
|
1286
|
+
},
|
|
1287
|
+
to: {
|
|
1288
|
+
group: this.group,
|
|
1289
|
+
position: 'center',
|
|
1290
|
+
index: insertionIndex,
|
|
1291
|
+
},
|
|
1292
|
+
});
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
_clearGroupDragClasses(sourceTabGroupId) {
|
|
1296
|
+
const chipEntry = this._tabGroupManager.chipRenderers.get(sourceTabGroupId);
|
|
1297
|
+
if (chipEntry) {
|
|
1298
|
+
this._removeClassInstantlyBatch([chipEntry.chip.element], 'dv-tab-group-chip--dragging');
|
|
1299
|
+
}
|
|
1300
|
+
this._removeClassInstantlyBatch(this._tabs.map((t) => t.value.element), 'dv-tab--dragging');
|
|
1301
|
+
// Restore underline
|
|
1302
|
+
const underline = this._tabGroupManager.groupUnderlines.get(sourceTabGroupId);
|
|
1303
|
+
if (underline) {
|
|
1304
|
+
underline.style.removeProperty('display');
|
|
1305
|
+
}
|
|
1306
|
+
// The subsequent moveTabGroup will re-create tabs and call
|
|
1307
|
+
// updateTabGroups → _updateTabGroupClasses. For collapsed groups
|
|
1308
|
+
// the new tabs don't have dv-tab--group-collapsed yet, which
|
|
1309
|
+
// would trigger the collapse animation. Skip it.
|
|
1310
|
+
this._tabGroupManager.skipNextCollapseAnimation = true;
|
|
431
1311
|
}
|
|
432
1312
|
resetDragAnimation() {
|
|
1313
|
+
var _a, _b;
|
|
1314
|
+
this._pendingCollapse = false;
|
|
433
1315
|
this.resetTabTransforms();
|
|
1316
|
+
// Clear drag-collapse classes instantly (no transition)
|
|
1317
|
+
if ((_a = this._animState) === null || _a === void 0 ? void 0 : _a.sourceTabGroupId) {
|
|
1318
|
+
this._clearGroupDragClasses(this._animState.sourceTabGroupId);
|
|
1319
|
+
}
|
|
1320
|
+
else {
|
|
1321
|
+
this._removeClassInstantlyBatch(this._tabs.map((t) => t.value.element), 'dv-tab--dragging');
|
|
1322
|
+
}
|
|
434
1323
|
this._animState = null;
|
|
435
|
-
|
|
436
|
-
|
|
1324
|
+
(_b = this._chipDragCleanup) === null || _b === void 0 ? void 0 : _b.dispose();
|
|
1325
|
+
this._chipDragCleanup = null;
|
|
1326
|
+
// Restore any hidden underlines from group drags
|
|
1327
|
+
for (const [, el] of this._tabGroupManager.groupUnderlines) {
|
|
1328
|
+
el.style.removeProperty('display');
|
|
437
1329
|
}
|
|
438
1330
|
}
|
|
439
1331
|
runFlipAnimation(firstPositions, sourceTabId, isCrossGroup = false, animRange) {
|
|
1332
|
+
const isVertical = this._direction === 'vertical';
|
|
440
1333
|
let hasAnimation = false;
|
|
441
1334
|
for (let i = 0; i < this._tabs.length; i++) {
|
|
442
1335
|
const tab = this._tabs[i];
|
|
443
1336
|
const panelId = tab.value.panel.id;
|
|
444
1337
|
if (panelId === sourceTabId) {
|
|
445
1338
|
if (isCrossGroup) {
|
|
446
|
-
// Newly inserted tab: slide in from the
|
|
1339
|
+
// Newly inserted tab: slide in from the end
|
|
447
1340
|
const rect = tab.value.element.getBoundingClientRect();
|
|
448
|
-
tab.value.element.style.transform =
|
|
1341
|
+
tab.value.element.style.transform = isVertical
|
|
1342
|
+
? `translateY(${rect.height}px)`
|
|
1343
|
+
: `translateX(${rect.width}px)`;
|
|
449
1344
|
toggleClass(tab.value.element, 'dv-tab--shifting', true);
|
|
450
1345
|
hasAnimation = true;
|
|
451
1346
|
}
|
|
@@ -461,11 +1356,15 @@ export class Tabs extends CompositeDisposable {
|
|
|
461
1356
|
continue;
|
|
462
1357
|
}
|
|
463
1358
|
const lastRect = tab.value.element.getBoundingClientRect();
|
|
464
|
-
const
|
|
465
|
-
|
|
1359
|
+
const delta = isVertical
|
|
1360
|
+
? firstRect.top - lastRect.top
|
|
1361
|
+
: firstRect.left - lastRect.left;
|
|
1362
|
+
if (Math.abs(delta) < 1) {
|
|
466
1363
|
continue;
|
|
467
1364
|
}
|
|
468
|
-
tab.value.element.style.transform =
|
|
1365
|
+
tab.value.element.style.transform = isVertical
|
|
1366
|
+
? `translateY(${delta}px)`
|
|
1367
|
+
: `translateX(${delta}px)`;
|
|
469
1368
|
toggleClass(tab.value.element, 'dv-tab--shifting', true);
|
|
470
1369
|
hasAnimation = true;
|
|
471
1370
|
}
|
|
@@ -473,19 +1372,32 @@ export class Tabs extends CompositeDisposable {
|
|
|
473
1372
|
return;
|
|
474
1373
|
}
|
|
475
1374
|
requestAnimationFrame(() => {
|
|
1375
|
+
var _a;
|
|
476
1376
|
for (const tab of this._tabs) {
|
|
477
1377
|
if (tab.value.element.style.transform) {
|
|
478
1378
|
tab.value.element.style.transform = '';
|
|
479
1379
|
}
|
|
480
1380
|
}
|
|
1381
|
+
// Track underlines during the FLIP transition so they
|
|
1382
|
+
// follow tabs as they slide to their final positions.
|
|
1383
|
+
this._tabGroupManager.trackUnderlines();
|
|
1384
|
+
// Clean up any previous flip transition listener
|
|
1385
|
+
(_a = this._flipTransitionCleanup) === null || _a === void 0 ? void 0 : _a.call(this);
|
|
481
1386
|
const onTransitionEnd = (event) => {
|
|
482
1387
|
if (event.propertyName === 'transform') {
|
|
483
|
-
|
|
1388
|
+
cleanup();
|
|
484
1389
|
for (const tab of this._tabs) {
|
|
485
1390
|
toggleClass(tab.value.element, 'dv-tab--shifting', false);
|
|
486
1391
|
}
|
|
1392
|
+
// Final reposition after animation settles
|
|
1393
|
+
this._tabGroupManager.positionUnderlines();
|
|
487
1394
|
}
|
|
488
1395
|
};
|
|
1396
|
+
const cleanup = () => {
|
|
1397
|
+
this._tabsList.removeEventListener('transitionend', onTransitionEnd);
|
|
1398
|
+
this._flipTransitionCleanup = null;
|
|
1399
|
+
};
|
|
1400
|
+
this._flipTransitionCleanup = cleanup;
|
|
489
1401
|
this._tabsList.addEventListener('transitionend', onTransitionEnd);
|
|
490
1402
|
});
|
|
491
1403
|
}
|