dockview-core 6.0.5 → 6.0.7

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.
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * dockview-core
3
- * @version 6.0.5
3
+ * @version 6.0.7
4
4
  * @link https://github.com/mathuo/dockview
5
5
  * @license MIT
6
6
  */
@@ -7205,10 +7205,12 @@
7205
7205
  const index = this.indexOf(id);
7206
7206
  const tabToRemove = this._tabs.splice(index, 1)[0];
7207
7207
  this._tabMap.delete(id);
7208
- const { value, disposable } = tabToRemove;
7209
- disposable.dispose();
7210
- value.dispose();
7211
- value.element.remove();
7208
+ if (tabToRemove) {
7209
+ const { value, disposable } = tabToRemove;
7210
+ disposable.dispose();
7211
+ value.dispose();
7212
+ value.element.remove();
7213
+ }
7212
7214
  // If a non-source tab was removed during active drag, refresh positions
7213
7215
  if (this._animState) {
7214
7216
  this._animState.tabPositions = this.snapshotTabPositions();
@@ -7330,8 +7332,23 @@
7330
7332
  new PanelTransfer(this.accessor.id, this.group.id, null, tabGroup.id),
7331
7333
  ], PanelTransfer.prototype);
7332
7334
  const iframes = disableIframePointEvents();
7335
+ // The dragend listener on `_tabsList` is unreachable for chip
7336
+ // drags because cross-group drops detach the chip from the DOM
7337
+ // before dragend fires (the source tab group becomes empty, so
7338
+ // `_positionChipForGroup` removes the chip element). Without
7339
+ // bubbling, the tabsList listener never runs and `_animState`,
7340
+ // `_chipDragCleanup`, and the dragging CSS classes leak. Listen
7341
+ // directly on the chip element so cleanup happens regardless of
7342
+ // whether it's still attached. (Issue #1254.)
7343
+ const chipElement = chip.element;
7344
+ const onChipDragEnd = () => {
7345
+ chipElement.removeEventListener('dragend', onChipDragEnd);
7346
+ this.resetDragAnimation();
7347
+ };
7348
+ chipElement.addEventListener('dragend', onChipDragEnd);
7333
7349
  this._chipDragCleanup = {
7334
7350
  dispose: () => {
7351
+ chipElement.removeEventListener('dragend', onChipDragEnd);
7335
7352
  panelTransfer.clearData(PanelTransfer.prototype);
7336
7353
  iframes.release();
7337
7354
  },
@@ -7880,6 +7897,14 @@
7880
7897
  // handles panel transfer and tab group recreation.
7881
7898
  // Use the REAL tab group ID from transfer data, not the
7882
7899
  // potentially stale one from _animState.
7900
+ //
7901
+ // Clear any inline gap margin / shifting class applied to
7902
+ // destination tabs during dragover. Cross-group moves don't
7903
+ // run the FLIP path, and `moveGroupOrPanel` only inserts new
7904
+ // panels — it doesn't recreate existing destination tabs, so
7905
+ // their inline `margin-left` would otherwise persist as a
7906
+ // visible gap (issue #1243).
7907
+ this.resetTabTransforms();
7883
7908
  this.accessor.moveGroupOrPanel({
7884
7909
  from: {
7885
7910
  groupId: data.groupId,
@@ -9660,15 +9685,17 @@
9660
9685
  if (position === 'center') {
9661
9686
  return;
9662
9687
  }
9663
- if (data.panelId === null) {
9664
- // don't allow group move to drop anywhere on self
9688
+ if (data.panelId === null && !data.tabGroupId) {
9689
+ // Full-group drops on self are a no-op.
9690
+ // Tab-group drags are partial moves: an edge drop
9691
+ // splits the layout and creates a new group.
9665
9692
  return;
9666
9693
  }
9667
9694
  }
9668
9695
  }
9669
9696
  if (type === 'header') {
9670
9697
  if (data.groupId === this.id) {
9671
- if (data.panelId === null) {
9698
+ if (data.panelId === null && !data.tabGroupId) {
9672
9699
  return;
9673
9700
  }
9674
9701
  }
@@ -13536,7 +13563,7 @@
13536
13563
  // Collapse when the group becomes empty
13537
13564
  const autoCollapseDisposable = group.model.onDidRemovePanel(() => {
13538
13565
  if (group.model.isEmpty) {
13539
- this._shellManager.setEdgeGroupCollapsed(position, true);
13566
+ this.setEdgeGroupCollapsed(group, true);
13540
13567
  }
13541
13568
  });
13542
13569
  this._edgeGroupDisposables.set(position, autoCollapseDisposable);
@@ -13580,6 +13607,13 @@
13580
13607
  setEdgeGroupCollapsed(group, collapsed) {
13581
13608
  for (const [position, edgeGroup] of this._edgeGroups) {
13582
13609
  if (edgeGroup === group) {
13610
+ if (this._shellManager.isEdgeGroupCollapsed(position) ===
13611
+ collapsed) {
13612
+ // Skip the splitview resize on a no-op: with non-zero
13613
+ // theme gap, redundant resizeView calls accumulate
13614
+ // rounding drift that gradually shrinks the group.
13615
+ return;
13616
+ }
13583
13617
  this._shellManager.setEdgeGroupCollapsed(position, collapsed);
13584
13618
  edgeGroup.api._onDidCollapsedChange.fire({
13585
13619
  isCollapsed: collapsed,
@@ -14532,7 +14566,14 @@
14532
14566
  const label = tabGroup.label;
14533
14567
  const color = tabGroup.color;
14534
14568
  const collapsed = tabGroup.collapsed;
14569
+ const componentParams = tabGroup.componentParams;
14535
14570
  const panelIds = [...tabGroup.panelIds];
14571
+ // Capture the destination's grid location BEFORE potentially
14572
+ // removing the source group, in case source === destination and
14573
+ // the source becomes empty after panel removal.
14574
+ const referenceLocation = destinationTarget && destinationTarget !== 'center'
14575
+ ? getGridLocation(destinationGroup.element)
14576
+ : undefined;
14536
14577
  // Remove panels from the source group
14537
14578
  const removedPanels = this.movingLock(() => panelIds
14538
14579
  .map((pid) => sourceGroup.model.removePanel(pid, {
@@ -14543,11 +14584,6 @@
14543
14584
  if (removedPanels.length === 0) {
14544
14585
  return;
14545
14586
  }
14546
- if (!options.keepEmptyGroups &&
14547
- sourceGroup.model.size === 0 &&
14548
- sourceGroup !== destinationGroup) {
14549
- this.doRemoveGroup(sourceGroup, { skipActive: true });
14550
- }
14551
14587
  const addPanelsToGroup = (targetGroup) => {
14552
14588
  this.movingLock(() => {
14553
14589
  for (const panel of removedPanels) {
@@ -14563,6 +14599,7 @@
14563
14599
  label,
14564
14600
  color,
14565
14601
  collapsed,
14602
+ componentParams,
14566
14603
  });
14567
14604
  for (const panel of removedPanels) {
14568
14605
  targetGroup.model.addPanelToTabGroup(newTabGroup.id, panel.id);
@@ -14577,15 +14614,27 @@
14577
14614
  });
14578
14615
  }
14579
14616
  };
14580
- if (!destinationTarget || destinationTarget === 'center') {
14581
- addPanelsToGroup(destinationGroup);
14617
+ let targetGroup;
14618
+ if (!destinationTarget ||
14619
+ destinationTarget === 'center' ||
14620
+ !referenceLocation) {
14621
+ targetGroup = destinationGroup;
14582
14622
  }
14583
14623
  else {
14584
- const referenceLocation = getGridLocation(destinationGroup.element);
14585
14624
  const dropLocation = getRelativeLocation(this.gridview.orientation, referenceLocation, destinationTarget);
14586
- const newGroup = this.createGroupAtLocation(dropLocation);
14587
- addPanelsToGroup(newGroup);
14625
+ targetGroup = this.createGroupAtLocation(dropLocation);
14588
14626
  }
14627
+ // Remove the source group if it became empty. We compare against
14628
+ // the actual targetGroup (which is a freshly-created group for
14629
+ // edge drops) rather than the originally-passed destinationGroup,
14630
+ // so a tab-group drag onto its own group's edge still cleans up
14631
+ // the now-empty source.
14632
+ if (!options.keepEmptyGroups &&
14633
+ sourceGroup.model.size === 0 &&
14634
+ sourceGroup !== targetGroup) {
14635
+ this.doRemoveGroup(sourceGroup, { skipActive: true });
14636
+ }
14637
+ addPanelsToGroup(targetGroup);
14589
14638
  }
14590
14639
  moveGroup(options) {
14591
14640
  const from = options.from.group;
@@ -14597,6 +14646,16 @@
14597
14646
  let source = from;
14598
14647
  if (target === 'center') {
14599
14648
  const activePanel = from.activePanel;
14649
+ // Snapshot tab group metadata before removing panels so we
14650
+ // can recreate the tab groups in the destination after the
14651
+ // panels are merged in.
14652
+ const tabGroupSnapshots = from.model.getTabGroups().map((tg) => ({
14653
+ label: tg.label,
14654
+ color: tg.color,
14655
+ collapsed: tg.collapsed,
14656
+ componentParams: tg.componentParams,
14657
+ panelIds: [...tg.panelIds],
14658
+ }));
14600
14659
  const panels = this.movingLock(() => [...from.panels].map((p) => from.model.removePanel(p.id, {
14601
14660
  skipSetActive: true,
14602
14661
  })));
@@ -14611,6 +14670,17 @@
14611
14670
  });
14612
14671
  }
14613
14672
  });
14673
+ for (const snapshot of tabGroupSnapshots) {
14674
+ const newTabGroup = to.model.createTabGroup({
14675
+ label: snapshot.label,
14676
+ color: snapshot.color,
14677
+ collapsed: snapshot.collapsed,
14678
+ componentParams: snapshot.componentParams,
14679
+ });
14680
+ for (const panelId of snapshot.panelIds) {
14681
+ to.model.addPanelToTabGroup(newTabGroup.id, panelId);
14682
+ }
14683
+ }
14614
14684
  // Ensure group becomes active after move
14615
14685
  if (options.skipSetActive !== true) {
14616
14686
  // For center moves (merges), we need to ensure the target group is active
@@ -14634,6 +14704,17 @@
14634
14704
  * positions `source` like any other moved group.
14635
14705
  */
14636
14706
  const activePanel = from.activePanel;
14707
+ // Snapshot tab group metadata so the new group inherits
14708
+ // the tab grouping from the edge slot.
14709
+ const tabGroupSnapshots = from.model
14710
+ .getTabGroups()
14711
+ .map((tg) => ({
14712
+ label: tg.label,
14713
+ color: tg.color,
14714
+ collapsed: tg.collapsed,
14715
+ componentParams: tg.componentParams,
14716
+ panelIds: [...tg.panelIds],
14717
+ }));
14637
14718
  const movedPanels = this.movingLock(() => [...from.panels].map((p) => from.model.removePanel(p.id, { skipSetActive: true })));
14638
14719
  source = this.createGroup();
14639
14720
  this.movingLock(() => {
@@ -14644,6 +14725,17 @@
14644
14725
  });
14645
14726
  }
14646
14727
  });
14728
+ for (const snapshot of tabGroupSnapshots) {
14729
+ const newTabGroup = source.model.createTabGroup({
14730
+ label: snapshot.label,
14731
+ color: snapshot.color,
14732
+ collapsed: snapshot.collapsed,
14733
+ componentParams: snapshot.componentParams,
14734
+ });
14735
+ for (const panelId of snapshot.panelIds) {
14736
+ source.model.addPanelToTabGroup(newTabGroup.id, panelId);
14737
+ }
14738
+ }
14647
14739
  }
14648
14740
  else {
14649
14741
  switch (from.api.location.type) {
@@ -604,10 +604,12 @@ export class Tabs extends CompositeDisposable {
604
604
  const index = this.indexOf(id);
605
605
  const tabToRemove = this._tabs.splice(index, 1)[0];
606
606
  this._tabMap.delete(id);
607
- const { value, disposable } = tabToRemove;
608
- disposable.dispose();
609
- value.dispose();
610
- value.element.remove();
607
+ if (tabToRemove) {
608
+ const { value, disposable } = tabToRemove;
609
+ disposable.dispose();
610
+ value.dispose();
611
+ value.element.remove();
612
+ }
611
613
  // If a non-source tab was removed during active drag, refresh positions
612
614
  if (this._animState) {
613
615
  this._animState.tabPositions = this.snapshotTabPositions();
@@ -729,8 +731,23 @@ export class Tabs extends CompositeDisposable {
729
731
  new PanelTransfer(this.accessor.id, this.group.id, null, tabGroup.id),
730
732
  ], PanelTransfer.prototype);
731
733
  const iframes = disableIframePointEvents();
734
+ // The dragend listener on `_tabsList` is unreachable for chip
735
+ // drags because cross-group drops detach the chip from the DOM
736
+ // before dragend fires (the source tab group becomes empty, so
737
+ // `_positionChipForGroup` removes the chip element). Without
738
+ // bubbling, the tabsList listener never runs and `_animState`,
739
+ // `_chipDragCleanup`, and the dragging CSS classes leak. Listen
740
+ // directly on the chip element so cleanup happens regardless of
741
+ // whether it's still attached. (Issue #1254.)
742
+ const chipElement = chip.element;
743
+ const onChipDragEnd = () => {
744
+ chipElement.removeEventListener('dragend', onChipDragEnd);
745
+ this.resetDragAnimation();
746
+ };
747
+ chipElement.addEventListener('dragend', onChipDragEnd);
732
748
  this._chipDragCleanup = {
733
749
  dispose: () => {
750
+ chipElement.removeEventListener('dragend', onChipDragEnd);
734
751
  panelTransfer.clearData(PanelTransfer.prototype);
735
752
  iframes.release();
736
753
  },
@@ -1279,6 +1296,14 @@ export class Tabs extends CompositeDisposable {
1279
1296
  // handles panel transfer and tab group recreation.
1280
1297
  // Use the REAL tab group ID from transfer data, not the
1281
1298
  // potentially stale one from _animState.
1299
+ //
1300
+ // Clear any inline gap margin / shifting class applied to
1301
+ // destination tabs during dragover. Cross-group moves don't
1302
+ // run the FLIP path, and `moveGroupOrPanel` only inserts new
1303
+ // panels — it doesn't recreate existing destination tabs, so
1304
+ // their inline `margin-left` would otherwise persist as a
1305
+ // visible gap (issue #1243).
1306
+ this.resetTabTransforms();
1282
1307
  this.accessor.moveGroupOrPanel({
1283
1308
  from: {
1284
1309
  groupId: data.groupId,
@@ -924,7 +924,7 @@ export class DockviewComponent extends BaseGrid {
924
924
  // Collapse when the group becomes empty
925
925
  const autoCollapseDisposable = group.model.onDidRemovePanel(() => {
926
926
  if (group.model.isEmpty) {
927
- this._shellManager.setEdgeGroupCollapsed(position, true);
927
+ this.setEdgeGroupCollapsed(group, true);
928
928
  }
929
929
  });
930
930
  this._edgeGroupDisposables.set(position, autoCollapseDisposable);
@@ -968,6 +968,13 @@ export class DockviewComponent extends BaseGrid {
968
968
  setEdgeGroupCollapsed(group, collapsed) {
969
969
  for (const [position, edgeGroup] of this._edgeGroups) {
970
970
  if (edgeGroup === group) {
971
+ if (this._shellManager.isEdgeGroupCollapsed(position) ===
972
+ collapsed) {
973
+ // Skip the splitview resize on a no-op: with non-zero
974
+ // theme gap, redundant resizeView calls accumulate
975
+ // rounding drift that gradually shrinks the group.
976
+ return;
977
+ }
971
978
  this._shellManager.setEdgeGroupCollapsed(position, collapsed);
972
979
  edgeGroup.api._onDidCollapsedChange.fire({
973
980
  isCollapsed: collapsed,
@@ -1920,7 +1927,14 @@ export class DockviewComponent extends BaseGrid {
1920
1927
  const label = tabGroup.label;
1921
1928
  const color = tabGroup.color;
1922
1929
  const collapsed = tabGroup.collapsed;
1930
+ const componentParams = tabGroup.componentParams;
1923
1931
  const panelIds = [...tabGroup.panelIds];
1932
+ // Capture the destination's grid location BEFORE potentially
1933
+ // removing the source group, in case source === destination and
1934
+ // the source becomes empty after panel removal.
1935
+ const referenceLocation = destinationTarget && destinationTarget !== 'center'
1936
+ ? getGridLocation(destinationGroup.element)
1937
+ : undefined;
1924
1938
  // Remove panels from the source group
1925
1939
  const removedPanels = this.movingLock(() => panelIds
1926
1940
  .map((pid) => sourceGroup.model.removePanel(pid, {
@@ -1931,11 +1945,6 @@ export class DockviewComponent extends BaseGrid {
1931
1945
  if (removedPanels.length === 0) {
1932
1946
  return;
1933
1947
  }
1934
- if (!options.keepEmptyGroups &&
1935
- sourceGroup.model.size === 0 &&
1936
- sourceGroup !== destinationGroup) {
1937
- this.doRemoveGroup(sourceGroup, { skipActive: true });
1938
- }
1939
1948
  const addPanelsToGroup = (targetGroup) => {
1940
1949
  this.movingLock(() => {
1941
1950
  for (const panel of removedPanels) {
@@ -1951,6 +1960,7 @@ export class DockviewComponent extends BaseGrid {
1951
1960
  label,
1952
1961
  color,
1953
1962
  collapsed,
1963
+ componentParams,
1954
1964
  });
1955
1965
  for (const panel of removedPanels) {
1956
1966
  targetGroup.model.addPanelToTabGroup(newTabGroup.id, panel.id);
@@ -1965,15 +1975,27 @@ export class DockviewComponent extends BaseGrid {
1965
1975
  });
1966
1976
  }
1967
1977
  };
1968
- if (!destinationTarget || destinationTarget === 'center') {
1969
- addPanelsToGroup(destinationGroup);
1978
+ let targetGroup;
1979
+ if (!destinationTarget ||
1980
+ destinationTarget === 'center' ||
1981
+ !referenceLocation) {
1982
+ targetGroup = destinationGroup;
1970
1983
  }
1971
1984
  else {
1972
- const referenceLocation = getGridLocation(destinationGroup.element);
1973
1985
  const dropLocation = getRelativeLocation(this.gridview.orientation, referenceLocation, destinationTarget);
1974
- const newGroup = this.createGroupAtLocation(dropLocation);
1975
- addPanelsToGroup(newGroup);
1986
+ targetGroup = this.createGroupAtLocation(dropLocation);
1976
1987
  }
1988
+ // Remove the source group if it became empty. We compare against
1989
+ // the actual targetGroup (which is a freshly-created group for
1990
+ // edge drops) rather than the originally-passed destinationGroup,
1991
+ // so a tab-group drag onto its own group's edge still cleans up
1992
+ // the now-empty source.
1993
+ if (!options.keepEmptyGroups &&
1994
+ sourceGroup.model.size === 0 &&
1995
+ sourceGroup !== targetGroup) {
1996
+ this.doRemoveGroup(sourceGroup, { skipActive: true });
1997
+ }
1998
+ addPanelsToGroup(targetGroup);
1977
1999
  }
1978
2000
  moveGroup(options) {
1979
2001
  const from = options.from.group;
@@ -1985,6 +2007,16 @@ export class DockviewComponent extends BaseGrid {
1985
2007
  let source = from;
1986
2008
  if (target === 'center') {
1987
2009
  const activePanel = from.activePanel;
2010
+ // Snapshot tab group metadata before removing panels so we
2011
+ // can recreate the tab groups in the destination after the
2012
+ // panels are merged in.
2013
+ const tabGroupSnapshots = from.model.getTabGroups().map((tg) => ({
2014
+ label: tg.label,
2015
+ color: tg.color,
2016
+ collapsed: tg.collapsed,
2017
+ componentParams: tg.componentParams,
2018
+ panelIds: [...tg.panelIds],
2019
+ }));
1988
2020
  const panels = this.movingLock(() => [...from.panels].map((p) => from.model.removePanel(p.id, {
1989
2021
  skipSetActive: true,
1990
2022
  })));
@@ -1999,6 +2031,17 @@ export class DockviewComponent extends BaseGrid {
1999
2031
  });
2000
2032
  }
2001
2033
  });
2034
+ for (const snapshot of tabGroupSnapshots) {
2035
+ const newTabGroup = to.model.createTabGroup({
2036
+ label: snapshot.label,
2037
+ color: snapshot.color,
2038
+ collapsed: snapshot.collapsed,
2039
+ componentParams: snapshot.componentParams,
2040
+ });
2041
+ for (const panelId of snapshot.panelIds) {
2042
+ to.model.addPanelToTabGroup(newTabGroup.id, panelId);
2043
+ }
2044
+ }
2002
2045
  // Ensure group becomes active after move
2003
2046
  if (options.skipSetActive !== true) {
2004
2047
  // For center moves (merges), we need to ensure the target group is active
@@ -2022,6 +2065,17 @@ export class DockviewComponent extends BaseGrid {
2022
2065
  * positions `source` like any other moved group.
2023
2066
  */
2024
2067
  const activePanel = from.activePanel;
2068
+ // Snapshot tab group metadata so the new group inherits
2069
+ // the tab grouping from the edge slot.
2070
+ const tabGroupSnapshots = from.model
2071
+ .getTabGroups()
2072
+ .map((tg) => ({
2073
+ label: tg.label,
2074
+ color: tg.color,
2075
+ collapsed: tg.collapsed,
2076
+ componentParams: tg.componentParams,
2077
+ panelIds: [...tg.panelIds],
2078
+ }));
2025
2079
  const movedPanels = this.movingLock(() => [...from.panels].map((p) => from.model.removePanel(p.id, { skipSetActive: true })));
2026
2080
  source = this.createGroup();
2027
2081
  this.movingLock(() => {
@@ -2032,6 +2086,17 @@ export class DockviewComponent extends BaseGrid {
2032
2086
  });
2033
2087
  }
2034
2088
  });
2089
+ for (const snapshot of tabGroupSnapshots) {
2090
+ const newTabGroup = source.model.createTabGroup({
2091
+ label: snapshot.label,
2092
+ color: snapshot.color,
2093
+ collapsed: snapshot.collapsed,
2094
+ componentParams: snapshot.componentParams,
2095
+ });
2096
+ for (const panelId of snapshot.panelIds) {
2097
+ source.model.addPanelToTabGroup(newTabGroup.id, panelId);
2098
+ }
2099
+ }
2035
2100
  }
2036
2101
  else {
2037
2102
  switch (from.api.location.type) {
@@ -1061,15 +1061,17 @@ export class DockviewGroupPanelModel extends CompositeDisposable {
1061
1061
  if (position === 'center') {
1062
1062
  return;
1063
1063
  }
1064
- if (data.panelId === null) {
1065
- // don't allow group move to drop anywhere on self
1064
+ if (data.panelId === null && !data.tabGroupId) {
1065
+ // Full-group drops on self are a no-op.
1066
+ // Tab-group drags are partial moves: an edge drop
1067
+ // splits the layout and creates a new group.
1066
1068
  return;
1067
1069
  }
1068
1070
  }
1069
1071
  }
1070
1072
  if (type === 'header') {
1071
1073
  if (data.groupId === this.id) {
1072
- if (data.panelId === null) {
1074
+ if (data.panelId === null && !data.tabGroupId) {
1073
1075
  return;
1074
1076
  }
1075
1077
  }