dockview-core 5.1.0 → 5.2.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/dist/cjs/dockview/components/tab/tab.d.ts +2 -0
- package/dist/cjs/dockview/components/tab/tab.js +17 -1
- package/dist/cjs/dockview/components/titlebar/tabs.d.ts +8 -0
- package/dist/cjs/dockview/components/titlebar/tabs.js +352 -5
- package/dist/cjs/dockview/options.d.ts +13 -0
- package/dist/cjs/dockview/options.js +3 -1
- package/dist/dockview-core.js +302 -8
- 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 +301 -7
- package/dist/esm/dockview/components/tab/tab.d.ts +2 -0
- package/dist/esm/dockview/components/tab/tab.js +17 -1
- package/dist/esm/dockview/components/titlebar/tabs.d.ts +8 -0
- package/dist/esm/dockview/components/titlebar/tabs.js +281 -6
- package/dist/esm/dockview/options.d.ts +13 -0
- package/dist/esm/dockview/options.js +2 -0
- package/dist/package/main.cjs.js +302 -8
- package/dist/package/main.cjs.min.js +2 -2
- package/dist/package/main.esm.min.mjs +2 -2
- package/dist/package/main.esm.mjs +302 -9
- package/dist/styles/dockview.css +25 -1
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getPanelData } from '../../../dnd/dataTransfer';
|
|
2
|
-
import { addClasses, isChildEntirelyVisibleWithinParent, OverflowObserver, removeClasses, } from '../../../dom';
|
|
2
|
+
import { addClasses, 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';
|
|
@@ -66,6 +66,7 @@ export class Tabs extends CompositeDisposable {
|
|
|
66
66
|
this.selectedIndex = -1;
|
|
67
67
|
this._showTabsOverflowControl = false;
|
|
68
68
|
this._direction = 'horizontal';
|
|
69
|
+
this._animState = null;
|
|
69
70
|
this._onTabDragStart = new Emitter();
|
|
70
71
|
this.onTabDragStart = this._onTabDragStart.event;
|
|
71
72
|
this._onDrop = new Emitter();
|
|
@@ -94,7 +95,88 @@ export class Tabs extends CompositeDisposable {
|
|
|
94
95
|
if (isLeftClick) {
|
|
95
96
|
this.accessor.doSetGroupActive(this.group);
|
|
96
97
|
}
|
|
97
|
-
}),
|
|
98
|
+
}), addDisposableListener(this._tabsList, 'dragover', (event) => {
|
|
99
|
+
if (!this._animState) {
|
|
100
|
+
// Check for external drag from another group
|
|
101
|
+
if (this.accessor.options.tabAnimation !== 'smooth' ||
|
|
102
|
+
this.accessor.options.disableDnd) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const data = getPanelData();
|
|
106
|
+
if (data &&
|
|
107
|
+
data.panelId &&
|
|
108
|
+
data.groupId !== this.group.id) {
|
|
109
|
+
this._animState = {
|
|
110
|
+
sourceTabId: data.panelId,
|
|
111
|
+
sourceIndex: -1,
|
|
112
|
+
tabPositions: this.snapshotTabPositions(),
|
|
113
|
+
currentInsertionIndex: null,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
this.handleDragOver(event);
|
|
121
|
+
}, true), addDisposableListener(this._tabsList, 'dragleave', (event) => {
|
|
122
|
+
if (!this._animState) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
// Only handle if leaving the container itself, not moving between children
|
|
126
|
+
if (event.relatedTarget &&
|
|
127
|
+
this._tabsList.contains(event.relatedTarget)) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
this.resetTabTransforms();
|
|
131
|
+
if (this._animState) {
|
|
132
|
+
if (this._animState.sourceIndex === -1) {
|
|
133
|
+
// External drag left — clear state entirely
|
|
134
|
+
// (no dragend will fire on this tab list)
|
|
135
|
+
this._animState = null;
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
this._animState.currentInsertionIndex = null;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}, 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
|
+
this.resetDragAnimation();
|
|
145
|
+
}), addDisposableListener(this._tabsList, 'drop', (event) => {
|
|
146
|
+
if (this.accessor.options.tabAnimation !== 'smooth' ||
|
|
147
|
+
!this._animState ||
|
|
148
|
+
this._animState.currentInsertionIndex === null) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
event.stopPropagation();
|
|
152
|
+
event.preventDefault();
|
|
153
|
+
const animState = this._animState;
|
|
154
|
+
this._animState = null;
|
|
155
|
+
const insertionIndex = animState.currentInsertionIndex;
|
|
156
|
+
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
|
+
const adjustedIndex = insertionIndex -
|
|
160
|
+
(sourceIndex !== -1 && sourceIndex < insertionIndex
|
|
161
|
+
? 1
|
|
162
|
+
: 0);
|
|
163
|
+
// No-op: drop at the same position, nothing to animate
|
|
164
|
+
if (adjustedIndex === sourceIndex) {
|
|
165
|
+
this.resetTabTransforms();
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
// Snapshot current visual positions (with margins still applied)
|
|
169
|
+
// before resetting transforms, so FLIP starts from what the
|
|
170
|
+
// user currently sees — not from a teleported state.
|
|
171
|
+
const firstPositions = this.snapshotTabPositions();
|
|
172
|
+
this.resetTabTransforms();
|
|
173
|
+
this._onDrop.fire({ event, index: adjustedIndex });
|
|
174
|
+
this.runFlipAnimation(firstPositions, animState.sourceTabId, animState.sourceIndex === -1, {
|
|
175
|
+
from: Math.min(sourceIndex, adjustedIndex),
|
|
176
|
+
to: Math.max(sourceIndex, adjustedIndex),
|
|
177
|
+
});
|
|
178
|
+
}, true), Disposable.from(() => {
|
|
179
|
+
this.resetDragAnimation();
|
|
98
180
|
for (const { value, disposable } of this._tabs) {
|
|
99
181
|
disposable.dispose();
|
|
100
182
|
value.dispose();
|
|
@@ -134,6 +216,14 @@ export class Tabs extends CompositeDisposable {
|
|
|
134
216
|
tab.setContent(panel.view.tab);
|
|
135
217
|
const disposable = new CompositeDisposable(tab.onDragStart((event) => {
|
|
136
218
|
this._onTabDragStart.fire({ nativeEvent: event, panel });
|
|
219
|
+
if (this.accessor.options.tabAnimation === 'smooth') {
|
|
220
|
+
this._animState = {
|
|
221
|
+
sourceTabId: panel.id,
|
|
222
|
+
sourceIndex: this._tabs.findIndex((x) => x.value === tab),
|
|
223
|
+
tabPositions: this.snapshotTabPositions(),
|
|
224
|
+
currentInsertionIndex: null,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
137
227
|
}), tab.onPointerDown((event) => {
|
|
138
228
|
if (event.defaultPrevented) {
|
|
139
229
|
return;
|
|
@@ -163,10 +253,29 @@ export class Tabs extends CompositeDisposable {
|
|
|
163
253
|
break;
|
|
164
254
|
}
|
|
165
255
|
}), tab.onDrop((event) => {
|
|
166
|
-
this.
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
256
|
+
const animState = this._animState;
|
|
257
|
+
this._animState = null;
|
|
258
|
+
const dropIndex = this._tabs.findIndex((x) => x.value === tab);
|
|
259
|
+
if (animState) {
|
|
260
|
+
const firstPositions = this.snapshotTabPositions();
|
|
261
|
+
this.resetTabTransforms();
|
|
262
|
+
this._onDrop.fire({
|
|
263
|
+
event: event.nativeEvent,
|
|
264
|
+
index: dropIndex,
|
|
265
|
+
});
|
|
266
|
+
this.runFlipAnimation(firstPositions, animState.sourceTabId, animState.sourceIndex === -1, animState.sourceIndex !== -1
|
|
267
|
+
? {
|
|
268
|
+
from: Math.min(animState.sourceIndex, dropIndex),
|
|
269
|
+
to: Math.max(animState.sourceIndex, dropIndex),
|
|
270
|
+
}
|
|
271
|
+
: undefined);
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
this._onDrop.fire({
|
|
275
|
+
event: event.nativeEvent,
|
|
276
|
+
index: dropIndex,
|
|
277
|
+
});
|
|
278
|
+
}
|
|
170
279
|
}), tab.onWillShowOverlay((event) => {
|
|
171
280
|
this._onWillShowOverlay.fire(new DockviewWillShowOverlayLocationEvent(event, {
|
|
172
281
|
kind: 'tab',
|
|
@@ -178,14 +287,29 @@ export class Tabs extends CompositeDisposable {
|
|
|
178
287
|
}));
|
|
179
288
|
const value = { value: tab, disposable };
|
|
180
289
|
this.addTab(value, index);
|
|
290
|
+
// If a tab was added during active drag, refresh positions
|
|
291
|
+
if (this._animState) {
|
|
292
|
+
this._animState.tabPositions = this.snapshotTabPositions();
|
|
293
|
+
this.applyDragOverTransforms();
|
|
294
|
+
}
|
|
181
295
|
}
|
|
182
296
|
delete(id) {
|
|
297
|
+
var _a;
|
|
298
|
+
if (((_a = this._animState) === null || _a === void 0 ? void 0 : _a.sourceTabId) === id) {
|
|
299
|
+
this.resetTabTransforms();
|
|
300
|
+
this._animState = null;
|
|
301
|
+
}
|
|
183
302
|
const index = this.indexOf(id);
|
|
184
303
|
const tabToRemove = this._tabs.splice(index, 1)[0];
|
|
185
304
|
const { value, disposable } = tabToRemove;
|
|
186
305
|
disposable.dispose();
|
|
187
306
|
value.dispose();
|
|
188
307
|
value.element.remove();
|
|
308
|
+
// If a non-source tab was removed during active drag, refresh positions
|
|
309
|
+
if (this._animState) {
|
|
310
|
+
this._animState.tabPositions = this.snapshotTabPositions();
|
|
311
|
+
this.applyDragOverTransforms();
|
|
312
|
+
}
|
|
189
313
|
}
|
|
190
314
|
addTab(tab, index = this._tabs.length) {
|
|
191
315
|
if (index < 0 || index > this._tabs.length) {
|
|
@@ -214,4 +338,155 @@ export class Tabs extends CompositeDisposable {
|
|
|
214
338
|
tab.value.updateDragAndDropState();
|
|
215
339
|
}
|
|
216
340
|
}
|
|
341
|
+
snapshotTabPositions() {
|
|
342
|
+
const positions = new Map();
|
|
343
|
+
for (const tab of this._tabs) {
|
|
344
|
+
positions.set(tab.value.panel.id, tab.value.element.getBoundingClientRect());
|
|
345
|
+
}
|
|
346
|
+
return positions;
|
|
347
|
+
}
|
|
348
|
+
getAverageTabWidth() {
|
|
349
|
+
if (this._tabs.length === 0) {
|
|
350
|
+
return 0;
|
|
351
|
+
}
|
|
352
|
+
let totalWidth = 0;
|
|
353
|
+
for (const tab of this._tabs) {
|
|
354
|
+
totalWidth += tab.value.element.getBoundingClientRect().width;
|
|
355
|
+
}
|
|
356
|
+
return totalWidth / this._tabs.length;
|
|
357
|
+
}
|
|
358
|
+
handleDragOver(event) {
|
|
359
|
+
if (!this._animState) {
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
const mouseX = event.clientX;
|
|
363
|
+
let insertionIndex = null;
|
|
364
|
+
for (let i = 0; i < this._tabs.length; i++) {
|
|
365
|
+
const tab = this._tabs[i].value;
|
|
366
|
+
if (tab.panel.id === this._animState.sourceTabId) {
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
const rect = tab.element.getBoundingClientRect();
|
|
370
|
+
const midpoint = rect.left + rect.width / 2;
|
|
371
|
+
if (mouseX < midpoint) {
|
|
372
|
+
insertionIndex = i;
|
|
373
|
+
break;
|
|
374
|
+
}
|
|
375
|
+
insertionIndex = i + 1;
|
|
376
|
+
}
|
|
377
|
+
if (insertionIndex === this._animState.currentInsertionIndex) {
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
this._animState.currentInsertionIndex = insertionIndex;
|
|
381
|
+
this.applyDragOverTransforms();
|
|
382
|
+
}
|
|
383
|
+
applyDragOverTransforms() {
|
|
384
|
+
if (!this._animState ||
|
|
385
|
+
this._animState.currentInsertionIndex === null) {
|
|
386
|
+
this.resetTabTransforms();
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
const insertionIndex = this._animState.currentInsertionIndex;
|
|
390
|
+
const sourceRect = this._animState.tabPositions.get(this._animState.sourceTabId);
|
|
391
|
+
const gapWidth = sourceRect
|
|
392
|
+
? sourceRect.width
|
|
393
|
+
: this.getAverageTabWidth();
|
|
394
|
+
// Find the first non-source tab at insertionIndex to receive the gap margin
|
|
395
|
+
let gapApplied = false;
|
|
396
|
+
for (let i = 0; i < this._tabs.length; i++) {
|
|
397
|
+
const tab = this._tabs[i].value;
|
|
398
|
+
if (tab.panel.id === this._animState.sourceTabId) {
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
if (!gapApplied && i >= insertionIndex) {
|
|
402
|
+
tab.element.style.marginLeft = `${gapWidth}px`;
|
|
403
|
+
toggleClass(tab.element, 'dv-tab--shifting', true);
|
|
404
|
+
gapApplied = true;
|
|
405
|
+
}
|
|
406
|
+
else {
|
|
407
|
+
// Keep shifting class while margin animates back to 0,
|
|
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
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
resetTabTransforms() {
|
|
426
|
+
for (const tab of this._tabs) {
|
|
427
|
+
tab.value.element.style.removeProperty('margin-left');
|
|
428
|
+
tab.value.element.style.removeProperty('transform');
|
|
429
|
+
toggleClass(tab.value.element, 'dv-tab--shifting', false);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
resetDragAnimation() {
|
|
433
|
+
this.resetTabTransforms();
|
|
434
|
+
this._animState = null;
|
|
435
|
+
for (const tab of this._tabs) {
|
|
436
|
+
toggleClass(tab.value.element, 'dv-tab--dragging', false);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
runFlipAnimation(firstPositions, sourceTabId, isCrossGroup = false, animRange) {
|
|
440
|
+
let hasAnimation = false;
|
|
441
|
+
for (let i = 0; i < this._tabs.length; i++) {
|
|
442
|
+
const tab = this._tabs[i];
|
|
443
|
+
const panelId = tab.value.panel.id;
|
|
444
|
+
if (panelId === sourceTabId) {
|
|
445
|
+
if (isCrossGroup) {
|
|
446
|
+
// Newly inserted tab: slide in from the right
|
|
447
|
+
const rect = tab.value.element.getBoundingClientRect();
|
|
448
|
+
tab.value.element.style.transform = `translateX(${rect.width}px)`;
|
|
449
|
+
toggleClass(tab.value.element, 'dv-tab--shifting', true);
|
|
450
|
+
hasAnimation = true;
|
|
451
|
+
}
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
// Skip tabs outside the affected range (they don't logically move)
|
|
455
|
+
if (animRange !== undefined &&
|
|
456
|
+
(i < animRange.from || i > animRange.to)) {
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
const firstRect = firstPositions.get(panelId);
|
|
460
|
+
if (!firstRect) {
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
463
|
+
const lastRect = tab.value.element.getBoundingClientRect();
|
|
464
|
+
const deltaX = firstRect.left - lastRect.left;
|
|
465
|
+
if (Math.abs(deltaX) < 1) {
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
tab.value.element.style.transform = `translateX(${deltaX}px)`;
|
|
469
|
+
toggleClass(tab.value.element, 'dv-tab--shifting', true);
|
|
470
|
+
hasAnimation = true;
|
|
471
|
+
}
|
|
472
|
+
if (!hasAnimation) {
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
requestAnimationFrame(() => {
|
|
476
|
+
for (const tab of this._tabs) {
|
|
477
|
+
if (tab.value.element.style.transform) {
|
|
478
|
+
tab.value.element.style.transform = '';
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
const onTransitionEnd = (event) => {
|
|
482
|
+
if (event.propertyName === 'transform') {
|
|
483
|
+
this._tabsList.removeEventListener('transitionend', onTransitionEnd);
|
|
484
|
+
for (const tab of this._tabs) {
|
|
485
|
+
toggleClass(tab.value.element, 'dv-tab--shifting', false);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
};
|
|
489
|
+
this._tabsList.addEventListener('transitionend', onTransitionEnd);
|
|
490
|
+
});
|
|
491
|
+
}
|
|
217
492
|
}
|
|
@@ -69,7 +69,20 @@ export interface DockviewOptions {
|
|
|
69
69
|
* This is only applied to the tab header section. Defaults to `custom`.
|
|
70
70
|
*/
|
|
71
71
|
scrollbars?: 'native' | 'custom';
|
|
72
|
+
/**
|
|
73
|
+
* Controls tab drag-and-drop reorder animation style.
|
|
74
|
+
*
|
|
75
|
+
* - `"smooth"`: tabs animate smoothly during drag-and-drop reorder —
|
|
76
|
+
* tabs slide apart to reveal the insertion gap, then animate to their
|
|
77
|
+
* final positions on drop (Chrome-like behavior).
|
|
78
|
+
* - `"default"`: standard tab reorder behavior without animation.
|
|
79
|
+
*
|
|
80
|
+
* Defaults to `"default"`.
|
|
81
|
+
*/
|
|
82
|
+
tabAnimation?: TabAnimation;
|
|
72
83
|
}
|
|
84
|
+
export type TabAnimation = 'smooth' | 'default';
|
|
85
|
+
export declare const DEFAULT_TAB_ANIMATION: TabAnimation;
|
|
73
86
|
export interface DockviewDndOverlayEvent extends IAcceptableEvent {
|
|
74
87
|
nativeEvent: DragEvent;
|
|
75
88
|
target: DockviewGroupDropLocation;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { AcceptableEvent } from '../events';
|
|
2
|
+
export const DEFAULT_TAB_ANIMATION = 'default';
|
|
2
3
|
export class DockviewUnhandledDragOverEvent extends AcceptableEvent {
|
|
3
4
|
constructor(nativeEvent, target, position, getData, group) {
|
|
4
5
|
super();
|
|
@@ -33,6 +34,7 @@ export const PROPERTY_KEYS_DOCKVIEW = (() => {
|
|
|
33
34
|
theme: undefined,
|
|
34
35
|
disableTabsOverflowList: undefined,
|
|
35
36
|
scrollbars: undefined,
|
|
37
|
+
tabAnimation: undefined,
|
|
36
38
|
};
|
|
37
39
|
return Object.keys(properties);
|
|
38
40
|
})();
|