dockview-core 6.6.1 → 7.0.3

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.
Files changed (203) hide show
  1. package/README.md +8 -1
  2. package/dist/cjs/api/component.api.d.ts +42 -21
  3. package/dist/cjs/api/component.api.js +111 -20
  4. package/dist/cjs/api/dockviewGroupPanelApi.d.ts +23 -8
  5. package/dist/cjs/api/dockviewGroupPanelApi.js +24 -1
  6. package/dist/cjs/api/dockviewPanelApi.d.ts +4 -3
  7. package/dist/cjs/api/dockviewPanelApi.js +8 -0
  8. package/dist/cjs/dnd/dropTargetAnchorContainer.js +1 -2
  9. package/dist/cjs/dnd/droptarget.d.ts +8 -0
  10. package/dist/cjs/dnd/droptarget.js +28 -0
  11. package/dist/cjs/dnd/pointer/longPress.js +1 -1
  12. package/dist/cjs/dnd/pointer/pointerDragController.js +9 -7
  13. package/dist/cjs/dnd/pointer/pointerDragSource.js +4 -2
  14. package/dist/cjs/dockview/accessibilityMessages.d.ts +32 -0
  15. package/dist/cjs/dockview/accessibilityMessages.js +51 -0
  16. package/dist/cjs/dockview/allModules.d.ts +8 -0
  17. package/dist/cjs/dockview/allModules.js +25 -0
  18. package/dist/cjs/dockview/components/panel/content.d.ts +2 -0
  19. package/dist/cjs/dockview/components/panel/content.js +41 -10
  20. package/dist/cjs/dockview/components/popupService.js +1 -1
  21. package/dist/cjs/dockview/components/tab/tab.js +35 -7
  22. package/dist/cjs/dockview/components/titlebar/floatingTitleBar.d.ts +34 -0
  23. package/dist/cjs/dockview/components/titlebar/floatingTitleBar.js +95 -0
  24. package/dist/cjs/dockview/components/titlebar/groupDragSource.d.ts +51 -0
  25. package/dist/cjs/dockview/components/titlebar/groupDragSource.js +218 -0
  26. package/dist/cjs/dockview/components/titlebar/tabGroupIndicator.d.ts +2 -1
  27. package/dist/cjs/dockview/components/titlebar/tabGroupIndicator.js +36 -30
  28. package/dist/cjs/dockview/components/titlebar/tabGroups.d.ts +27 -5
  29. package/dist/cjs/dockview/components/titlebar/tabGroups.js +104 -53
  30. package/dist/cjs/dockview/components/titlebar/tabs.d.ts +12 -0
  31. package/dist/cjs/dockview/components/titlebar/tabs.js +121 -25
  32. package/dist/cjs/dockview/components/titlebar/tabsContainer.d.ts +4 -0
  33. package/dist/cjs/dockview/components/titlebar/tabsContainer.js +13 -3
  34. package/dist/cjs/dockview/components/titlebar/voidContainer.d.ts +1 -4
  35. package/dist/cjs/dockview/components/titlebar/voidContainer.js +32 -156
  36. package/dist/cjs/dockview/dndCapabilities.js +3 -3
  37. package/dist/cjs/dockview/dockviewComponent.d.ts +304 -49
  38. package/dist/cjs/dockview/dockviewComponent.js +1839 -1075
  39. package/dist/cjs/dockview/dockviewFloatingGroupPanel.d.ts +33 -2
  40. package/dist/cjs/dockview/dockviewFloatingGroupPanel.js +39 -3
  41. package/dist/cjs/dockview/dockviewGroupPanel.d.ts +1 -2
  42. package/dist/cjs/dockview/dockviewGroupPanelModel.d.ts +36 -14
  43. package/dist/cjs/dockview/dockviewGroupPanelModel.js +151 -125
  44. package/dist/cjs/dockview/dockviewPanel.d.ts +2 -2
  45. package/dist/cjs/dockview/dockviewPanel.js +4 -5
  46. package/dist/cjs/dockview/dockviewShell.d.ts +29 -7
  47. package/dist/cjs/dockview/dockviewShell.js +94 -36
  48. package/dist/cjs/dockview/edgeGroupService.d.ts +38 -0
  49. package/dist/cjs/dockview/edgeGroupService.js +128 -0
  50. package/dist/cjs/dockview/floatingGroupService.d.ts +37 -0
  51. package/dist/cjs/dockview/floatingGroupService.js +231 -0
  52. package/dist/cjs/dockview/headerActionsService.d.ts +32 -0
  53. package/dist/cjs/dockview/headerActionsService.js +149 -0
  54. package/dist/cjs/dockview/liveRegionService.d.ts +53 -0
  55. package/dist/cjs/dockview/liveRegionService.js +185 -0
  56. package/dist/cjs/dockview/moduleContracts.d.ts +119 -0
  57. package/dist/cjs/dockview/moduleContracts.js +2 -0
  58. package/dist/cjs/dockview/modules.d.ts +110 -0
  59. package/dist/cjs/dockview/modules.js +304 -0
  60. package/dist/cjs/dockview/options.d.ts +159 -6
  61. package/dist/cjs/dockview/options.js +8 -1
  62. package/dist/cjs/dockview/popoutWindowService.d.ts +95 -0
  63. package/dist/cjs/dockview/popoutWindowService.js +261 -0
  64. package/dist/cjs/dockview/rootDropTargetService.d.ts +35 -0
  65. package/dist/cjs/dockview/rootDropTargetService.js +87 -0
  66. package/dist/cjs/dockview/strictEventsSequencing.js +6 -6
  67. package/dist/cjs/dockview/tabGroup.js +3 -3
  68. package/dist/cjs/dockview/tabGroupAccent.js +1 -3
  69. package/dist/cjs/dockview/watermarkService.d.ts +30 -0
  70. package/dist/cjs/dockview/watermarkService.js +61 -0
  71. package/dist/cjs/dom.js +1 -1
  72. package/dist/cjs/events.js +1 -1
  73. package/dist/cjs/gridview/baseComponentGridview.d.ts +1 -1
  74. package/dist/cjs/gridview/baseComponentGridview.js +6 -10
  75. package/dist/cjs/gridview/branchNode.js +13 -17
  76. package/dist/cjs/gridview/gridview.js +8 -8
  77. package/dist/cjs/gridview/gridviewComponent.d.ts +3 -3
  78. package/dist/cjs/gridview/gridviewComponent.js +1 -6
  79. package/dist/cjs/gridview/gridviewPanel.d.ts +1 -1
  80. package/dist/cjs/index.d.ts +11 -4
  81. package/dist/cjs/index.js +14 -1
  82. package/dist/cjs/overlay/overlay.d.ts +43 -1
  83. package/dist/cjs/overlay/overlay.js +82 -36
  84. package/dist/cjs/overlay/overlayRenderContainer.js +3 -4
  85. package/dist/cjs/paneview/draggablePaneviewPanel.d.ts +2 -2
  86. package/dist/cjs/paneview/draggablePaneviewPanel.js +5 -5
  87. package/dist/cjs/paneview/paneviewComponent.d.ts +3 -3
  88. package/dist/cjs/paneview/paneviewComponent.js +7 -11
  89. package/dist/cjs/popoutWindow.js +4 -4
  90. package/dist/cjs/scrollbar.d.ts +1 -1
  91. package/dist/cjs/scrollbar.js +7 -6
  92. package/dist/cjs/splitview/splitview.d.ts +1 -1
  93. package/dist/cjs/splitview/splitview.js +21 -20
  94. package/dist/cjs/splitview/viewItem.js +1 -1
  95. package/dist/dockview-core.js +3548 -1544
  96. package/dist/dockview-core.min.js +2 -2
  97. package/dist/dockview-core.min.js.map +1 -1
  98. package/dist/dockview-core.min.noStyle.js +2 -2
  99. package/dist/dockview-core.min.noStyle.js.map +1 -1
  100. package/dist/dockview-core.noStyle.js +3547 -1543
  101. package/dist/esm/api/component.api.d.ts +42 -21
  102. package/dist/esm/api/component.api.js +63 -18
  103. package/dist/esm/api/dockviewGroupPanelApi.d.ts +23 -8
  104. package/dist/esm/api/dockviewGroupPanelApi.js +20 -1
  105. package/dist/esm/api/dockviewPanelApi.d.ts +4 -3
  106. package/dist/esm/api/dockviewPanelApi.js +7 -0
  107. package/dist/esm/dnd/dropTargetAnchorContainer.js +1 -2
  108. package/dist/esm/dnd/droptarget.d.ts +8 -0
  109. package/dist/esm/dnd/droptarget.js +28 -0
  110. package/dist/esm/dnd/pointer/longPress.js +1 -1
  111. package/dist/esm/dnd/pointer/pointerDragController.js +9 -7
  112. package/dist/esm/dnd/pointer/pointerDragSource.js +4 -2
  113. package/dist/esm/dockview/accessibilityMessages.d.ts +32 -0
  114. package/dist/esm/dockview/accessibilityMessages.js +30 -0
  115. package/dist/esm/dockview/allModules.d.ts +8 -0
  116. package/dist/esm/dockview/allModules.js +22 -0
  117. package/dist/esm/dockview/components/panel/content.d.ts +2 -0
  118. package/dist/esm/dockview/components/panel/content.js +42 -11
  119. package/dist/esm/dockview/components/popupService.js +1 -1
  120. package/dist/esm/dockview/components/tab/tab.js +35 -7
  121. package/dist/esm/dockview/components/titlebar/floatingTitleBar.d.ts +34 -0
  122. package/dist/esm/dockview/components/titlebar/floatingTitleBar.js +65 -0
  123. package/dist/esm/dockview/components/titlebar/groupDragSource.d.ts +51 -0
  124. package/dist/esm/dockview/components/titlebar/groupDragSource.js +178 -0
  125. package/dist/esm/dockview/components/titlebar/tabGroupIndicator.d.ts +2 -1
  126. package/dist/esm/dockview/components/titlebar/tabGroupIndicator.js +36 -30
  127. package/dist/esm/dockview/components/titlebar/tabGroups.d.ts +27 -5
  128. package/dist/esm/dockview/components/titlebar/tabGroups.js +91 -41
  129. package/dist/esm/dockview/components/titlebar/tabs.d.ts +12 -0
  130. package/dist/esm/dockview/components/titlebar/tabs.js +119 -26
  131. package/dist/esm/dockview/components/titlebar/tabsContainer.d.ts +4 -0
  132. package/dist/esm/dockview/components/titlebar/tabsContainer.js +8 -2
  133. package/dist/esm/dockview/components/titlebar/voidContainer.d.ts +1 -4
  134. package/dist/esm/dockview/components/titlebar/voidContainer.js +34 -146
  135. package/dist/esm/dockview/dndCapabilities.js +3 -3
  136. package/dist/esm/dockview/dockviewComponent.d.ts +304 -49
  137. package/dist/esm/dockview/dockviewComponent.js +1459 -772
  138. package/dist/esm/dockview/dockviewFloatingGroupPanel.d.ts +33 -2
  139. package/dist/esm/dockview/dockviewFloatingGroupPanel.js +35 -3
  140. package/dist/esm/dockview/dockviewGroupPanel.d.ts +1 -2
  141. package/dist/esm/dockview/dockviewGroupPanelModel.d.ts +36 -14
  142. package/dist/esm/dockview/dockviewGroupPanelModel.js +127 -117
  143. package/dist/esm/dockview/dockviewPanel.d.ts +2 -2
  144. package/dist/esm/dockview/dockviewPanel.js +1 -2
  145. package/dist/esm/dockview/dockviewShell.d.ts +29 -7
  146. package/dist/esm/dockview/dockviewShell.js +89 -37
  147. package/dist/esm/dockview/edgeGroupService.d.ts +38 -0
  148. package/dist/esm/dockview/edgeGroupService.js +63 -0
  149. package/dist/esm/dockview/floatingGroupService.d.ts +37 -0
  150. package/dist/esm/dockview/floatingGroupService.js +150 -0
  151. package/dist/esm/dockview/headerActionsService.d.ts +32 -0
  152. package/dist/esm/dockview/headerActionsService.js +86 -0
  153. package/dist/esm/dockview/liveRegionService.d.ts +53 -0
  154. package/dist/esm/dockview/liveRegionService.js +159 -0
  155. package/dist/esm/dockview/moduleContracts.d.ts +119 -0
  156. package/dist/esm/dockview/moduleContracts.js +1 -0
  157. package/dist/esm/dockview/modules.d.ts +110 -0
  158. package/dist/esm/dockview/modules.js +170 -0
  159. package/dist/esm/dockview/options.d.ts +159 -6
  160. package/dist/esm/dockview/options.js +8 -1
  161. package/dist/esm/dockview/popoutWindowService.d.ts +95 -0
  162. package/dist/esm/dockview/popoutWindowService.js +175 -0
  163. package/dist/esm/dockview/rootDropTargetService.d.ts +35 -0
  164. package/dist/esm/dockview/rootDropTargetService.js +82 -0
  165. package/dist/esm/dockview/strictEventsSequencing.js +6 -6
  166. package/dist/esm/dockview/tabGroup.js +3 -3
  167. package/dist/esm/dockview/tabGroupAccent.js +1 -3
  168. package/dist/esm/dockview/watermarkService.d.ts +30 -0
  169. package/dist/esm/dockview/watermarkService.js +56 -0
  170. package/dist/esm/dom.js +1 -1
  171. package/dist/esm/events.js +1 -1
  172. package/dist/esm/gridview/baseComponentGridview.d.ts +1 -1
  173. package/dist/esm/gridview/baseComponentGridview.js +5 -10
  174. package/dist/esm/gridview/branchNode.js +13 -15
  175. package/dist/esm/gridview/gridview.js +8 -8
  176. package/dist/esm/gridview/gridviewComponent.d.ts +3 -3
  177. package/dist/esm/gridview/gridviewComponent.js +1 -6
  178. package/dist/esm/gridview/gridviewPanel.d.ts +1 -1
  179. package/dist/esm/index.d.ts +11 -4
  180. package/dist/esm/index.js +4 -0
  181. package/dist/esm/overlay/overlay.d.ts +43 -1
  182. package/dist/esm/overlay/overlay.js +78 -36
  183. package/dist/esm/overlay/overlayRenderContainer.js +3 -4
  184. package/dist/esm/paneview/draggablePaneviewPanel.d.ts +2 -2
  185. package/dist/esm/paneview/draggablePaneviewPanel.js +5 -5
  186. package/dist/esm/paneview/paneviewComponent.d.ts +3 -3
  187. package/dist/esm/paneview/paneviewComponent.js +7 -11
  188. package/dist/esm/popoutWindow.js +4 -4
  189. package/dist/esm/scrollbar.d.ts +1 -1
  190. package/dist/esm/scrollbar.js +7 -6
  191. package/dist/esm/splitview/splitview.d.ts +1 -1
  192. package/dist/esm/splitview/splitview.js +21 -20
  193. package/dist/esm/splitview/viewItem.js +1 -1
  194. package/dist/package/main.cjs.js +3550 -1546
  195. package/dist/package/main.cjs.min.js +2 -2
  196. package/dist/package/main.esm.min.mjs +2 -2
  197. package/dist/package/main.esm.mjs +3538 -1545
  198. package/dist/styles/dockview.css +283 -21
  199. package/package.json +10 -1
  200. package/dist/cjs/dockview/contextMenu.d.ts +0 -10
  201. package/dist/cjs/dockview/contextMenu.js +0 -313
  202. package/dist/esm/dockview/contextMenu.d.ts +0 -10
  203. package/dist/esm/dockview/contextMenu.js +0 -228
@@ -1,7 +1,6 @@
1
- import { getRelativeLocation, getGridLocation, orthogonal, } from '../gridview/gridview';
1
+ import { getRelativeLocation, getGridLocation, orthogonal, Gridview, } from '../gridview/gridview';
2
2
  import { directionToPosition, } from '../dnd/droptarget';
3
- import { html5Backend, pointerBackend } from '../dnd/backend';
4
- import { tail, sequenceEquals, remove } from '../array';
3
+ import { tail, sequenceEquals } from '../array';
5
4
  import { DockviewPanel } from './dockviewPanel';
6
5
  import { CompositeDisposable, Disposable } from '../lifecycle';
7
6
  import { Event, Emitter, addDisposableListener } from '../events';
@@ -11,29 +10,26 @@ import { DefaultDockviewDeserialzier } from './deserializer';
11
10
  import { DockviewUnhandledDragOverEvent, isGroupOptionsWithGroup, isGroupOptionsWithPanel, isPanelOptionsWithGroup, isPanelOptionsWithPanel, } from './options';
12
11
  import { BaseGrid, toTarget, } from '../gridview/baseComponentGridview';
13
12
  import { DockviewApi } from '../api/component.api';
14
- import { Orientation } from '../splitview/splitview';
13
+ import { Orientation, Sizing } from '../splitview/splitview';
15
14
  import { DockviewDidDropEvent, DockviewWillDropEvent, } from './dockviewGroupPanelModel';
16
15
  import { DockviewWillShowOverlayLocationEvent, } from './events';
17
16
  import { DockviewGroupPanel } from './dockviewGroupPanel';
18
17
  import { DockviewPanelModel } from './dockviewPanelModel';
19
18
  import { getPanelData } from '../dnd/dataTransfer';
20
19
  import { Overlay } from '../overlay/overlay';
21
- import { addTestId, Classnames, getDockviewTheme, onDidWindowResizeEnd, onDidWindowMoveEnd, toggleClass, watchElementResize, } from '../dom';
22
- import { DockviewFloatingGroupPanel } from './dockviewFloatingGroupPanel';
20
+ import { Classnames, getDockviewTheme, onDidWindowResizeEnd, onDidWindowMoveEnd, toggleClass, } from '../dom';
21
+ import { FloatingTitleBar } from './components/titlebar/floatingTitleBar';
22
+ import { assertModule, getRegisteredModules, isDockviewPackageLoaded, ModuleRegistry, } from './modules';
23
+ import { AllModules } from './allModules';
23
24
  import { DEFAULT_FLOATING_GROUP_OVERFLOW_SIZE, DEFAULT_FLOATING_GROUP_POSITION, DESERIALIZATION_POPOUT_DELAY_MS, } from '../constants';
24
25
  import { OverlayRenderContainer, } from '../overlay/overlayRenderContainer';
25
26
  import { PopoutWindow } from '../popoutWindow';
26
27
  import { StrictEventsSequencing } from './strictEventsSequencing';
27
28
  import { PopupService } from './components/popupService';
28
- import { ContextMenuController } from './contextMenu';
29
29
  import { DropTargetAnchorContainer } from '../dnd/dropTargetAnchorContainer';
30
30
  import { themeAbyss } from './theme';
31
31
  import { ShellManager, } from './dockviewShell';
32
32
  import { DEFAULT_TAB_GROUP_COLORS, TabGroupColorPalette, } from './tabGroupAccent';
33
- const DEFAULT_ROOT_OVERLAY_MODEL = {
34
- activationSize: { type: 'pixels', value: 10 },
35
- size: { type: 'pixels', value: 20 },
36
- };
37
33
  function buildTabGroupColorPalette(options) {
38
34
  var _a;
39
35
  const entries = (_a = options.tabGroupColors) !== null && _a !== void 0 ? _a : DEFAULT_TAB_GROUP_COLORS;
@@ -54,7 +50,51 @@ function moveGroupWithoutDestroying(options) {
54
50
  });
55
51
  });
56
52
  }
53
+ let _hasWarnedUsingCoreDirectly = false;
54
+ /**
55
+ * `dockview-core` is an internal package. The public `dockview` package calls
56
+ * `markDockviewPackageLoaded()` on import; if that marker is absent the consumer
57
+ * is using `dockview-core` directly, so emit a one-time console warning
58
+ * steering them to `dockview`.
59
+ *
60
+ * Suppressed in production builds: it is a development-time nudge and most
61
+ * bundlers inline `process.env.NODE_ENV` so the branch is dropped entirely. The
62
+ * `typeof process` guard keeps this safe in plain browser/UMD contexts where
63
+ * `process` is undefined.
64
+ */
65
+ function warnIfUsingCoreDirectly() {
66
+ var _a;
67
+ if (typeof process !== 'undefined' &&
68
+ ((_a = process.env) === null || _a === void 0 ? void 0 : _a.NODE_ENV) === 'production') {
69
+ return;
70
+ }
71
+ if (_hasWarnedUsingCoreDirectly || isDockviewPackageLoaded()) {
72
+ return;
73
+ }
74
+ _hasWarnedUsingCoreDirectly = true;
75
+ console.warn('dockview: do not use "dockview-core" directly — it is an internal ' +
76
+ 'package. Use the "dockview" package, the JavaScript version of ' +
77
+ 'dockview, instead. This notice is shown once.');
78
+ }
57
79
  export class DockviewComponent extends BaseGrid {
80
+ fireDidCreateTabGroup(event) {
81
+ this._onDidCreateTabGroup.fire(event);
82
+ }
83
+ fireDidDestroyTabGroup(event) {
84
+ this._onDidDestroyTabGroup.fire(event);
85
+ }
86
+ fireDidAddPanelToTabGroup(event) {
87
+ this._onDidAddPanelToTabGroup.fire(event);
88
+ }
89
+ fireDidRemovePanelFromTabGroup(event) {
90
+ this._onDidRemovePanelFromTabGroup.fire(event);
91
+ }
92
+ fireDidTabGroupChange(event) {
93
+ this._onDidTabGroupChange.fire(event);
94
+ }
95
+ fireDidTabGroupCollapsedChange(event) {
96
+ this._onDidTabGroupCollapsedChange.fire(event);
97
+ }
58
98
  get orientation() {
59
99
  return this.gridview.orientation;
60
100
  }
@@ -89,17 +129,210 @@ export class DockviewComponent extends BaseGrid {
89
129
  return this._api;
90
130
  }
91
131
  get floatingGroups() {
92
- return this._floatingGroups;
132
+ var _a, _b, _c;
133
+ return ((_c = (_b = (_a = this._moduleRegistry) === null || _a === void 0 ? void 0 : _a.services.floatingGroupService) === null || _b === void 0 ? void 0 : _b.floatingGroups) !== null && _c !== void 0 ? _c : []);
134
+ }
135
+ /**
136
+ * Boxes of the floating groups other than `exclude`, in coordinates
137
+ * relative to the floating overlay container. Supplied to a
138
+ * `transformFloatingGroupDrag` callback as `context.others` so it can
139
+ * align the dragged float against its siblings.
140
+ */
141
+ _gatherFloatingGroupBoxes(exclude) {
142
+ var _a;
143
+ const container = (_a = this._floatingOverlayHost) !== null && _a !== void 0 ? _a : this.gridview.element;
144
+ const containerRect = container.getBoundingClientRect();
145
+ return this.floatingGroups
146
+ .filter((floating) => floating.group !== exclude)
147
+ .map((floating) => {
148
+ const rect = floating.overlay.element.getBoundingClientRect();
149
+ return {
150
+ left: rect.left - containerRect.left,
151
+ top: rect.top - containerRect.top,
152
+ width: rect.width,
153
+ height: rect.height,
154
+ };
155
+ });
156
+ }
157
+ get _floatingGroupService() {
158
+ return this._moduleRegistry.services.floatingGroupService;
159
+ }
160
+ get _popoutWindowService() {
161
+ return this._moduleRegistry.services.popoutWindowService;
162
+ }
163
+ get _watermarkService() {
164
+ // Tier 1 module — optional. Callers must `?.`-guard so the module
165
+ // can be removed from AllModules without crashing the component.
166
+ return this._moduleRegistry.services.watermarkService;
167
+ }
168
+ get _edgeGroupService() {
169
+ return this._moduleRegistry.services.edgeGroupService;
170
+ }
171
+ get _rootDropTargetService() {
172
+ // Optional like every other module service — RootDropTargetModule can be
173
+ // removed from the registered set without crashing the component.
174
+ return this._moduleRegistry.services.rootDropTargetService;
175
+ }
176
+ get _advancedDnDService() {
177
+ // Optional — callers `?.`-guard so the module can be removed from
178
+ // AllModules. Absent ⇒ the onWill* hooks simply don't fire (≡ no
179
+ // subscriber), which is invisible to apps not customising DnD.
180
+ return this._moduleRegistry.services.advancedDnDService;
181
+ }
182
+ get headerActionsService() {
183
+ return this._moduleRegistry.services.headerActionsService;
184
+ }
185
+ isGridEmpty() {
186
+ return this.gridview.length === 0;
187
+ }
188
+ rootDropTargetOverrideTarget() {
189
+ var _a;
190
+ return (_a = this.rootDropTargetContainer) === null || _a === void 0 ? void 0 : _a.model;
191
+ }
192
+ dispatchUnhandledDragOver(nativeEvent, position) {
193
+ const event = new DockviewUnhandledDragOverEvent(nativeEvent, 'edge', position, getPanelData);
194
+ this._onUnhandledDragOver.fire(event);
195
+ return event.isAccepted;
196
+ }
197
+ // IAdvancedDnDHost — the emitters stay here so the public onWill* event
198
+ // shape is unchanged; AdvancedDnDService routes the per-group fires
199
+ // through these. Engine guards (e.g. disableDnd) run on the component
200
+ // ahead of the dispatch.
201
+ fireWillDragPanel(event) {
202
+ this._onWillDragPanel.fire(event);
203
+ }
204
+ fireWillDragGroup(event) {
205
+ this._onWillDragGroup.fire(event);
206
+ }
207
+ fireWillDrop(event) {
208
+ this._onWillDrop.fire(event);
209
+ }
210
+ fireWillShowOverlay(event) {
211
+ this._onWillShowOverlay.fire(event);
212
+ }
213
+ /**
214
+ * Resolve the custom group drag ghost (via the AdvancedDnD module), or
215
+ * `undefined` to fall back to the default chip. Returns `undefined` when
216
+ * the module is absent — the default ghost then renders.
217
+ */
218
+ buildGroupDragGhost(group) {
219
+ var _a;
220
+ return (_a = this._advancedDnDService) === null || _a === void 0 ? void 0 : _a.buildGroupDragGhost(group);
221
+ }
222
+ /**
223
+ * Resolve the app-supplied drop overlay model (via the AdvancedDnD module)
224
+ * for a group drop target, or `undefined` to keep the target's default.
225
+ */
226
+ resolveDropOverlayModel(location, group) {
227
+ var _a;
228
+ return (_a = this._advancedDnDService) === null || _a === void 0 ? void 0 : _a.resolveOverlayModel(location, group);
229
+ }
230
+ // IAccessibilityHost — keyboard docking reaches the AdvancedDnD preview +
231
+ // LiveRegion announcer through these so the service stays decoupled.
232
+ /** Outermost element — the shell (incl. edge groups) once built, else the gridview. */
233
+ get rootElement() {
234
+ var _a, _b;
235
+ return (_b = (_a = this._shellManager) === null || _a === void 0 ? void 0 : _a.element) !== null && _b !== void 0 ? _b : this.element;
236
+ }
237
+ /**
238
+ * The next / previous group in gridview (spatial) order, wrapping round.
239
+ * The keyboard accessibility module's focus navigation is built on this
240
+ * primitive — the only piece that needs the grid internals; the rest of
241
+ * the navigation logic lives in the AccessibilityService.
242
+ */
243
+ adjacentGroup(group, reverse) {
244
+ var _a;
245
+ // gridview traversal only covers grid groups; a floating/popout group
246
+ // isn't in the grid, so there's no adjacent grid group to step to.
247
+ if (group.api.location.type !== 'grid') {
248
+ return undefined;
249
+ }
250
+ const location = getGridLocation(group.element);
251
+ return ((_a = (reverse
252
+ ? this.gridview.previous(location)
253
+ : this.gridview.next(location))) === null || _a === void 0 ? void 0 : _a.view);
254
+ }
255
+ /**
256
+ * The nearest grid group in a spatial direction from `group`, by
257
+ * comparing group centre points. Floating and popout groups sit outside
258
+ * the grid's geometry and are ignored. Returns `undefined` when there is
259
+ * no group in that direction.
260
+ */
261
+ adjacentGroupInDirection(group, direction) {
262
+ if (group.api.location.type !== 'grid') {
263
+ return undefined;
264
+ }
265
+ const from = group.element.getBoundingClientRect();
266
+ const fromX = from.left + from.width / 2;
267
+ const fromY = from.top + from.height / 2;
268
+ let best;
269
+ let bestDistance = Number.POSITIVE_INFINITY;
270
+ for (const candidate of this.groups) {
271
+ if (candidate === group || candidate.api.location.type !== 'grid') {
272
+ continue;
273
+ }
274
+ const rect = candidate.element.getBoundingClientRect();
275
+ const dx = rect.left + rect.width / 2 - fromX;
276
+ const dy = rect.top + rect.height / 2 - fromY;
277
+ // Require the candidate to sit predominantly in the asked-for
278
+ // direction (dominant axis), so 'left' ignores a group that's
279
+ // mostly above/below.
280
+ const inDirection = direction === 'left'
281
+ ? dx < 0 && Math.abs(dx) >= Math.abs(dy)
282
+ : direction === 'right'
283
+ ? dx > 0 && Math.abs(dx) >= Math.abs(dy)
284
+ : direction === 'up'
285
+ ? dy < 0 && Math.abs(dy) >= Math.abs(dx)
286
+ : dy > 0 && Math.abs(dy) >= Math.abs(dx);
287
+ if (!inDirection) {
288
+ continue;
289
+ }
290
+ const distance = dx * dx + dy * dy;
291
+ if (distance < bestDistance) {
292
+ bestDistance = distance;
293
+ best = candidate;
294
+ }
295
+ }
296
+ return best;
297
+ }
298
+ showDropPreview(group, position) {
299
+ var _a, _b;
300
+ return ((_b = (_a = this._advancedDnDService) === null || _a === void 0 ? void 0 : _a.showPreviewOverlay(group, position)) !== null && _b !== void 0 ? _b : Disposable.NONE);
301
+ }
302
+ announce(message) {
303
+ var _a;
304
+ (_a = this._moduleRegistry.services.liveRegionService) === null || _a === void 0 ? void 0 : _a.announce(message);
305
+ }
306
+ dockPanel(panel, group, position) {
307
+ this.moveGroupOrPanel({
308
+ from: { groupId: panel.group.id, panelId: panel.id },
309
+ to: { group, position },
310
+ });
311
+ }
312
+ get contextMenuService() {
313
+ // Owned by ContextMenuModule — undefined when the module is not
314
+ // registered, so callers must `?.`-guard.
315
+ return this._moduleRegistry.services.contextMenuService;
316
+ }
317
+ get mountElement() {
318
+ return this.gridview.element;
319
+ }
320
+ hasVisibleGridGroup() {
321
+ return this.groups.some((group) => group.api.location.type === 'grid' && group.api.isVisible);
322
+ }
323
+ fireLayoutChange() {
324
+ this._bufferOnDidLayoutChange.fire();
93
325
  }
94
326
  /**
95
327
  * Promise that resolves when all popout groups from the last fromJSON call are restored.
96
328
  * Useful for tests that need to wait for delayed popout creation.
97
329
  */
98
330
  get popoutRestorationPromise() {
99
- return this._popoutRestorationPromise;
331
+ var _a, _b;
332
+ return ((_b = (_a = this._popoutWindowService) === null || _a === void 0 ? void 0 : _a.restorationPromise) !== null && _b !== void 0 ? _b : Promise.resolve());
100
333
  }
101
334
  constructor(container, options) {
102
- var _a, _b, _c, _d, _e, _f, _g;
335
+ var _a, _b, _c, _d, _e;
103
336
  super(container, {
104
337
  proportionalLayout: true,
105
338
  orientation: Orientation.HORIZONTAL,
@@ -113,8 +346,7 @@ export class DockviewComponent extends BaseGrid {
113
346
  });
114
347
  this.nextGroupId = sequentialNumberGenerator();
115
348
  this._deserializer = new DefaultDockviewDeserialzier(this);
116
- this._watermark = null;
117
- this._popoutPopupServices = new Map();
349
+ this._moduleRegistry = new ModuleRegistry();
118
350
  this._onWillDragPanel = new Emitter();
119
351
  this.onWillDragPanel = this._onWillDragPanel.event;
120
352
  this._onWillDragGroup = new Emitter();
@@ -123,10 +355,25 @@ export class DockviewComponent extends BaseGrid {
123
355
  this.onDidDrop = this._onDidDrop.event;
124
356
  this._onWillDrop = new Emitter();
125
357
  this.onWillDrop = this._onWillDrop.event;
358
+ // Transaction boundary bracketing each top-level structural mutation.
359
+ // Compound operations (e.g. a drag that relocates a panel) nest via the
360
+ // depth counter and bracket as a single transaction. See `mutation()`.
361
+ this._mutationDepth = 0;
362
+ // Current operation origin. Defaults to `'user'`; the DockviewApi boundary
363
+ // flips it to `'api'` for the duration of a programmatic call via
364
+ // `withOrigin`. Nested operations inherit the outermost origin (tracked by
365
+ // `_originDepth`, independent of mutation bracketing so it also covers
366
+ // active-panel changes that are not structural mutations).
367
+ this._origin = 'user';
368
+ this._originDepth = 0;
369
+ this._onWillMutateLayout = new Emitter();
370
+ this.onWillMutateLayout = this._onWillMutateLayout.event;
371
+ this._onDidMutateLayout = new Emitter();
372
+ this.onDidMutateLayout = this._onDidMutateLayout.event;
126
373
  this._onWillShowOverlay = new Emitter();
127
374
  this.onWillShowOverlay = this._onWillShowOverlay.event;
128
- this._onUnhandledDragOverEvent = new Emitter();
129
- this.onUnhandledDragOverEvent = this._onUnhandledDragOverEvent.event;
375
+ this._onUnhandledDragOver = new Emitter();
376
+ this.onUnhandledDragOver = this._onUnhandledDragOver.event;
130
377
  this._onDidRemovePanel = new Emitter();
131
378
  this.onDidRemovePanel = this._onDidRemovePanel.event;
132
379
  this._onDidAddPanel = new Emitter();
@@ -135,6 +382,10 @@ export class DockviewComponent extends BaseGrid {
135
382
  this.onDidPopoutGroupSizeChange = this._onDidPopoutGroupSizeChange.event;
136
383
  this._onDidPopoutGroupPositionChange = new Emitter();
137
384
  this.onDidPopoutGroupPositionChange = this._onDidPopoutGroupPositionChange.event;
385
+ this._onDidAddPopoutGroup = new Emitter();
386
+ this.onDidAddPopoutGroup = this._onDidAddPopoutGroup.event;
387
+ this._onDidRemovePopoutGroup = new Emitter();
388
+ this.onDidRemovePopoutGroup = this._onDidRemovePopoutGroup.event;
138
389
  this._onDidOpenPopoutWindowFail = new Emitter();
139
390
  this.onDidOpenPopoutWindowFail = this._onDidOpenPopoutWindowFail.event;
140
391
  this._onDidLayoutFromJSON = new Emitter();
@@ -158,12 +409,6 @@ export class DockviewComponent extends BaseGrid {
158
409
  this._onDidMaximizedGroupChange = new Emitter();
159
410
  this.onDidMaximizedGroupChange = this._onDidMaximizedGroupChange.event;
160
411
  this._inShellLayout = false;
161
- this._edgeGroups = new Map();
162
- this._edgeGroupDisposables = new Map();
163
- this._floatingGroups = [];
164
- this._popoutGroups = [];
165
- this._popoutRestorationPromise = Promise.resolve();
166
- this._popoutRestorationCleanups = new Set();
167
412
  this._onDidRemoveGroup = new Emitter();
168
413
  this.onDidRemoveGroup = this._onDidRemoveGroup.event;
169
414
  this._onDidAddGroup = new Emitter();
@@ -175,13 +420,43 @@ export class DockviewComponent extends BaseGrid {
175
420
  this._moving = false;
176
421
  this._options = options;
177
422
  this._tabGroupColorPalette = buildTabGroupColorPalette(options);
423
+ // Internal seam: defaults to the full set, but tests can supply a
424
+ // subset to assert every module is independently removable (and the
425
+ // opt-in module API will build on this later). Not part of the public
426
+ // options surface.
427
+ const explicitModules = options
428
+ .modules;
429
+ const modules = explicitModules !== null && explicitModules !== void 0 ? explicitModules : [
430
+ ...AllModules,
431
+ ...getRegisteredModules(),
432
+ ];
433
+ for (const module of modules) {
434
+ this._moduleRegistry.register(module);
435
+ }
436
+ this._moduleRegistry.initialize(this);
437
+ // Surface popout removal symmetrically with `onDidAddPopoutGroup`. The
438
+ // service is the single point every removal path funnels through — a
439
+ // genuine window close and an explicit removal alike — and it suppresses
440
+ // the event during component teardown.
441
+ const popoutWindowService = this._popoutWindowService;
442
+ if (popoutWindowService) {
443
+ this.addDisposables(popoutWindowService.onDidRemove((entry) => {
444
+ this._onDidRemovePopoutGroup.fire({
445
+ id: entry.popoutGroup.id,
446
+ group: entry.popoutGroup,
447
+ window: entry.getWindow(),
448
+ });
449
+ }));
450
+ }
451
+ // Purely a developer warning (no functional effect): nudge anyone using
452
+ // the internal `dockview-core` package directly towards `dockview`.
453
+ warnIfUsingCoreDirectly();
178
454
  this.popupService = new PopupService(this.element);
179
- this.contextMenuController = new ContextMenuController(this);
180
455
  this._api = new DockviewApi(this);
181
456
  // The shell always wraps the dockview element so edge groups can be
182
457
  // added at any time via addEdgeGroup() without re-parenting the DOM.
183
458
  this.disableResizing = true;
184
- container.removeChild(this.element);
459
+ this.element.remove();
185
460
  this._shellManager = new ShellManager(container, this.element, (w, h) => this._layoutFromShell(w, h), (_d = (_c = options.theme) === null || _c === void 0 ? void 0 : _c.gap) !== null && _d !== void 0 ? _d : 0, (_e = options.theme) === null || _e === void 0 ? void 0 : _e.edgeGroupCollapsedSize);
186
461
  // The shell wraps the dockview element, so move the popup anchor
187
462
  // into the shell so overflow dropdowns in edge groups position correctly
@@ -198,69 +473,23 @@ export class DockviewComponent extends BaseGrid {
198
473
  this._floatingOverlayHost = document.createElement('div');
199
474
  this._floatingOverlayHost.className = 'dv-floating-overlay-host';
200
475
  this._shellManager.element.appendChild(this._floatingOverlayHost);
201
- const rootCanDisplayOverlay = (event, position) => {
202
- const data = getPanelData();
203
- if (data) {
204
- if (data.viewId !== this.id) {
205
- return false;
206
- }
207
- if (position === 'center') {
208
- // center drop target is only allowed if there are no panels in the grid
209
- // floating panels are allowed
210
- return this.gridview.length === 0;
211
- }
212
- return true;
213
- }
214
- if (position === 'center' && this.gridview.length !== 0) {
215
- /**
216
- * for external events only show the four-corner drag overlays, disable
217
- * the center position so that external drag events can fall through to the group
218
- * and panel drop target handlers
219
- */
220
- return false;
221
- }
222
- const firedEvent = new DockviewUnhandledDragOverEvent(event, 'edge', position, getPanelData);
223
- this._onUnhandledDragOverEvent.fire(firedEvent);
224
- return firedEvent.isAccepted;
225
- };
226
- this._rootDropTarget = html5Backend.createDropTarget(this.element, {
227
- className: 'dv-drop-target-edge',
228
- canDisplayOverlay: rootCanDisplayOverlay,
229
- acceptedTargetZones: ['top', 'bottom', 'left', 'right', 'center'],
230
- overlayModel: (_f = options.rootOverlayModel) !== null && _f !== void 0 ? _f : DEFAULT_ROOT_OVERLAY_MODEL,
231
- getOverrideTarget: () => { var _a; return (_a = this.rootDropTargetContainer) === null || _a === void 0 ? void 0 : _a.model; },
232
- });
233
- this._rootPointerDropTarget = pointerBackend.createDropTarget(this.element, {
234
- className: 'dv-drop-target-edge',
235
- canDisplayOverlay: rootCanDisplayOverlay,
236
- acceptedTargetZones: [
237
- 'top',
238
- 'bottom',
239
- 'left',
240
- 'right',
241
- 'center',
242
- ],
243
- overlayModel: (_g = options.rootOverlayModel) !== null && _g !== void 0 ? _g : DEFAULT_ROOT_OVERLAY_MODEL,
244
- getOverrideTarget: () => { var _a; return (_a = this.rootDropTargetContainer) === null || _a === void 0 ? void 0 : _a.model; },
245
- });
246
- this.updateDropTargetModel(options);
247
476
  toggleClass(this.gridview.element, 'dv-dockview', true);
248
477
  toggleClass(this.element, 'dv-debug', !!options.debug);
249
478
  this.updateTheme();
250
- this.updateWatermark();
251
479
  if (options.debug) {
252
480
  this.addDisposables(new StrictEventsSequencing(this));
253
481
  }
254
- this.addDisposables(this.rootDropTargetContainer, this.overlayRenderContainer, this._onWillDragPanel, this._onWillDragGroup, this._onWillShowOverlay, this._onDidActivePanelChange, this._onDidAddPanel, this._onDidRemovePanel, this._onDidLayoutFromJSON, this._onDidDrop, this._onWillDrop, this._onDidMovePanel, this._onDidMovePanel.event(() => {
482
+ this.addDisposables(this.rootDropTargetContainer, this.overlayRenderContainer, this._onWillDragPanel, this._onWillDragGroup, this._onWillShowOverlay, this._onDidActivePanelChange, this._onDidAddPanel, this._onDidRemovePanel, this._onDidLayoutFromJSON, this._onDidDrop, this._onWillDrop, this._onWillMutateLayout, this._onDidMutateLayout, this._onDidMovePanel, this._onDidMovePanel.event(() => {
255
483
  /**
256
484
  * Update overlay positions after DOM layout completes to prevent 0×0 dimensions.
257
485
  * With defaultRenderer="always" this results in panel content not showing after move operations.
258
486
  * Debounced to avoid multiple calls when moving groups with multiple panels.
259
487
  */
260
488
  this.debouncedUpdateAllPositions();
261
- }), this._onDidAddGroup, this._onDidRemoveGroup, this._onDidActiveGroupChange, this._onUnhandledDragOverEvent, this._onDidMaximizedGroupChange, this._onDidOptionsChange, this._onDidPopoutGroupSizeChange, this._onDidPopoutGroupPositionChange, this._onDidOpenPopoutWindowFail, this._onDidCreateTabGroup, this._onDidDestroyTabGroup, this._onDidAddPanelToTabGroup, this._onDidRemovePanelFromTabGroup, this._onDidTabGroupChange, this._onDidTabGroupCollapsedChange, this.onDidViewVisibilityChangeMicroTaskQueue(() => {
262
- this.updateWatermark();
263
- }), this.onDidAdd((event) => {
489
+ }), this._onDidAddGroup, this._onDidRemoveGroup, this._onDidActiveGroupChange, this._onUnhandledDragOver, this._onDidMaximizedGroupChange, this._onDidPopoutGroupSizeChange, this._onDidPopoutGroupPositionChange, this._onDidAddPopoutGroup, this._onDidRemovePopoutGroup, this._onDidOpenPopoutWindowFail, this._onDidCreateTabGroup, this._onDidDestroyTabGroup, this._onDidAddPanelToTabGroup, this._onDidRemovePanelFromTabGroup, this._onDidTabGroupChange, this._onDidTabGroupCollapsedChange, Event.any(this.onDidPopoutGroupSizeChange, this.onDidPopoutGroupPositionChange, this.onDidCreateTabGroup, this.onDidDestroyTabGroup, this.onDidAddPanelToTabGroup, this.onDidRemovePanelFromTabGroup, this.onDidTabGroupChange, this.onDidTabGroupCollapsedChange)(() => {
490
+ // Popout size/position and tab-group mutations persist as layout changes.
491
+ this.fireLayoutChange();
492
+ }), this._onDidOptionsChange, this.onDidAdd((event) => {
264
493
  if (!this._moving) {
265
494
  this._onDidAddGroup.fire(event);
266
495
  }
@@ -277,84 +506,80 @@ export class DockviewComponent extends BaseGrid {
277
506
  group: event.panel,
278
507
  isMaximized: event.isMaximized,
279
508
  });
280
- }), Event.any(this.onDidAdd, this.onDidRemove)(() => {
281
- this.updateWatermark();
282
- }), Event.any(this.onDidAddPanel, this.onDidRemovePanel, this.onDidAddGroup, this.onDidRemove, this.onDidRemoveGroup, this.onDidMovePanel, this.onDidActivePanelChange, this.onDidPopoutGroupPositionChange, this.onDidPopoutGroupSizeChange, this.onDidCreateTabGroup, this.onDidDestroyTabGroup, this.onDidAddPanelToTabGroup, this.onDidRemovePanelFromTabGroup, this.onDidTabGroupChange, this.onDidTabGroupCollapsedChange)(() => {
509
+ }), Event.any(this.onDidAddPanel, this.onDidRemovePanel, this.onDidAddGroup, this.onDidRemove, this.onDidRemoveGroup, this.onDidMovePanel, this.onDidActivePanelChange)(() => {
283
510
  this._bufferOnDidLayoutChange.fire();
284
511
  }), Disposable.from(() => {
285
512
  var _a;
286
- // Cancel any pending popout-restoration timers scheduled by
287
- // fromJSON so they don't open new browser windows after
288
- // dispose, and resolve their promises so callers awaiting
289
- // popoutRestorationPromise don't hang. See issue #851.
290
- for (const cleanup of [...this._popoutRestorationCleanups]) {
291
- cleanup();
292
- }
293
- this._popoutRestorationCleanups.clear();
294
- // iterate over a copy of the array since .dispose() mutates the original array
295
- for (const group of [...this._floatingGroups]) {
296
- group.dispose();
297
- }
298
- // iterate over a copy of the array since .dispose() mutates the original array
299
- for (const group of [...this._popoutGroups]) {
300
- group.disposable.dispose();
301
- }
513
+ // Registry disposes init() subscriptions then every module
514
+ // service that implements IDisposable. The order matters so
515
+ // init handlers stop firing into services about to be torn
516
+ // down. Includes popout-restoration timer cancellation that
517
+ // resolves promises so awaiters of popoutRestorationPromise
518
+ // don't hang. See issue #851.
519
+ this._moduleRegistry.dispose();
302
520
  (_a = this._shellManager) === null || _a === void 0 ? void 0 : _a.dispose();
303
- for (const d of this._edgeGroupDisposables.values()) {
304
- d.dispose();
305
- }
306
- this._edgeGroupDisposables.clear();
307
- }), this._rootDropTarget, this._rootPointerDropTarget, Event.any(this._rootDropTarget.onWillShowOverlay, this._rootPointerDropTarget.onWillShowOverlay)((event) => {
308
- if (this.gridview.length > 0 && event.position === 'center') {
309
- // option only available when no panels in primary grid
310
- return;
311
- }
312
- this._onWillShowOverlay.fire(new DockviewWillShowOverlayLocationEvent(event, {
313
- kind: 'edge',
314
- panel: undefined,
315
- api: this._api,
316
- group: undefined,
317
- getData: getPanelData,
318
- }));
319
- }), Event.any(this._rootDropTarget.onDrop, this._rootPointerDropTarget.onDrop)((event) => {
320
- var _a;
321
- const willDropEvent = new DockviewWillDropEvent({
322
- nativeEvent: event.nativeEvent,
323
- position: event.position,
324
- panel: undefined,
325
- api: this._api,
326
- group: undefined,
327
- getData: getPanelData,
328
- kind: 'edge',
329
- });
330
- this._onWillDrop.fire(willDropEvent);
331
- if (willDropEvent.defaultPrevented) {
332
- return;
333
- }
334
- const data = getPanelData();
335
- if (data) {
336
- this.moveGroupOrPanel({
337
- from: {
338
- groupId: data.groupId,
339
- panelId: (_a = data.panelId) !== null && _a !== void 0 ? _a : undefined,
340
- },
341
- to: {
342
- group: this.orthogonalize(event.position),
343
- position: 'center',
344
- },
345
- });
346
- }
347
- else {
348
- this._onDidDrop.fire(new DockviewDidDropEvent({
521
+ }));
522
+ // Root edge-drop wiring lives with its (optional) module — guard it so
523
+ // the module is independently removable.
524
+ const rootDropTarget = this._rootDropTargetService;
525
+ if (rootDropTarget) {
526
+ this.addDisposables(rootDropTarget.onWillShowOverlay((event) => {
527
+ if (this.gridview.length > 0 &&
528
+ event.position === 'center') {
529
+ // option only available when no panels in primary grid
530
+ return;
531
+ }
532
+ this._onWillShowOverlay.fire(new DockviewWillShowOverlayLocationEvent(event, {
533
+ kind: 'edge',
534
+ panel: undefined,
535
+ api: this._api,
536
+ group: undefined,
537
+ getData: getPanelData,
538
+ }));
539
+ }), rootDropTarget.onDrop((event) => {
540
+ var _a;
541
+ const willDropEvent = new DockviewWillDropEvent({
349
542
  nativeEvent: event.nativeEvent,
350
543
  position: event.position,
351
544
  panel: undefined,
352
545
  api: this._api,
353
546
  group: undefined,
354
547
  getData: getPanelData,
355
- }));
356
- }
357
- }));
548
+ kind: 'edge',
549
+ });
550
+ this._onWillDrop.fire(willDropEvent);
551
+ if (willDropEvent.defaultPrevented) {
552
+ return;
553
+ }
554
+ const data = getPanelData();
555
+ if (data) {
556
+ this.moveGroupOrPanel({
557
+ from: {
558
+ groupId: data.groupId,
559
+ panelId: (_a = data.panelId) !== null && _a !== void 0 ? _a : undefined,
560
+ },
561
+ to: {
562
+ group: this.orthogonalize(event.position),
563
+ position: 'center',
564
+ },
565
+ });
566
+ }
567
+ else {
568
+ this._onDidDrop.fire(new DockviewDidDropEvent({
569
+ nativeEvent: event.nativeEvent,
570
+ position: event.position,
571
+ panel: undefined,
572
+ api: this._api,
573
+ group: undefined,
574
+ getData: getPanelData,
575
+ }));
576
+ }
577
+ }));
578
+ }
579
+ // Final module wiring: runs after the host is fully constructed.
580
+ // Modules subscribe to host events here so the component doesn't
581
+ // need to manually invoke them at scattered call sites.
582
+ this._moduleRegistry.postConstruct(this);
358
583
  }
359
584
  setVisible(panel, visible) {
360
585
  switch (panel.api.location.type) {
@@ -386,11 +611,29 @@ export class DockviewComponent extends BaseGrid {
386
611
  * and dismisses on events from that window.
387
612
  */
388
613
  getPopupServiceForGroup(group) {
389
- var _a;
390
- return (_a = this._popoutPopupServices.get(group.id)) !== null && _a !== void 0 ? _a : this.popupService;
614
+ var _a, _b;
615
+ return ((_b = (_a = this._popoutWindowService) === null || _a === void 0 ? void 0 : _a.getPopupService(group.id)) !== null && _b !== void 0 ? _b : this.popupService);
391
616
  }
392
617
  addPopoutGroup(itemToPopout, options) {
393
- var _a, _b, _c, _d, _e, _f;
618
+ // The transaction brackets the synchronous structural change; the
619
+ // popout window opens asynchronously after it resolves.
620
+ return this.mutation('popout', () => this._doAddPopoutGroup(itemToPopout, options));
621
+ }
622
+ /** Enumerate the popout groups currently open in their own windows. */
623
+ getPopouts() {
624
+ var _a, _b;
625
+ return ((_b = (_a = this._popoutWindowService) === null || _a === void 0 ? void 0 : _a.entries.map((entry) => ({
626
+ id: entry.popoutGroup.id,
627
+ group: entry.popoutGroup,
628
+ window: entry.getWindow(),
629
+ }))) !== null && _b !== void 0 ? _b : []);
630
+ }
631
+ _doAddPopoutGroup(itemToPopout, options) {
632
+ var _a, _b, _c, _d, _e;
633
+ const service = assertModule(this._popoutWindowService, 'PopoutWindow', 'api.addPopoutGroup');
634
+ if (!service) {
635
+ return Promise.resolve(false);
636
+ }
394
637
  if (itemToPopout instanceof DockviewGroupPanel &&
395
638
  itemToPopout.model.location.type === 'edge') {
396
639
  // edge groups are permanent structural elements and cannot be popped out
@@ -416,16 +659,21 @@ export class DockviewComponent extends BaseGrid {
416
659
  }
417
660
  const box = getBox();
418
661
  const groupId = (_b = (_a = options === null || options === void 0 ? void 0 : options.overridePopoutGroup) === null || _a === void 0 ? void 0 : _a.id) !== null && _b !== void 0 ? _b : this.getNextGroupId();
662
+ // Resolve the configured popout URL once (per-call override → global
663
+ // option). Recorded on the entry / group locations so it survives
664
+ // serialization; the '/popout.html' default is applied only when
665
+ // actually opening the window, not baked into saved layouts.
666
+ const resolvedPopoutUrl = (_c = options === null || options === void 0 ? void 0 : options.popoutUrl) !== null && _c !== void 0 ? _c : (_d = this.options) === null || _d === void 0 ? void 0 : _d.popoutUrl;
419
667
  const _window = new PopoutWindow(`${this.id}-${groupId}`, // unique id
420
668
  theme !== null && theme !== void 0 ? theme : '', {
421
- url: (_e = (_c = options === null || options === void 0 ? void 0 : options.popoutUrl) !== null && _c !== void 0 ? _c : (_d = this.options) === null || _d === void 0 ? void 0 : _d.popoutUrl) !== null && _e !== void 0 ? _e : '/popout.html',
669
+ url: resolvedPopoutUrl !== null && resolvedPopoutUrl !== void 0 ? resolvedPopoutUrl : '/popout.html',
422
670
  left: window.screenX + box.left,
423
671
  top: window.screenY + box.top,
424
672
  width: box.width,
425
673
  height: box.height,
426
674
  onDidOpen: options === null || options === void 0 ? void 0 : options.onDidOpen,
427
675
  onWillClose: options === null || options === void 0 ? void 0 : options.onWillClose,
428
- nonce: (_f = this.options) === null || _f === void 0 ? void 0 : _f.nonce,
676
+ nonce: (_e = this.options) === null || _e === void 0 ? void 0 : _e.nonce,
429
677
  });
430
678
  const popoutWindowDisposable = new CompositeDisposable(_window, _window.onDidClose(() => {
431
679
  popoutWindowDisposable.dispose();
@@ -433,15 +681,13 @@ export class DockviewComponent extends BaseGrid {
433
681
  return _window
434
682
  .open()
435
683
  .then((popoutContainer) => {
436
- var _a;
684
+ var _a, _b, _c, _d;
437
685
  if (_window.isDisposed) {
438
686
  return false;
439
687
  }
440
- const referenceGroup = (options === null || options === void 0 ? void 0 : options.referenceGroup)
441
- ? options.referenceGroup
442
- : itemToPopout instanceof DockviewPanel
443
- ? itemToPopout.group
444
- : itemToPopout;
688
+ const referenceGroup = (_a = options === null || options === void 0 ? void 0 : options.referenceGroup) !== null && _a !== void 0 ? _a : (itemToPopout instanceof DockviewPanel
689
+ ? itemToPopout.group
690
+ : itemToPopout);
445
691
  const referenceLocation = itemToPopout.api.location.type;
446
692
  /**
447
693
  * The group that is being added doesn't already exist within the DOM, the most likely occurrence
@@ -449,7 +695,12 @@ export class DockviewComponent extends BaseGrid {
449
695
  */
450
696
  const isGroupAddedToDom = referenceGroup.element.parentElement !== null;
451
697
  let group;
452
- if (!isGroupAddedToDom) {
698
+ if (options === null || options === void 0 ? void 0 : options.overridePopoutGridview) {
699
+ // Restoring a multi-group window: the anchor group is
700
+ // already built inside the supplied gridview.
701
+ group = (_b = options.overridePopoutGroup) !== null && _b !== void 0 ? _b : referenceGroup;
702
+ }
703
+ else if (!isGroupAddedToDom) {
453
704
  group = referenceGroup;
454
705
  }
455
706
  else if (options === null || options === void 0 ? void 0 : options.overridePopoutGroup) {
@@ -462,27 +713,46 @@ export class DockviewComponent extends BaseGrid {
462
713
  }
463
714
  }
464
715
  if (popoutContainer === null) {
465
- console.error('dockview: failed to create popout. perhaps you need to allow pop-ups for this website');
466
- popoutWindowDisposable.dispose();
467
- this._onDidOpenPopoutWindowFail.fire();
468
- // if the popout window was blocked, we need to move the group back to the reference group
469
- // and set it to visible
470
- this.movingLock(() => moveGroupWithoutDestroying({
471
- from: group,
472
- to: referenceGroup,
473
- }));
474
- if (!referenceGroup.api.isVisible) {
475
- referenceGroup.api.setVisible(true);
476
- }
716
+ this.handleBlockedPopout({
717
+ group,
718
+ referenceGroup,
719
+ options,
720
+ popoutWindowDisposable,
721
+ });
477
722
  return false;
478
723
  }
479
724
  const gready = document.createElement('div');
480
725
  gready.className = 'dv-overlay-render-container';
481
726
  const overlayRenderContainer = new OverlayRenderContainer(gready, this);
482
727
  group.model.renderContainer = overlayRenderContainer;
483
- group.layout(_window.window.innerWidth, _window.window.innerHeight);
728
+ // The popout window hosts its own gridview so it can grow into
729
+ // a nested splitview layout. The window starts with the single
730
+ // anchor group; further groups arrive via drag-and-drop. On
731
+ // restore a pre-populated gridview is supplied instead.
732
+ const popoutGridview = (_c = options === null || options === void 0 ? void 0 : options.overridePopoutGridview) !== null && _c !== void 0 ? _c : this.createNestedGridview();
733
+ if (!(options === null || options === void 0 ? void 0 : options.overridePopoutGridview)) {
734
+ popoutGridview.addView(group, Sizing.Distribute, [0]);
735
+ }
736
+ // Fill the popout window. Unlike the main grid (explicit px) and
737
+ // floating windows (CSS inside .dv-resize-container), the popout
738
+ // gridview has no sizing context, so without this it collapses
739
+ // to 0 height and nothing renders.
740
+ popoutGridview.element.style.width = '100%';
741
+ popoutGridview.element.style.height = '100%';
742
+ popoutGridview.layout(_window.window.innerWidth, _window.window.innerHeight);
743
+ // Guarded so the teardown's re-entrant paths (window close
744
+ // re-enters via the anchor's doRemoveGroup) never double-dispose.
745
+ let popoutGridviewDisposed = false;
746
+ const disposePopoutGridview = () => {
747
+ if (!popoutGridviewDisposed) {
748
+ popoutGridviewDisposed = true;
749
+ popoutGridview.dispose();
750
+ }
751
+ };
484
752
  let floatingBox;
485
- if (!(options === null || options === void 0 ? void 0 : options.overridePopoutGroup) && isGroupAddedToDom) {
753
+ if (!(options === null || options === void 0 ? void 0 : options.overridePopoutGroup) &&
754
+ !(options === null || options === void 0 ? void 0 : options.overridePopoutGridview) &&
755
+ isGroupAddedToDom) {
486
756
  if (itemToPopout instanceof DockviewPanel) {
487
757
  this.movingLock(() => {
488
758
  const panel = referenceGroup.model.removePanel(itemToPopout);
@@ -500,9 +770,9 @@ export class DockviewComponent extends BaseGrid {
500
770
  break;
501
771
  case 'floating':
502
772
  case 'popout':
503
- floatingBox = (_a = this._floatingGroups
773
+ floatingBox = (_d = this.floatingGroups
504
774
  .find((value) => value.group.api.id ===
505
- itemToPopout.api.id)) === null || _a === void 0 ? void 0 : _a.overlay.toJSON();
775
+ itemToPopout.api.id)) === null || _d === void 0 ? void 0 : _d.overlay.toJSON();
506
776
  this.removeGroup(referenceGroup);
507
777
  break;
508
778
  }
@@ -511,7 +781,7 @@ export class DockviewComponent extends BaseGrid {
511
781
  popoutContainer.classList.add('dv-dockview');
512
782
  popoutContainer.style.overflow = 'hidden';
513
783
  popoutContainer.appendChild(gready);
514
- popoutContainer.appendChild(group.element);
784
+ popoutContainer.appendChild(popoutGridview.element);
515
785
  const anchor = document.createElement('div');
516
786
  const dropTargetContainer = new DropTargetAnchorContainer(anchor, { disabled: this.rootDropTargetContainer.disabled });
517
787
  popoutContainer.appendChild(anchor);
@@ -522,20 +792,42 @@ export class DockviewComponent extends BaseGrid {
522
792
  // their pointerdown/keydown listeners fire on the right
523
793
  // window for outside-click and Escape dismissal.
524
794
  const popoutPopupService = new PopupService(popoutContainer, _window.window);
525
- this._popoutPopupServices.set(group.id, popoutPopupService);
795
+ service.setPopupService(group.id, popoutPopupService);
526
796
  popoutWindowDisposable.addDisposables(popoutPopupService, Disposable.from(() => {
527
- this._popoutPopupServices.delete(group.id);
797
+ service.deletePopupService(group.id);
528
798
  }));
529
799
  group.model.location = {
530
800
  type: 'popout',
531
801
  getWindow: () => _window.window,
532
- popoutUrl: options === null || options === void 0 ? void 0 : options.popoutUrl,
802
+ popoutUrl: resolvedPopoutUrl,
533
803
  };
804
+ if (options === null || options === void 0 ? void 0 : options.overridePopoutGridview) {
805
+ // Restored multi-group window. Wire every member (including
806
+ // the anchor) to this window's containers and popout
807
+ // location now that the gridview is attached and laid out —
808
+ // re-setting renderContainer forces a re-render at the right
809
+ // time so 'always'-rendered content positions in this
810
+ // window rather than where it was first created.
811
+ const members = this.groups.filter((candidate) => popoutGridview.element.contains(candidate.element));
812
+ for (const member of members) {
813
+ member.model.renderContainer = overlayRenderContainer;
814
+ member.model.dropTargetContainer = dropTargetContainer;
815
+ member.model.location = {
816
+ type: 'popout',
817
+ getWindow: () => _window.window,
818
+ popoutUrl: resolvedPopoutUrl,
819
+ };
820
+ }
821
+ }
534
822
  if (isGroupAddedToDom &&
535
823
  itemToPopout.api.location.type === 'grid') {
536
824
  itemToPopout.api.setVisible(false);
537
825
  }
538
826
  this.doSetGroupAndPanelActive(group);
827
+ const resizeObserverDisposable = service.observeGridviewSize(_window, popoutGridview, overlayRenderContainer);
828
+ if (resizeObserverDisposable) {
829
+ popoutWindowDisposable.addDisposables(resizeObserverDisposable);
830
+ }
539
831
  popoutWindowDisposable.addDisposables(group.api.onDidActiveChange((event) => {
540
832
  var _a;
541
833
  if (event.isActive) {
@@ -545,20 +837,28 @@ export class DockviewComponent extends BaseGrid {
545
837
  var _a;
546
838
  (_a = _window.window) === null || _a === void 0 ? void 0 : _a.focus();
547
839
  }));
548
- let returnedGroup;
840
+ // Holder so the close teardown (extracted below) can publish
841
+ // the group that was returned to the main grid back to the
842
+ // entry's `dispose()` contract.
843
+ const closeResult = {};
549
844
  const isValidReferenceGroup = isGroupAddedToDom &&
550
845
  referenceGroup &&
551
846
  this.getPanel(referenceGroup.id);
552
847
  const value = {
553
848
  window: _window,
554
849
  popoutGroup: group,
850
+ gridview: popoutGridview,
851
+ overlayRenderContainer,
852
+ dropTargetContainer,
853
+ getWindow: () => _window.window,
854
+ popoutUrl: resolvedPopoutUrl,
555
855
  referenceGroup: isValidReferenceGroup
556
856
  ? referenceGroup.id
557
857
  : undefined,
558
858
  disposable: {
559
859
  dispose: () => {
560
860
  popoutWindowDisposable.dispose();
561
- return returnedGroup;
861
+ return closeResult.returnedGroup;
562
862
  },
563
863
  },
564
864
  };
@@ -575,73 +875,23 @@ export class DockviewComponent extends BaseGrid {
575
875
  screenY: _window.window.screenX,
576
876
  group,
577
877
  });
578
- }),
579
- /**
580
- * ResizeObserver seems slow here, I do not know why but we don't need it
581
- * since we can reply on the window resize event as we will occupy the full
582
- * window dimensions
583
- */
584
- addDisposableListener(_window.window, 'resize', () => {
585
- group.layout(_window.window.innerWidth, _window.window.innerHeight);
586
- }), overlayRenderContainer, Disposable.from(() => {
587
- if (this.isDisposed) {
588
- return; // cleanup may run after instance is disposed
589
- }
590
- if (isGroupAddedToDom &&
591
- this.getPanel(referenceGroup.id)) {
592
- this.movingLock(() => moveGroupWithoutDestroying({
593
- from: group,
594
- to: referenceGroup,
595
- }));
596
- if (!referenceGroup.api.isVisible) {
597
- referenceGroup.api.setVisible(true);
598
- }
599
- if (this.getPanel(group.id)) {
600
- this.doRemoveGroup(group, {
601
- skipPopoutAssociated: true,
602
- });
603
- }
604
- }
605
- else if (this.getPanel(group.id)) {
606
- group.model.renderContainer =
607
- this.overlayRenderContainer;
608
- group.model.dropTargetContainer =
609
- this.rootDropTargetContainer;
610
- returnedGroup = group;
611
- const alreadyRemoved = !this._popoutGroups.find((p) => p.popoutGroup === group);
612
- if (alreadyRemoved) {
613
- /**
614
- * If this popout group was explicitly removed then we shouldn't run the additional
615
- * steps. To tell if the running of this disposable is the result of this popout group
616
- * being explicitly removed we can check if this popout group is still referenced in
617
- * the `this._popoutGroups` list.
618
- */
619
- return;
620
- }
621
- if (floatingBox) {
622
- this.addFloatingGroup(group, {
623
- height: floatingBox.height,
624
- width: floatingBox.width,
625
- position: floatingBox,
626
- });
627
- }
628
- else {
629
- this.doRemoveGroup(group, {
630
- skipDispose: true,
631
- skipActive: true,
632
- skipPopoutReturn: true,
633
- });
634
- group.model.location = { type: 'grid' };
635
- this.movingLock(() => {
636
- // suppress group add events since the group already exists
637
- this.doAddGroup(group, [0]);
638
- });
639
- }
640
- this.doSetGroupAndPanelActive(group);
641
- }
642
- }));
643
- this._popoutGroups.push(value);
644
- this.updateWatermark();
878
+ }), addDisposableListener(_window.window, 'resize', () => {
879
+ popoutGridview.layout(_window.window.innerWidth, _window.window.innerHeight);
880
+ }), overlayRenderContainer, Disposable.from(() => this.disposePopoutWindow({
881
+ group,
882
+ referenceGroup,
883
+ popoutGridview,
884
+ isGroupAddedToDom,
885
+ floatingBox,
886
+ disposePopoutGridview,
887
+ closeResult,
888
+ })));
889
+ service.add(value);
890
+ this._onDidAddPopoutGroup.fire({
891
+ id: value.popoutGroup.id,
892
+ group: value.popoutGroup,
893
+ window: value.getWindow(),
894
+ });
645
895
  return true;
646
896
  })
647
897
  .catch((err) => {
@@ -649,8 +899,189 @@ export class DockviewComponent extends BaseGrid {
649
899
  return false;
650
900
  });
651
901
  }
902
+ /**
903
+ * The popout window was blocked (e.g. by the browser's popup blocker —
904
+ * common when restoring popouts on load). Fall back gracefully so the
905
+ * group(s) end up valid and visible in the main grid rather than as
906
+ * orphans that later crash clear()/remove().
907
+ */
908
+ handleBlockedPopout(params) {
909
+ const { group, referenceGroup, options, popoutWindowDisposable } = params;
910
+ console.error('dockview: failed to create popout. perhaps you need to allow pop-ups for this website');
911
+ popoutWindowDisposable.dispose();
912
+ this._onDidOpenPopoutWindowFail.fire();
913
+ if (options === null || options === void 0 ? void 0 : options.overridePopoutGridview) {
914
+ // Restoring a multi-group popout window: its nested gridview was
915
+ // built up-front but never attached to a window. Dock every member
916
+ // into the main grid so no group is lost, then discard the
917
+ // detached gridview.
918
+ const blockedGridview = options.overridePopoutGridview;
919
+ const members = this.groups.filter((candidate) => blockedGridview.element.contains(candidate.element));
920
+ for (const member of members) {
921
+ this.movingLock(() => {
922
+ blockedGridview.remove(member);
923
+ this.redockGroupToMainGrid(member);
924
+ });
925
+ }
926
+ blockedGridview.dispose();
927
+ if (referenceGroup && !referenceGroup.api.isVisible) {
928
+ referenceGroup.api.setVisible(true);
929
+ }
930
+ return;
931
+ }
932
+ if (group === referenceGroup) {
933
+ // No separate grid group to return to (e.g. restoring a popout
934
+ // straight from JSON) — dock this group into the main grid.
935
+ if (!this.gridview.element.contains(group.element)) {
936
+ this.movingLock(() => this.doAddGroup(group, [0]));
937
+ group.model.location = { type: 'grid' };
938
+ }
939
+ }
940
+ else {
941
+ // A fresh group was created for the popout — return its panels to
942
+ // the reference group and discard the now-empty popout group so it
943
+ // doesn't linger as an orphan.
944
+ this.movingLock(() => moveGroupWithoutDestroying({
945
+ from: group,
946
+ to: referenceGroup,
947
+ }));
948
+ if (group.model.size === 0 && this._groups.has(group.id)) {
949
+ group.dispose();
950
+ this._groups.delete(group.id);
951
+ this._onDidRemoveGroup.fire(group);
952
+ }
953
+ }
954
+ if (!referenceGroup.api.isVisible) {
955
+ referenceGroup.api.setVisible(true);
956
+ }
957
+ }
958
+ /**
959
+ * Wire a group that has been displaced from a floating / popout window back
960
+ * to the main grid's render & drop-target containers and dock it at the
961
+ * root. The caller is responsible for first detaching it from its old
962
+ * gridview — the detach strategy differs between the window-teardown path
963
+ * (`doRemoveGroup`) and the blocked-window path (`gridview.remove`).
964
+ */
965
+ redockGroupToMainGrid(group) {
966
+ group.model.renderContainer = this.overlayRenderContainer;
967
+ group.model.dropTargetContainer = this.rootDropTargetContainer;
968
+ group.model.location = { type: 'grid' };
969
+ this.doAddGroup(group, [0]);
970
+ }
971
+ /**
972
+ * Teardown for a popout window's `popoutWindowDisposable`. Runs when the
973
+ * window closes (by user, by `removeGroup`, or by component disposal):
974
+ * relocates every member group back to the main grid (or to a floating
975
+ * window when the anchor came from one), then disposes the nested gridview.
976
+ * `closeResult.returnedGroup` is read by the entry's `dispose()` contract.
977
+ */
978
+ disposePopoutWindow(params) {
979
+ var _a;
980
+ const { group, referenceGroup, popoutGridview, isGroupAddedToDom, floatingBox, disposePopoutGridview, closeResult, } = params;
981
+ if (this.isDisposed) {
982
+ // cleanup may run after instance is disposed; just tear down the
983
+ // nested gridview.
984
+ disposePopoutGridview();
985
+ return;
986
+ }
987
+ // Distinguish a genuine window close from an explicit `removeGroup`:
988
+ // the explicit-removal paths remove the service entry *before* this
989
+ // disposable runs. Key off the stable `popoutGridview` rather than the
990
+ // captured `group`, which may no longer be the window's anchor (it can
991
+ // have been dragged out, promoting another member to anchor).
992
+ const genuineClose = !!((_a = this._popoutWindowService) === null || _a === void 0 ? void 0 : _a.entries.find((entry) => entry.gridview === popoutGridview));
993
+ // The groups still living in this window, resolved from the gridview so
994
+ // a reassigned anchor doesn't hide surviving members.
995
+ const members = this.groups.filter((candidate) => popoutGridview.element.contains(candidate.element));
996
+ const anchorPresent = members.includes(group);
997
+ const anchorIsSoleMember = anchorPresent && members.length === 1;
998
+ // On a genuine close, relocate every member that ISN'T the captured
999
+ // anchor back to the main grid. The captured anchor (if still here) gets
1000
+ // the reference-return / re-float treatment below. Explicit removal
1001
+ // relocates via its own path, so the loop is skipped for it.
1002
+ if (genuineClose) {
1003
+ for (const member of members) {
1004
+ if (member === group) {
1005
+ continue;
1006
+ }
1007
+ this.movingLock(() => {
1008
+ this.doRemoveGroup(member, {
1009
+ skipDispose: true,
1010
+ skipActive: true,
1011
+ skipPopoutReturn: true,
1012
+ });
1013
+ this.redockGroupToMainGrid(member);
1014
+ });
1015
+ }
1016
+ }
1017
+ if (anchorPresent &&
1018
+ isGroupAddedToDom &&
1019
+ this.getPanel(referenceGroup.id)) {
1020
+ this.movingLock(() => moveGroupWithoutDestroying({
1021
+ from: group,
1022
+ to: referenceGroup,
1023
+ }));
1024
+ if (!referenceGroup.api.isVisible) {
1025
+ referenceGroup.api.setVisible(true);
1026
+ }
1027
+ if (this.getPanel(group.id)) {
1028
+ this.doRemoveGroup(group, {
1029
+ skipPopoutAssociated: true,
1030
+ });
1031
+ }
1032
+ }
1033
+ else if (anchorPresent && this.getPanel(group.id)) {
1034
+ group.model.renderContainer = this.overlayRenderContainer;
1035
+ group.model.dropTargetContainer = this.rootDropTargetContainer;
1036
+ closeResult.returnedGroup = group;
1037
+ if (!genuineClose) {
1038
+ /**
1039
+ * If this popout group was explicitly removed then we shouldn't run the additional
1040
+ * steps. The explicit remover re-docks the returned group itself; here we only hand
1041
+ * it back (above) and tear down the nested gridview.
1042
+ */
1043
+ disposePopoutGridview();
1044
+ return;
1045
+ }
1046
+ // Re-float only restores the pre-popout state of a SINGLE popped-out
1047
+ // group. A multi-group window must not be split (anchor re-floats
1048
+ // while the rest dock to the grid), so dock the anchor to the grid
1049
+ // alongside the other members once they're no longer alone.
1050
+ if (floatingBox && anchorIsSoleMember) {
1051
+ this.addFloatingGroup(group, {
1052
+ height: floatingBox.height,
1053
+ width: floatingBox.width,
1054
+ position: floatingBox,
1055
+ });
1056
+ }
1057
+ else {
1058
+ this.doRemoveGroup(group, {
1059
+ skipDispose: true,
1060
+ skipActive: true,
1061
+ skipPopoutReturn: true,
1062
+ });
1063
+ group.model.location = { type: 'grid' };
1064
+ this.movingLock(() => {
1065
+ // suppress group add events since the group already exists
1066
+ this.doAddGroup(group, [0]);
1067
+ });
1068
+ }
1069
+ this.doSetGroupAndPanelActive(group);
1070
+ }
1071
+ // All members have been relocated out; tear down the window's nested
1072
+ // gridview (does not dispose the leaf views — their lifecycle stays
1073
+ // with `_groups`).
1074
+ disposePopoutGridview();
1075
+ }
652
1076
  addFloatingGroup(item, options) {
653
- var _a, _b, _c, _d, _e, _f;
1077
+ this.mutation('float', () => this._doAddFloatingGroup(item, options));
1078
+ }
1079
+ _doAddFloatingGroup(item, options) {
1080
+ var _a;
1081
+ const service = assertModule(this._floatingGroupService, 'FloatingGroup', 'api.addFloatingGroup');
1082
+ if (!service) {
1083
+ return;
1084
+ }
654
1085
  if (item instanceof DockviewGroupPanel &&
655
1086
  item.model.location.type === 'edge') {
656
1087
  // edge groups are permanent structural elements and cannot be floated
@@ -669,7 +1100,7 @@ export class DockviewComponent extends BaseGrid {
669
1100
  }
670
1101
  else {
671
1102
  group = item;
672
- const popoutReferenceGroupId = (_a = this._popoutGroups.find((_) => _.popoutGroup === group)) === null || _a === void 0 ? void 0 : _a.referenceGroup;
1103
+ const popoutReferenceGroupId = (_a = this._popoutWindowService) === null || _a === void 0 ? void 0 : _a.findReferenceGroupId(group);
673
1104
  const popoutReferenceGroup = popoutReferenceGroupId
674
1105
  ? this.getPanel(popoutReferenceGroupId)
675
1106
  : undefined;
@@ -750,65 +1181,87 @@ export class DockviewComponent extends BaseGrid {
750
1181
  };
751
1182
  }
752
1183
  const anchoredBox = getAnchoredBox();
753
- const overlay = new Overlay(Object.assign(Object.assign({ container: (_b = this._floatingOverlayHost) !== null && _b !== void 0 ? _b : this.gridview.element, content: group.element }, anchoredBox), { minimumInViewportWidth: this.options.floatingGroupBounds === 'boundedWithinViewport'
1184
+ // The floating window hosts its own gridview so it can grow into a
1185
+ // nested splitview layout. The window starts with the single anchor
1186
+ // group; further groups are added via drag-and-drop.
1187
+ const floatingGridview = this.createNestedGridview();
1188
+ floatingGridview.addView(group, Sizing.Distribute, [0]);
1189
+ this.mountFloatingWindow(floatingGridview, group, [group], anchoredBox, {
1190
+ dragHandle: options === null || options === void 0 ? void 0 : options.dragHandle,
1191
+ inDragMode: options === null || options === void 0 ? void 0 : options.inDragMode,
1192
+ skipActiveGroup: options === null || options === void 0 ? void 0 : options.skipActiveGroup,
1193
+ });
1194
+ }
1195
+ /**
1196
+ * Build an empty gridview configured to match the main grid's styling, for
1197
+ * hosting a nested layout inside a floating or popout window.
1198
+ */
1199
+ createNestedGridview(orientation = Orientation.HORIZONTAL) {
1200
+ var _a, _b;
1201
+ return new Gridview(true, this.options.hideBorders
1202
+ ? { separatorBorder: 'transparent' }
1203
+ : undefined, orientation, false, (_b = (_a = this.options.theme) === null || _a === void 0 ? void 0 : _a.gap) !== null && _b !== void 0 ? _b : 0);
1204
+ }
1205
+ /**
1206
+ * Wrap a (populated) floating gridview in an overlay window: title bar /
1207
+ * move handle, drag wiring, the floating-group service entry and the
1208
+ * `floating` location tag for every member group.
1209
+ */
1210
+ mountFloatingWindow(floatingGridview, anchorGroup, members, anchoredBox, options) {
1211
+ var _a, _b, _c, _d, _e, _f, _g, _h;
1212
+ const service = assertModule(this._floatingGroupService, 'FloatingGroup', 'api.addFloatingGroup');
1213
+ if (!service) {
1214
+ return;
1215
+ }
1216
+ const dragHandleMode = (_b = (_a = options === null || options === void 0 ? void 0 : options.dragHandle) !== null && _a !== void 0 ? _a : this.options.floatingGroupDragHandle) !== null && _b !== void 0 ? _b : 'titlebar';
1217
+ // `'titlebar'` renders a dedicated grab bar above the tab bar and uses
1218
+ // it as the move handle; `'tabbar'` falls back to moving via the
1219
+ // tab-bar void container.
1220
+ const titleBar = dragHandleMode === 'titlebar'
1221
+ ? new FloatingTitleBar(this, anchorGroup)
1222
+ : undefined;
1223
+ const overlay = new Overlay(Object.assign(Object.assign({ container: (_c = this._floatingOverlayHost) !== null && _c !== void 0 ? _c : this.gridview.element, content: floatingGridview.element, header: titleBar === null || titleBar === void 0 ? void 0 : titleBar.element }, anchoredBox), { minimumInViewportWidth: this.options.floatingGroupBounds === 'boundedWithinViewport'
754
1224
  ? undefined
755
- : ((_d = (_c = this.options.floatingGroupBounds) === null || _c === void 0 ? void 0 : _c.minimumWidthWithinViewport) !== null && _d !== void 0 ? _d : DEFAULT_FLOATING_GROUP_OVERFLOW_SIZE), minimumInViewportHeight: this.options.floatingGroupBounds === 'boundedWithinViewport'
1225
+ : ((_e = (_d = this.options.floatingGroupBounds) === null || _d === void 0 ? void 0 : _d.minimumWidthWithinViewport) !== null && _e !== void 0 ? _e : DEFAULT_FLOATING_GROUP_OVERFLOW_SIZE), minimumInViewportHeight: this.options.floatingGroupBounds === 'boundedWithinViewport'
756
1226
  ? undefined
757
- : ((_f = (_e = this.options.floatingGroupBounds) === null || _e === void 0 ? void 0 : _e.minimumHeightWithinViewport) !== null && _f !== void 0 ? _f : DEFAULT_FLOATING_GROUP_OVERFLOW_SIZE) }));
758
- const el = group.element.querySelector('.dv-void-container');
759
- if (!el) {
1227
+ : ((_g = (_f = this.options.floatingGroupBounds) === null || _f === void 0 ? void 0 : _f.minimumHeightWithinViewport) !== null && _g !== void 0 ? _g : DEFAULT_FLOATING_GROUP_OVERFLOW_SIZE), transformDragPosition: this.options.transformFloatingGroupDrag
1228
+ ? (context) => this.options.transformFloatingGroupDrag({
1229
+ group: anchorGroup,
1230
+ proposed: context.proposed,
1231
+ container: context.container,
1232
+ others: context.others,
1233
+ })
1234
+ : undefined, getSiblingBoxes: () => this._gatherFloatingGroupBoxes(anchorGroup) }));
1235
+ const dragHandle = (_h = titleBar === null || titleBar === void 0 ? void 0 : titleBar.element) !== null && _h !== void 0 ? _h : anchorGroup.element.querySelector('.dv-void-container');
1236
+ if (!dragHandle) {
760
1237
  throw new Error('dockview: failed to find drag handle');
761
1238
  }
762
- overlay.setupDrag(el, {
1239
+ overlay.setupDrag(dragHandle, {
763
1240
  inDragMode: typeof (options === null || options === void 0 ? void 0 : options.inDragMode) === 'boolean'
764
1241
  ? options.inDragMode
765
1242
  : false,
766
1243
  });
767
- const floatingGroupPanel = new DockviewFloatingGroupPanel(group, overlay);
768
- const disposable = new CompositeDisposable(group.api.onDidActiveChange((event) => {
769
- if (event.isActive) {
770
- overlay.bringToFront();
771
- }
772
- }), (() => {
773
- let lastWidth = -1;
774
- let lastHeight = -1;
775
- return watchElementResize(group.element, (entry) => {
776
- const width = Math.round(entry.contentRect.width);
777
- const height = Math.round(entry.contentRect.height);
778
- if (width === lastWidth && height === lastHeight) {
779
- return;
780
- }
781
- lastWidth = width;
782
- lastHeight = height;
783
- group.layout(width, height); // let the group know it's size is changing so it can fire events to the panel
784
- });
785
- })());
786
- floatingGroupPanel.addDisposables(overlay.onDidChange(() => {
787
- // this is either a resize or a move
788
- // to inform the panels .layout(...) the group with it's current size
789
- // don't care about resize since the above watcher handles that
790
- group.layout(group.width, group.height);
791
- }), overlay.onDidChangeEnd(() => {
792
- this._bufferOnDidLayoutChange.fire();
793
- }), group.onDidChange((event) => {
794
- overlay.setBounds({
795
- height: event === null || event === void 0 ? void 0 : event.height,
796
- width: event === null || event === void 0 ? void 0 : event.width,
797
- });
798
- }), {
799
- dispose: () => {
800
- disposable.dispose();
801
- remove(this._floatingGroups, floatingGroupPanel);
802
- group.model.location = { type: 'grid' };
803
- this.updateWatermark();
804
- },
805
- });
806
- this._floatingGroups.push(floatingGroupPanel);
807
- group.model.location = { type: 'floating' };
1244
+ const floatingGroupPanel = service.add(anchorGroup, overlay, floatingGridview);
1245
+ if (titleBar) {
1246
+ // Tie the title bar's lifetime to the floating window and surface
1247
+ // its redock drag through the same public `onWillDragGroup` event
1248
+ // the tab-bar handle uses. Register it so anchor reassignment (when
1249
+ // the original anchor leaves a multi-group window) retargets the
1250
+ // bar at a group that still lives here.
1251
+ floatingGroupPanel.setTitleBar(titleBar);
1252
+ floatingGroupPanel.addDisposables(titleBar, Disposable.from(() => floatingGroupPanel.setTitleBar(undefined)), titleBar.onDragStart((event) => {
1253
+ this._onWillDragGroup.fire({
1254
+ nativeEvent: event,
1255
+ group: floatingGroupPanel.group,
1256
+ });
1257
+ }));
1258
+ }
1259
+ for (const member of members) {
1260
+ member.model.location = { type: 'floating' };
1261
+ }
808
1262
  if (!(options === null || options === void 0 ? void 0 : options.skipActiveGroup)) {
809
- this.doSetGroupAndPanelActive(group);
1263
+ this.doSetGroupAndPanelActive(anchorGroup);
810
1264
  }
811
- this.updateWatermark();
812
1265
  }
813
1266
  orthogonalize(position, options) {
814
1267
  this.gridview.normalize();
@@ -847,29 +1300,8 @@ export class DockviewComponent extends BaseGrid {
847
1300
  updateOptions(options) {
848
1301
  var _a, _b, _c, _d, _e;
849
1302
  super.updateOptions(options);
850
- if ('floatingGroupBounds' in options) {
851
- for (const group of this._floatingGroups) {
852
- switch (options.floatingGroupBounds) {
853
- case 'boundedWithinViewport':
854
- group.overlay.minimumInViewportHeight = undefined;
855
- group.overlay.minimumInViewportWidth = undefined;
856
- break;
857
- case undefined:
858
- group.overlay.minimumInViewportHeight =
859
- DEFAULT_FLOATING_GROUP_OVERFLOW_SIZE;
860
- group.overlay.minimumInViewportWidth =
861
- DEFAULT_FLOATING_GROUP_OVERFLOW_SIZE;
862
- break;
863
- default:
864
- group.overlay.minimumInViewportHeight =
865
- (_a = options.floatingGroupBounds) === null || _a === void 0 ? void 0 : _a.minimumHeightWithinViewport;
866
- group.overlay.minimumInViewportWidth =
867
- (_b = options.floatingGroupBounds) === null || _b === void 0 ? void 0 : _b.minimumWidthWithinViewport;
868
- }
869
- group.overlay.setBounds();
870
- }
871
- }
872
- this.updateDropTargetModel(options);
1303
+ (_a = this._floatingGroupService) === null || _a === void 0 ? void 0 : _a.updateBounds(options);
1304
+ (_b = this._rootDropTargetService) === null || _b === void 0 ? void 0 : _b.setOptions(options);
873
1305
  const oldDisableDnd = this.options.disableDnd;
874
1306
  const oldDndStrategy = this.options.dndStrategy;
875
1307
  this._options = Object.assign(Object.assign({}, this.options), options);
@@ -885,17 +1317,10 @@ export class DockviewComponent extends BaseGrid {
885
1317
  if ('createRightHeaderActionComponent' in options ||
886
1318
  'createLeftHeaderActionComponent' in options ||
887
1319
  'createPrefixHeaderActionComponent' in options) {
888
- for (const group of this.groups) {
889
- group.model.updateHeaderActions();
890
- }
1320
+ (_c = this.headerActionsService) === null || _c === void 0 ? void 0 : _c.refreshAll();
891
1321
  }
892
1322
  if ('createWatermarkComponent' in options) {
893
- if (this._watermark) {
894
- this._watermark.element.parentElement.remove();
895
- (_d = (_c = this._watermark).dispose) === null || _d === void 0 ? void 0 : _d.call(_c);
896
- this._watermark = null;
897
- }
898
- this.updateWatermark();
1323
+ (_d = this._watermarkService) === null || _d === void 0 ? void 0 : _d.refresh();
899
1324
  for (const group of this.groups) {
900
1325
  group.model.refreshWatermark();
901
1326
  }
@@ -912,6 +1337,7 @@ export class DockviewComponent extends BaseGrid {
912
1337
  this._layoutFromShell(this.gridview.width, this.gridview.height);
913
1338
  }
914
1339
  layout(width, height, forceResize) {
1340
+ var _a, _b;
915
1341
  if (this._shellManager && !this._inShellLayout) {
916
1342
  this._shellManager.layout(width, height);
917
1343
  }
@@ -919,12 +1345,9 @@ export class DockviewComponent extends BaseGrid {
919
1345
  super.layout(width, height, forceResize);
920
1346
  }
921
1347
  this._syncFloatingOverlayHost();
922
- if (this._floatingGroups) {
923
- for (const floating of this._floatingGroups) {
924
- // ensure floting groups stay within visible boundaries
925
- floating.overlay.setBounds();
926
- }
927
- }
1348
+ // floatingGroupService may be undefined during super() (BaseGrid calls
1349
+ // layout(0, 0) before subclass field initialisers run).
1350
+ (_b = (_a = this._moduleRegistry) === null || _a === void 0 ? void 0 : _a.services.floatingGroupService) === null || _b === void 0 ? void 0 : _b.constrainBounds();
928
1351
  }
929
1352
  _syncFloatingOverlayHost() {
930
1353
  if (!this._floatingOverlayHost || !this._shellManager) {
@@ -952,27 +1375,32 @@ export class DockviewComponent extends BaseGrid {
952
1375
  }
953
1376
  }
954
1377
  addEdgeGroup(position, options) {
955
- if (this._edgeGroups.has(position)) {
1378
+ const service = assertModule(this._edgeGroupService, 'EdgeGroup', 'api.addEdgeGroup');
1379
+ if (!service) {
1380
+ throw new Error(`dockview: EdgeGroup module is not registered`);
1381
+ }
1382
+ if (service.has(position)) {
956
1383
  throw new Error(`dockview: edge group already exists at position '${position}'`);
957
1384
  }
958
- const group = this.createGroup({ id: options.id });
959
- group.model.location = { type: 'edge', position };
960
- group.model.headerPosition = position;
961
- this._edgeGroups.set(position, group);
962
- this._onDidAddGroup.fire(group);
963
- // Collapse when the group becomes empty
964
- const autoCollapseDisposable = group.model.onDidRemovePanel(() => {
965
- if (group.model.isEmpty) {
966
- this.setEdgeGroupCollapsed(group, true);
967
- }
1385
+ return this.mutation('add', () => {
1386
+ const group = this.createGroup({ id: options.id });
1387
+ group.model.location = { type: 'edge', position };
1388
+ group.model.headerPosition = position;
1389
+ // Collapse when the group becomes empty
1390
+ const autoCollapseDisposable = group.model.onDidRemovePanel(() => {
1391
+ if (group.model.isEmpty) {
1392
+ this.setEdgeGroupCollapsed(group, true);
1393
+ }
1394
+ });
1395
+ service.add(position, group, autoCollapseDisposable);
1396
+ this._onDidAddGroup.fire(group);
1397
+ this._shellManager.addEdgeView(position, options, group);
1398
+ return group.api;
968
1399
  });
969
- this._edgeGroupDisposables.set(position, autoCollapseDisposable);
970
- this._shellManager.addEdgeView(position, options, group);
971
- return group.api;
972
1400
  }
973
1401
  getEdgeGroup(position) {
974
- var _a;
975
- return (_a = this._edgeGroups.get(position)) === null || _a === void 0 ? void 0 : _a.api;
1402
+ var _a, _b;
1403
+ return (_b = (_a = this._edgeGroupService) === null || _a === void 0 ? void 0 : _a.get(position)) === null || _b === void 0 ? void 0 : _b.api;
976
1404
  }
977
1405
  setEdgeGroupVisible(position, visible) {
978
1406
  this._shellManager.setEdgeGroupVisible(position, visible);
@@ -981,54 +1409,54 @@ export class DockviewComponent extends BaseGrid {
981
1409
  return this._shellManager.isEdgeGroupVisible(position);
982
1410
  }
983
1411
  removeEdgeGroup(position) {
984
- var _a;
985
- const group = this._edgeGroups.get(position);
1412
+ const service = assertModule(this._edgeGroupService, 'EdgeGroup', 'api.removeEdgeGroup');
1413
+ if (!service) {
1414
+ return;
1415
+ }
1416
+ const group = service.get(position);
986
1417
  if (!group) {
987
1418
  throw new Error(`dockview: no edge group exists at position '${position}'`);
988
1419
  }
989
- // Remove panels inside the group first
990
- for (const panel of [...group.panels]) {
991
- this.removePanel(panel, {
992
- removeEmptyGroup: false,
993
- skipDispose: false,
994
- });
995
- }
996
- // Dispose the auto-collapse listener
997
- (_a = this._edgeGroupDisposables.get(position)) === null || _a === void 0 ? void 0 : _a.dispose();
998
- this._edgeGroupDisposables.delete(position);
999
- // Remove from the shell splitview
1000
- this._shellManager.removeEdgeView(position);
1001
- // Clean up group state
1002
- this._edgeGroups.delete(position);
1003
- group.dispose();
1004
- this._groups.delete(group.id);
1005
- this._onDidRemoveGroup.fire(group);
1006
- }
1007
- setEdgeGroupCollapsed(group, collapsed) {
1008
- for (const [position, edgeGroup] of this._edgeGroups) {
1009
- if (edgeGroup === group) {
1010
- if (this._shellManager.isEdgeGroupCollapsed(position) ===
1011
- collapsed) {
1012
- // Skip the splitview resize on a no-op: with non-zero
1013
- // theme gap, redundant resizeView calls accumulate
1014
- // rounding drift that gradually shrinks the group.
1015
- return;
1016
- }
1017
- this._shellManager.setEdgeGroupCollapsed(position, collapsed);
1018
- edgeGroup.api._onDidCollapsedChange.fire({
1019
- isCollapsed: collapsed,
1420
+ // One transaction — the per-panel removals below nest via the depth
1421
+ // counter, so consumers see a single edge-group removal.
1422
+ this.mutation('remove', () => {
1423
+ // Remove panels inside the group first
1424
+ for (const panel of [...group.panels]) {
1425
+ this.removePanel(panel, {
1426
+ removeEmptyGroup: false,
1427
+ skipDispose: false,
1020
1428
  });
1021
- return;
1022
1429
  }
1430
+ // Remove from the shell splitview
1431
+ this._shellManager.removeEdgeView(position);
1432
+ // Clean up service-tracked state + group itself
1433
+ service.remove(position);
1434
+ group.dispose();
1435
+ this._groups.delete(group.id);
1436
+ this._onDidRemoveGroup.fire(group);
1437
+ });
1438
+ }
1439
+ setEdgeGroupCollapsed(group, collapsed) {
1440
+ var _a;
1441
+ const position = (_a = this._edgeGroupService) === null || _a === void 0 ? void 0 : _a.findPositionOf(group);
1442
+ if (!position) {
1443
+ return;
1444
+ }
1445
+ if (this._shellManager.isEdgeGroupCollapsed(position) === collapsed) {
1446
+ // Skip the splitview resize on a no-op: with non-zero theme gap,
1447
+ // redundant resizeView calls accumulate rounding drift that
1448
+ // gradually shrinks the group.
1449
+ return;
1023
1450
  }
1451
+ this._shellManager.setEdgeGroupCollapsed(position, collapsed);
1452
+ group.api._onDidCollapsedChange.fire({ isCollapsed: collapsed });
1024
1453
  }
1025
1454
  isEdgeGroupCollapsed(group) {
1026
- for (const [position, edgeGroup] of this._edgeGroups) {
1027
- if (edgeGroup === group) {
1028
- return this._shellManager.isEdgeGroupCollapsed(position);
1029
- }
1030
- }
1031
- return false;
1455
+ var _a;
1456
+ const position = (_a = this._edgeGroupService) === null || _a === void 0 ? void 0 : _a.findPositionOf(group);
1457
+ return position
1458
+ ? this._shellManager.isEdgeGroupCollapsed(position)
1459
+ : false;
1032
1460
  }
1033
1461
  updateDragAndDropState() {
1034
1462
  // Update draggable state for all tabs and void containers
@@ -1092,32 +1520,18 @@ export class DockviewComponent extends BaseGrid {
1092
1520
  * @returns A JSON respresentation of the layout
1093
1521
  */
1094
1522
  toJSON() {
1095
- var _a;
1523
+ var _a, _b, _c, _d, _e, _f, _g, _h;
1096
1524
  const data = this.gridview.serialize();
1097
1525
  const panels = this.panels.reduce((collection, panel) => {
1098
1526
  collection[panel.id] = panel.toJSON();
1099
1527
  return collection;
1100
1528
  }, {});
1101
- const floats = this._floatingGroups.map((group) => {
1102
- return {
1103
- data: group.group.toJSON(),
1104
- position: group.overlay.toJSON(),
1105
- };
1106
- });
1107
- const popoutGroups = this._popoutGroups.map((group) => {
1108
- return {
1109
- data: group.popoutGroup.toJSON(),
1110
- gridReferenceGroup: group.referenceGroup,
1111
- position: group.window.dimensions(),
1112
- url: group.popoutGroup.api.location.type === 'popout'
1113
- ? group.popoutGroup.api.location.popoutUrl
1114
- : undefined,
1115
- };
1116
- });
1529
+ const floats = (_b = (_a = this._floatingGroupService) === null || _a === void 0 ? void 0 : _a.serialize()) !== null && _b !== void 0 ? _b : [];
1530
+ const popoutGroups = (_d = (_c = this._popoutWindowService) === null || _c === void 0 ? void 0 : _c.serialize()) !== null && _d !== void 0 ? _d : [];
1117
1531
  const result = {
1118
1532
  grid: data,
1119
1533
  panels,
1120
- activeGroup: (_a = this.activeGroup) === null || _a === void 0 ? void 0 : _a.id,
1534
+ activeGroup: (_e = this.activeGroup) === null || _e === void 0 ? void 0 : _e.id,
1121
1535
  };
1122
1536
  if (floats.length > 0) {
1123
1537
  result.floatingGroups = floats;
@@ -1125,10 +1539,11 @@ export class DockviewComponent extends BaseGrid {
1125
1539
  if (popoutGroups.length > 0) {
1126
1540
  result.popoutGroups = popoutGroups;
1127
1541
  }
1128
- if (this._edgeGroups.size > 0) {
1542
+ const edgeEntries = (_g = (_f = this._edgeGroupService) === null || _f === void 0 ? void 0 : _f.entries()) !== null && _g !== void 0 ? _g : [];
1543
+ if ((_h = this._edgeGroupService) === null || _h === void 0 ? void 0 : _h.hasAny()) {
1129
1544
  const shellSerialized = this._shellManager.toJSON();
1130
1545
  // Augment each entry with the serialized group state
1131
- for (const [position, group] of this._edgeGroups) {
1546
+ for (const [position, group] of edgeEntries) {
1132
1547
  const entry = shellSerialized[position];
1133
1548
  if (entry) {
1134
1549
  entry.group = group.toJSON();
@@ -1139,17 +1554,19 @@ export class DockviewComponent extends BaseGrid {
1139
1554
  return result;
1140
1555
  }
1141
1556
  fromJSON(data, options) {
1142
- var _a, _b, _c;
1557
+ // One 'load' transaction for the whole deserialization — the many
1558
+ // nested add/remove mutations join it via the depth counter.
1559
+ this.mutation('load', () => this._doFromJSON(data, options));
1560
+ }
1561
+ _doFromJSON(data, options) {
1562
+ var _a, _b, _c, _d, _e, _f, _g;
1143
1563
  // Cancel any popout-restoration timers queued by a previous fromJSON
1144
- // that haven't fired yet. Each cleanup also disposes the orphan group
1145
- // that was registered in _groups synchronously but never parented
1146
- // into a popout window — otherwise the upcoming clear() would call
1147
- // gridview.remove() on an unparented element and throw
1148
- // "Invalid grid element". See issue #1304.
1149
- for (const cleanup of [...this._popoutRestorationCleanups]) {
1150
- cleanup();
1151
- }
1152
- this._popoutRestorationCleanups.clear();
1564
+ // that haven't fired yet. The cancel path also disposes orphan groups
1565
+ // registered in _groups synchronously but never parented into a popout
1566
+ // window — otherwise the upcoming clear() would call gridview.remove()
1567
+ // on an unparented element and throw "Invalid grid element". See
1568
+ // issue #1304.
1569
+ (_a = this._popoutWindowService) === null || _a === void 0 ? void 0 : _a.cancelPendingRestorations();
1153
1570
  const existingPanels = new Map();
1154
1571
  let tempGroup;
1155
1572
  if (options === null || options === void 0 ? void 0 : options.reuseExistingPanels) {
@@ -1198,7 +1615,7 @@ export class DockviewComponent extends BaseGrid {
1198
1615
  const createGroupFromSerializedState = (data) => {
1199
1616
  const { id, locked, hideHeader, headerPosition, views, activeView, } = data;
1200
1617
  if (typeof id !== 'string') {
1201
- throw new Error('dockview: group id must be of type string');
1618
+ throw new TypeError('dockview: group id must be of type string');
1202
1619
  }
1203
1620
  const group = this.createGroup({
1204
1621
  id,
@@ -1266,138 +1683,23 @@ export class DockviewComponent extends BaseGrid {
1266
1683
  });
1267
1684
  this._layoutFromShell(width, height);
1268
1685
  if (data.edgeGroups) {
1269
- // Auto-create edge groups for positions in the serialized state
1270
- // that don't already have a group registered (e.g. when fromJSON
1271
- // is called before the user has called addEdgeGroup).
1272
- for (const _position of [
1273
- 'top',
1274
- 'bottom',
1275
- 'left',
1276
- 'right',
1277
- ]) {
1278
- const fixedData = data.edgeGroups[_position];
1279
- if (fixedData && !this._edgeGroups.has(_position)) {
1280
- const groupState = fixedData.group;
1281
- const id = (_a = groupState === null || groupState === void 0 ? void 0 : groupState.id) !== null && _a !== void 0 ? _a : `${_position}-group`;
1282
- this.addEdgeGroup(_position, { id });
1283
- }
1284
- }
1285
- // Restore panel contents of edge groups
1286
- for (const [position, edgeGroup] of this._edgeGroups) {
1287
- const edgeData = data.edgeGroups[position];
1288
- const groupState = edgeData === null || edgeData === void 0 ? void 0 : edgeData.group;
1289
- if (groupState) {
1290
- const { views, activeView } = groupState;
1291
- const createdPanels = [];
1292
- for (const panelId of views) {
1293
- if (panels[panelId]) {
1294
- const panel = this._deserializer.fromJSON(panels[panelId], edgeGroup);
1295
- createdPanels.push(panel);
1296
- }
1297
- }
1298
- for (let i = 0; i < createdPanels.length; i++) {
1299
- const panel = createdPanels[i];
1300
- const isActive = activeView === panel.id;
1301
- edgeGroup.model.openPanel(panel, {
1302
- skipSetActive: !isActive,
1303
- skipSetGroupActive: true,
1304
- });
1305
- }
1306
- // Restore tab groups before activating a fallback panel
1307
- if (groupState.tabGroups &&
1308
- groupState.tabGroups.length > 0) {
1309
- edgeGroup.model.restoreTabGroups(groupState.tabGroups);
1310
- }
1311
- if (!edgeGroup.activePanel &&
1312
- edgeGroup.panels.length > 0) {
1313
- edgeGroup.model.openPanel(edgeGroup.panels[edgeGroup.panels.length - 1], { skipSetGroupActive: true });
1314
- }
1315
- }
1316
- }
1317
- this._shellManager.fromJSON(data.edgeGroups);
1318
- }
1319
- const serializedFloatingGroups = (_b = data.floatingGroups) !== null && _b !== void 0 ? _b : [];
1320
- for (const serializedFloatingGroup of serializedFloatingGroups) {
1321
- const { data, position } = serializedFloatingGroup;
1322
- const group = createGroupFromSerializedState(data);
1323
- this.addFloatingGroup(group, {
1324
- position: position,
1325
- width: position.width,
1326
- height: position.height,
1327
- skipRemoveGroup: true,
1328
- inDragMode: false,
1329
- });
1330
- }
1331
- const serializedPopoutGroups = (_c = data.popoutGroups) !== null && _c !== void 0 ? _c : [];
1332
- // Create a promise that resolves when all popout groups are created
1333
- const popoutPromises = [];
1334
- // Queue popup group creation with delays to avoid browser blocking
1335
- serializedPopoutGroups.forEach((serializedPopoutGroup, index) => {
1336
- const { data, position, gridReferenceGroup, url } = serializedPopoutGroup;
1337
- const group = createGroupFromSerializedState(data);
1338
- // Add a small delay for each popup after the first to avoid browser popup blocking
1339
- const popoutPromise = new Promise((resolve) => {
1340
- const cleanup = () => {
1341
- this._popoutRestorationCleanups.delete(cleanup);
1342
- clearTimeout(handle);
1343
- // The group was registered in _groups synchronously
1344
- // but the timer that would parent it into the popout
1345
- // window never ran. Dispose the orphan here so the
1346
- // next clear() doesn't trip over an unparented
1347
- // element. See issue #1304.
1348
- if (!this.isDisposed &&
1349
- this._groups.has(group.id) &&
1350
- group.element.parentElement === null) {
1351
- for (const panel of [...group.panels]) {
1352
- this.removePanel(panel, {
1353
- removeEmptyGroup: false,
1354
- });
1355
- }
1356
- group.dispose();
1357
- this._groups.delete(group.id);
1358
- this._onDidRemoveGroup.fire(group);
1359
- }
1360
- resolve();
1361
- };
1362
- const handle = setTimeout(() => {
1363
- this._popoutRestorationCleanups.delete(cleanup);
1364
- // Guard against the component being disposed before
1365
- // this timer fires. Under React StrictMode the
1366
- // component is mounted -> disposed -> remounted, and
1367
- // without this guard the first instance's queued
1368
- // restoration would open a second popout window.
1369
- // See issue #851.
1370
- if (this.isDisposed) {
1371
- resolve();
1372
- return;
1373
- }
1374
- this.addPopoutGroup(group, {
1375
- position: position !== null && position !== void 0 ? position : undefined,
1376
- overridePopoutGroup: gridReferenceGroup
1377
- ? group
1378
- : undefined,
1379
- referenceGroup: gridReferenceGroup
1380
- ? this.getPanel(gridReferenceGroup)
1381
- : undefined,
1382
- popoutUrl: url,
1383
- });
1384
- resolve();
1385
- }, index * DESERIALIZATION_POPOUT_DELAY_MS); // 100ms delay between each popup
1386
- this._popoutRestorationCleanups.add(cleanup);
1387
- });
1388
- popoutPromises.push(popoutPromise);
1389
- });
1390
- // Store the promise for tests to wait on
1391
- this._popoutRestorationPromise = Promise.all(popoutPromises).then(() => void 0);
1392
- for (const floatingGroup of this._floatingGroups) {
1393
- floatingGroup.overlay.setBounds();
1686
+ this.deserializeEdgeGroups(data.edgeGroups, panels);
1394
1687
  }
1688
+ this.deserializeFloatingWindows((_b = data.floatingGroups) !== null && _b !== void 0 ? _b : [], createGroupFromSerializedState);
1689
+ const popoutPromises = this.deserializePopoutWindows((_c = data.popoutGroups) !== null && _c !== void 0 ? _c : [], createGroupFromSerializedState);
1690
+ (_d = this._popoutWindowService) === null || _d === void 0 ? void 0 : _d.finishRestoration(popoutPromises);
1691
+ (_e = this._floatingGroupService) === null || _e === void 0 ? void 0 : _e.constrainBounds();
1395
1692
  if (typeof activeGroup === 'string') {
1396
1693
  const panel = this.getPanel(activeGroup);
1397
1694
  if (panel) {
1398
1695
  this.doSetGroupAndPanelActive(panel);
1399
1696
  }
1400
1697
  }
1698
+ // `gridview.deserialize()` rebuilds the grid without firing the
1699
+ // BaseGrid add events the watermark module reacts to, so the
1700
+ // watermark mounted during `clear()` would otherwise persist over
1701
+ // the restored layout. Re-evaluate now the layout is in place.
1702
+ (_f = this._watermarkService) === null || _f === void 0 ? void 0 : _f.update();
1401
1703
  }
1402
1704
  catch (err) {
1403
1705
  console.error('dockview: failed to deserialize layout. Reverting changes', err);
@@ -1421,10 +1723,7 @@ export class DockviewComponent extends BaseGrid {
1421
1723
  this._groups.delete(group.id);
1422
1724
  this._onDidRemoveGroup.fire(group);
1423
1725
  }
1424
- // iterate over a reassigned array since original array will be modified
1425
- for (const floatingGroup of [...this._floatingGroups]) {
1426
- floatingGroup.dispose();
1427
- }
1726
+ (_g = this._floatingGroupService) === null || _g === void 0 ? void 0 : _g.disposeAll();
1428
1727
  // fires clean-up events and clears the underlying HTML gridview.
1429
1728
  this.clear();
1430
1729
  /**
@@ -1434,16 +1733,178 @@ export class DockviewComponent extends BaseGrid {
1434
1733
  */
1435
1734
  throw err;
1436
1735
  }
1437
- this.updateWatermark();
1438
1736
  // Force position updates for always visible panels after DOM layout is complete
1439
1737
  this.debouncedUpdateAllPositions();
1440
1738
  this._onDidLayoutFromJSON.fire();
1441
1739
  }
1740
+ /**
1741
+ * Rebuild a floating / popout window's nested gridview from its serialized
1742
+ * tree, collecting the member groups (in deserialization order) so the
1743
+ * caller can mount or restore the window.
1744
+ */
1745
+ deserializeNestedGridview(grid, createGroup) {
1746
+ const gridview = this.createNestedGridview(grid.orientation);
1747
+ const members = [];
1748
+ gridview.deserialize(grid, {
1749
+ fromJSON: (node) => {
1750
+ const group = createGroup(node.data);
1751
+ members.push(group);
1752
+ return group;
1753
+ },
1754
+ });
1755
+ return { gridview, members };
1756
+ }
1757
+ deserializeEdgeGroups(edgeGroups, panels) {
1758
+ var _a;
1759
+ const edgeService = assertModule(this._edgeGroupService, 'EdgeGroup', 'fromJSON edge restoration');
1760
+ if (!edgeService) {
1761
+ return;
1762
+ }
1763
+ // Auto-create edge groups for positions in the serialized state that
1764
+ // don't already have a group registered (e.g. when fromJSON is called
1765
+ // before the user has called addEdgeGroup).
1766
+ for (const _position of [
1767
+ 'top',
1768
+ 'bottom',
1769
+ 'left',
1770
+ 'right',
1771
+ ]) {
1772
+ const fixedData = edgeGroups[_position];
1773
+ if (fixedData && !edgeService.has(_position)) {
1774
+ const groupState = fixedData.group;
1775
+ const id = (_a = groupState === null || groupState === void 0 ? void 0 : groupState.id) !== null && _a !== void 0 ? _a : `${_position}-group`;
1776
+ this.addEdgeGroup(_position, { id });
1777
+ }
1778
+ }
1779
+ // Restore panel contents of edge groups
1780
+ for (const [position, edgeGroup] of edgeService.entries()) {
1781
+ const edgeData = edgeGroups[position];
1782
+ const groupState = edgeData === null || edgeData === void 0 ? void 0 : edgeData.group;
1783
+ if (groupState) {
1784
+ const { views, activeView } = groupState;
1785
+ const createdPanels = [];
1786
+ for (const panelId of views) {
1787
+ if (panels[panelId]) {
1788
+ const panel = this._deserializer.fromJSON(panels[panelId], edgeGroup);
1789
+ createdPanels.push(panel);
1790
+ }
1791
+ }
1792
+ for (const panel of createdPanels) {
1793
+ const isActive = activeView === panel.id;
1794
+ edgeGroup.model.openPanel(panel, {
1795
+ skipSetActive: !isActive,
1796
+ skipSetGroupActive: true,
1797
+ });
1798
+ }
1799
+ // Restore tab groups before activating a fallback panel
1800
+ if (groupState.tabGroups && groupState.tabGroups.length > 0) {
1801
+ edgeGroup.model.restoreTabGroups(groupState.tabGroups);
1802
+ }
1803
+ if (!edgeGroup.activePanel && edgeGroup.panels.length > 0) {
1804
+ edgeGroup.model.openPanel(edgeGroup.panels[edgeGroup.panels.length - 1], { skipSetGroupActive: true });
1805
+ }
1806
+ }
1807
+ }
1808
+ this._shellManager.fromJSON(edgeGroups);
1809
+ }
1810
+ deserializeFloatingWindows(serialized, createGroup) {
1811
+ for (const serializedFloatingGroup of serialized) {
1812
+ const { data, grid, position } = serializedFloatingGroup;
1813
+ if (grid) {
1814
+ // Multi-group window: rebuild the window's nested gridview from
1815
+ // its serialized tree.
1816
+ const { gridview: floatingGridview, members } = this.deserializeNestedGridview(grid, createGroup);
1817
+ if (members.length === 0) {
1818
+ continue;
1819
+ }
1820
+ this.mountFloatingWindow(floatingGridview, members[0], members, position, { inDragMode: false });
1821
+ }
1822
+ else if (data) {
1823
+ const group = createGroup(data);
1824
+ this.addFloatingGroup(group, {
1825
+ position: position,
1826
+ width: position.width,
1827
+ height: position.height,
1828
+ skipRemoveGroup: true,
1829
+ inDragMode: false,
1830
+ });
1831
+ }
1832
+ }
1833
+ }
1834
+ deserializePopoutWindows(serialized, createGroup) {
1835
+ const popoutService = serialized.length > 0
1836
+ ? assertModule(this._popoutWindowService, 'PopoutWindow', 'fromJSON popout restoration')
1837
+ : this._popoutWindowService;
1838
+ if (!popoutService) {
1839
+ return [];
1840
+ }
1841
+ // Queue popup group creation with delays to avoid browser blocking
1842
+ return serialized.flatMap((serializedPopoutGroup, index) => {
1843
+ const { data, grid, position, gridReferenceGroup, url } = serializedPopoutGroup;
1844
+ // Multi-group popout windows rebuild their nested gridview from the
1845
+ // serialized tree; single-group windows use the legacy single-group
1846
+ // path.
1847
+ let overridePopoutGridview;
1848
+ let members = [];
1849
+ if (grid) {
1850
+ const built = this.deserializeNestedGridview(grid, createGroup);
1851
+ overridePopoutGridview = built.gridview;
1852
+ members = built.members;
1853
+ if (members.length === 0) {
1854
+ // A serialized window with no groups: nothing to restore.
1855
+ // Mirror the floating path's guard and discard the empty
1856
+ // gridview rather than passing an undefined anchor to
1857
+ // addPopoutGroup.
1858
+ overridePopoutGridview.dispose();
1859
+ return [];
1860
+ }
1861
+ }
1862
+ const group = grid ? members[0] : createGroup(data);
1863
+ return popoutService.scheduleRestoration(index * DESERIALIZATION_POPOUT_DELAY_MS, () => {
1864
+ this.addPopoutGroup(group, {
1865
+ position: position !== null && position !== void 0 ? position : undefined,
1866
+ overridePopoutGroup: gridReferenceGroup
1867
+ ? group
1868
+ : undefined,
1869
+ overridePopoutGridview,
1870
+ referenceGroup: gridReferenceGroup
1871
+ ? this.getPanel(gridReferenceGroup)
1872
+ : undefined,
1873
+ popoutUrl: url,
1874
+ });
1875
+ }, () => {
1876
+ // The group was registered in _groups synchronously but the
1877
+ // timer that would parent it into the popout window never
1878
+ // ran. Dispose the orphan here so the next clear() doesn't
1879
+ // trip over an unparented element. See issue #1304.
1880
+ for (const orphan of members.length > 0
1881
+ ? members
1882
+ : [group]) {
1883
+ if (!this.isDisposed &&
1884
+ this._groups.has(orphan.id) &&
1885
+ orphan.element.parentElement === null) {
1886
+ for (const panel of [...orphan.panels]) {
1887
+ this.removePanel(panel, {
1888
+ removeEmptyGroup: false,
1889
+ });
1890
+ }
1891
+ orphan.dispose();
1892
+ this._groups.delete(orphan.id);
1893
+ this._onDidRemoveGroup.fire(orphan);
1894
+ }
1895
+ }
1896
+ });
1897
+ });
1898
+ }
1442
1899
  clear() {
1900
+ this.mutation('clear', () => this._doClear());
1901
+ }
1902
+ _doClear() {
1903
+ var _a;
1443
1904
  const groups = Array.from(this._groups.values()).map((_) => _.value);
1444
1905
  const hasActiveGroup = !!this.activeGroup;
1445
1906
  for (const group of groups) {
1446
- if ([...this._edgeGroups.values()].includes(group)) {
1907
+ if ((_a = this._edgeGroupService) === null || _a === void 0 ? void 0 : _a.includes(group)) {
1447
1908
  // Edge groups are structural - only clear their panels, not the group itself
1448
1909
  const panels = [...group.panels];
1449
1910
  for (const panel of panels) {
@@ -1460,14 +1921,21 @@ export class DockviewComponent extends BaseGrid {
1460
1921
  this.gridview.clear();
1461
1922
  }
1462
1923
  closeAllGroups() {
1463
- for (const entry of this._groups.entries()) {
1464
- const [_, group] = entry;
1465
- group.value.model.closeAllPanels();
1466
- }
1924
+ // One transaction — the per-panel removals inside nest via the depth
1925
+ // counter, so consumers (undo, announcements) see a single mutation.
1926
+ this.mutation('remove', () => {
1927
+ for (const entry of this._groups.entries()) {
1928
+ const [_, group] = entry;
1929
+ group.value.model.closeAllPanels();
1930
+ }
1931
+ });
1467
1932
  }
1468
1933
  addPanel(options) {
1469
- var _a, _b;
1470
- if (this.panels.find((_) => _.id === options.id)) {
1934
+ return this.mutation('add', () => this._doAddPanel(options));
1935
+ }
1936
+ _doAddPanel(options) {
1937
+ var _a, _b, _c, _d;
1938
+ if (this.panels.some((_) => _.id === options.id)) {
1471
1939
  throw new Error(`dockview: panel with id ${options.id} already exists`);
1472
1940
  }
1473
1941
  let referenceGroup;
@@ -1486,18 +1954,24 @@ export class DockviewComponent extends BaseGrid {
1486
1954
  : options.position.referencePanel;
1487
1955
  index = options.position.index;
1488
1956
  if (!referencePanel) {
1489
- throw new Error(`dockview: referencePanel '${options.position.referencePanel}' does not exist`);
1957
+ const referenceId = typeof options.position.referencePanel === 'string'
1958
+ ? options.position.referencePanel
1959
+ : (_a = options.position.referencePanel) === null || _a === void 0 ? void 0 : _a.id;
1960
+ throw new Error(`dockview: referencePanel '${referenceId}' does not exist`);
1490
1961
  }
1491
1962
  referenceGroup = this.findGroup(referencePanel);
1492
1963
  }
1493
1964
  else if (isPanelOptionsWithGroup(options.position)) {
1494
1965
  referenceGroup =
1495
1966
  typeof options.position.referenceGroup === 'string'
1496
- ? (_a = this._groups.get(options.position.referenceGroup)) === null || _a === void 0 ? void 0 : _a.value
1967
+ ? (_b = this._groups.get(options.position.referenceGroup)) === null || _b === void 0 ? void 0 : _b.value
1497
1968
  : options.position.referenceGroup;
1498
1969
  index = options.position.index;
1499
1970
  if (!referenceGroup) {
1500
- throw new Error(`dockview: referenceGroup '${options.position.referenceGroup}' does not exist`);
1971
+ const referenceId = typeof options.position.referenceGroup === 'string'
1972
+ ? options.position.referenceGroup
1973
+ : (_c = options.position.referenceGroup) === null || _c === void 0 ? void 0 : _c.id;
1974
+ throw new Error(`dockview: referenceGroup '${referenceId}' does not exist`);
1501
1975
  }
1502
1976
  }
1503
1977
  else {
@@ -1523,7 +1997,7 @@ export class DockviewComponent extends BaseGrid {
1523
1997
  }
1524
1998
  let panel;
1525
1999
  if (referenceGroup) {
1526
- const target = toTarget(((_b = options.position) === null || _b === void 0 ? void 0 : _b.direction) || 'within');
2000
+ const target = toTarget(((_d = options.position) === null || _d === void 0 ? void 0 : _d.direction) || 'within');
1527
2001
  if (options.floating) {
1528
2002
  const group = this.createGroup();
1529
2003
  this._onDidAddGroup.fire(group);
@@ -1605,9 +2079,17 @@ export class DockviewComponent extends BaseGrid {
1605
2079
  }
1606
2080
  return panel;
1607
2081
  }
1608
- removePanel(panel, options = {
1609
- removeEmptyGroup: true,
1610
- }) {
2082
+ removePanel(panel, options) {
2083
+ this.mutation('remove', () => {
2084
+ var _a;
2085
+ return this._doRemovePanel(panel, {
2086
+ removeEmptyGroup: (_a = options === null || options === void 0 ? void 0 : options.removeEmptyGroup) !== null && _a !== void 0 ? _a : true,
2087
+ skipDispose: options === null || options === void 0 ? void 0 : options.skipDispose,
2088
+ skipSetActiveGroup: options === null || options === void 0 ? void 0 : options.skipSetActiveGroup,
2089
+ });
2090
+ });
2091
+ }
2092
+ _doRemovePanel(panel, options) {
1611
2093
  const group = panel.group;
1612
2094
  if (!group) {
1613
2095
  throw new Error(`dockview: cannot remove panel ${panel.id}. it's missing a group.`);
@@ -1629,50 +2111,38 @@ export class DockviewComponent extends BaseGrid {
1629
2111
  }
1630
2112
  return new Watermark();
1631
2113
  }
1632
- updateWatermark() {
1633
- var _a, _b;
1634
- if (this.groups.filter((x) => x.api.location.type === 'grid' && x.api.isVisible).length === 0) {
1635
- if (!this._watermark) {
1636
- this._watermark = this.createWatermarkComponent();
1637
- this._watermark.init({
1638
- containerApi: new DockviewApi(this),
1639
- });
1640
- const watermarkContainer = document.createElement('div');
1641
- watermarkContainer.className = 'dv-watermark-container';
1642
- addTestId(watermarkContainer, 'watermark-component');
1643
- watermarkContainer.appendChild(this._watermark.element);
1644
- this.gridview.element.appendChild(watermarkContainer);
1645
- }
1646
- }
1647
- else if (this._watermark) {
1648
- this._watermark.element.parentElement.remove();
1649
- (_b = (_a = this._watermark).dispose) === null || _b === void 0 ? void 0 : _b.call(_a);
1650
- this._watermark = null;
1651
- }
1652
- }
1653
2114
  addGroup(options) {
1654
- var _a;
2115
+ return this.mutation('add', () => this._doAddGroup(options));
2116
+ }
2117
+ _doAddGroup(options) {
2118
+ var _a, _b, _c;
1655
2119
  if (options) {
1656
2120
  let referenceGroup;
1657
2121
  if (isGroupOptionsWithPanel(options)) {
1658
2122
  const referencePanel = typeof options.referencePanel === 'string'
1659
2123
  ? this.panels.find((panel) => panel.id === options.referencePanel)
1660
2124
  : options.referencePanel;
2125
+ const referencePanelId = typeof options.referencePanel === 'string'
2126
+ ? options.referencePanel
2127
+ : (_a = options.referencePanel) === null || _a === void 0 ? void 0 : _a.id;
1661
2128
  if (!referencePanel) {
1662
- throw new Error(`dockview: reference panel ${options.referencePanel} does not exist`);
2129
+ throw new Error(`dockview: reference panel ${referencePanelId} does not exist`);
1663
2130
  }
1664
2131
  referenceGroup = this.findGroup(referencePanel);
1665
2132
  if (!referenceGroup) {
1666
- throw new Error(`dockview: reference group for reference panel ${options.referencePanel} does not exist`);
2133
+ throw new Error(`dockview: reference group for reference panel ${referencePanelId} does not exist`);
1667
2134
  }
1668
2135
  }
1669
2136
  else if (isGroupOptionsWithGroup(options)) {
1670
2137
  referenceGroup =
1671
2138
  typeof options.referenceGroup === 'string'
1672
- ? (_a = this._groups.get(options.referenceGroup)) === null || _a === void 0 ? void 0 : _a.value
2139
+ ? (_b = this._groups.get(options.referenceGroup)) === null || _b === void 0 ? void 0 : _b.value
1673
2140
  : options.referenceGroup;
1674
2141
  if (!referenceGroup) {
1675
- throw new Error(`dockview: reference group ${options.referenceGroup} does not exist`);
2142
+ const referenceId = typeof options.referenceGroup === 'string'
2143
+ ? options.referenceGroup
2144
+ : (_c = options.referenceGroup) === null || _c === void 0 ? void 0 : _c.id;
2145
+ throw new Error(`dockview: reference group ${referenceId} does not exist`);
1676
2146
  }
1677
2147
  }
1678
2148
  else {
@@ -1710,12 +2180,71 @@ export class DockviewComponent extends BaseGrid {
1710
2180
  : Orientation.VERTICAL;
1711
2181
  }
1712
2182
  removeGroup(group, options) {
1713
- this.doRemoveGroup(group, options);
2183
+ this.mutation('remove', () => this.doRemoveGroup(group, options));
2184
+ }
2185
+ /**
2186
+ * Detach a single group from the nested gridview of its floating / popout
2187
+ * window, keeping the window and its remaining members alive, and reassign
2188
+ * the window's anchor if the detached group was it.
2189
+ *
2190
+ * @returns `true` if the group was detached from a multi-member window;
2191
+ * `false` if `group` is not in a nested window, or is the window's only
2192
+ * member — in which case the caller is responsible for disposing the whole
2193
+ * window.
2194
+ */
2195
+ detachFromNestedWindow(group) {
2196
+ var _a, _b;
2197
+ const floating = (_a = this._floatingGroupService) === null || _a === void 0 ? void 0 : _a.findByGroup(group);
2198
+ if (floating) {
2199
+ const members = this.nestedWindowMembers(group);
2200
+ if (members.length <= 1) {
2201
+ return false;
2202
+ }
2203
+ floating.gridview.remove(group);
2204
+ if (floating.group === group) {
2205
+ // The anchor left; promote a remaining member.
2206
+ floating.setAnchorGroup(members.find((m) => m !== group));
2207
+ }
2208
+ return true;
2209
+ }
2210
+ const popout = (_b = this._popoutWindowService) === null || _b === void 0 ? void 0 : _b.findByGroup(group);
2211
+ if (popout) {
2212
+ const members = this.nestedWindowMembers(group);
2213
+ if (members.length <= 1) {
2214
+ return false;
2215
+ }
2216
+ popout.gridview.remove(group);
2217
+ if (popout.popoutGroup === group) {
2218
+ // The anchor left; promote a remaining member.
2219
+ popout.popoutGroup = members.find((m) => m !== group);
2220
+ }
2221
+ return true;
2222
+ }
2223
+ return false;
2224
+ }
2225
+ /**
2226
+ * Dispose a group and forget it: remove it from `_groups` and fire the
2227
+ * removed event.
2228
+ */
2229
+ disposeGroupRecord(group) {
2230
+ group.dispose();
2231
+ this._groups.delete(group.id);
2232
+ this._onDidRemoveGroup.fire(group);
2233
+ }
2234
+ /**
2235
+ * When `removed` was the active group, fall the active selection back to
2236
+ * the first remaining group (or clear it when none remain).
2237
+ */
2238
+ activateFallbackGroupIfRemoved(removed, skipActive) {
2239
+ if (!skipActive && this._activeGroup === removed) {
2240
+ const groups = Array.from(this._groups.values());
2241
+ this.doSetGroupAndPanelActive(groups.length > 0 ? groups[0].value : undefined);
2242
+ }
1714
2243
  }
1715
2244
  doRemoveGroup(group, options) {
1716
- var _a;
2245
+ var _a, _b, _c, _d, _e;
1717
2246
  // Edge groups are permanent structural elements - never remove them from the layout
1718
- if ([...this._edgeGroups.values()].includes(group)) {
2247
+ if ((_a = this._edgeGroupService) === null || _a === void 0 ? void 0 : _a.includes(group)) {
1719
2248
  return group;
1720
2249
  }
1721
2250
  const panels = [...group.panels]; // reassign since group panels will mutate
@@ -1723,64 +2252,99 @@ export class DockviewComponent extends BaseGrid {
1723
2252
  for (const panel of panels) {
1724
2253
  this.removePanel(panel, {
1725
2254
  removeEmptyGroup: false,
1726
- skipDispose: (_a = options === null || options === void 0 ? void 0 : options.skipDispose) !== null && _a !== void 0 ? _a : false,
2255
+ skipDispose: (_b = options === null || options === void 0 ? void 0 : options.skipDispose) !== null && _b !== void 0 ? _b : false,
1727
2256
  });
1728
2257
  }
1729
2258
  }
1730
2259
  const activePanel = this.activePanel;
1731
2260
  if (group.api.location.type === 'floating') {
1732
- const floatingGroup = this._floatingGroups.find((_) => _.group === group);
1733
- if (floatingGroup) {
1734
- if (!(options === null || options === void 0 ? void 0 : options.skipDispose)) {
1735
- floatingGroup.group.dispose();
1736
- this._groups.delete(group.id);
1737
- this._onDidRemoveGroup.fire(group);
2261
+ const floatingGroup = (_c = this._floatingGroupService) === null || _c === void 0 ? void 0 : _c.findByGroup(group);
2262
+ if (!floatingGroup) {
2263
+ throw new Error('dockview: failed to find floating group');
2264
+ }
2265
+ if (this.detachFromNestedWindow(group)) {
2266
+ // The floating window hosts other groups and stays alive —
2267
+ // finalize just this group.
2268
+ if (options === null || options === void 0 ? void 0 : options.skipDispose) {
2269
+ // Relocation: reset location so the destination root can
2270
+ // re-tag it.
2271
+ group.model.location = { type: 'grid' };
1738
2272
  }
1739
- remove(this._floatingGroups, floatingGroup);
1740
- floatingGroup.dispose();
1741
- if (!(options === null || options === void 0 ? void 0 : options.skipActive) && this._activeGroup === group) {
1742
- const groups = Array.from(this._groups.values());
1743
- this.doSetGroupAndPanelActive(groups.length > 0 ? groups[0].value : undefined);
2273
+ else {
2274
+ this.disposeGroupRecord(group);
1744
2275
  }
1745
- return floatingGroup.group;
2276
+ this.activateFallbackGroupIfRemoved(group, options === null || options === void 0 ? void 0 : options.skipActive);
2277
+ return group;
2278
+ }
2279
+ // Last group leaving — dispose the whole floating window.
2280
+ if (!(options === null || options === void 0 ? void 0 : options.skipDispose)) {
2281
+ this.disposeGroupRecord(group);
1746
2282
  }
1747
- throw new Error('dockview: failed to find floating group');
2283
+ // floatingGroup.dispose() removes itself from the service array
2284
+ floatingGroup.dispose();
2285
+ this.activateFallbackGroupIfRemoved(group, options === null || options === void 0 ? void 0 : options.skipActive);
2286
+ return group;
1748
2287
  }
1749
2288
  if (group.api.location.type === 'popout') {
1750
- const selectedGroup = this._popoutGroups.find((_) => _.popoutGroup === group);
1751
- if (selectedGroup) {
1752
- if (!(options === null || options === void 0 ? void 0 : options.skipDispose)) {
1753
- if (!(options === null || options === void 0 ? void 0 : options.skipPopoutAssociated)) {
1754
- const refGroup = selectedGroup.referenceGroup
1755
- ? this.getPanel(selectedGroup.referenceGroup)
1756
- : undefined;
1757
- if (refGroup && refGroup.panels.length === 0) {
1758
- this.removeGroup(refGroup);
1759
- }
1760
- }
1761
- selectedGroup.popoutGroup.dispose();
1762
- this._groups.delete(group.id);
1763
- this._onDidRemoveGroup.fire(group);
2289
+ const selectedGroup = (_d = this._popoutWindowService) === null || _d === void 0 ? void 0 : _d.findByGroup(group);
2290
+ if (!selectedGroup) {
2291
+ throw new Error('dockview: failed to find popout group');
2292
+ }
2293
+ if (this.detachFromNestedWindow(group)) {
2294
+ // The popout window hosts other groups and stays alive —
2295
+ // finalize just this group.
2296
+ if (options === null || options === void 0 ? void 0 : options.skipDispose) {
2297
+ // Relocation: reset location so the destination root can
2298
+ // re-tag it.
2299
+ group.model.location = { type: 'grid' };
1764
2300
  }
1765
- remove(this._popoutGroups, selectedGroup);
1766
- const removedGroup = selectedGroup.disposable.dispose();
1767
- if (!(options === null || options === void 0 ? void 0 : options.skipPopoutReturn) && removedGroup) {
1768
- this.doAddGroup(removedGroup, [0]);
1769
- this.doSetGroupAndPanelActive(removedGroup);
2301
+ else {
2302
+ this.disposeGroupRecord(group);
1770
2303
  }
1771
- if (!(options === null || options === void 0 ? void 0 : options.skipActive) && this._activeGroup === group) {
1772
- const groups = Array.from(this._groups.values());
1773
- this.doSetGroupAndPanelActive(groups.length > 0 ? groups[0].value : undefined);
2304
+ this.activateFallbackGroupIfRemoved(group, options === null || options === void 0 ? void 0 : options.skipActive);
2305
+ return group;
2306
+ }
2307
+ // Last group leaving — tear the whole popout window down.
2308
+ if (!(options === null || options === void 0 ? void 0 : options.skipDispose)) {
2309
+ if (!(options === null || options === void 0 ? void 0 : options.skipPopoutAssociated)) {
2310
+ const refGroup = selectedGroup.referenceGroup
2311
+ ? this.getPanel(selectedGroup.referenceGroup)
2312
+ : undefined;
2313
+ if ((refGroup === null || refGroup === void 0 ? void 0 : refGroup.panels.length) === 0) {
2314
+ this.removeGroup(refGroup);
2315
+ }
1774
2316
  }
1775
- this.updateWatermark();
1776
- return selectedGroup.popoutGroup;
2317
+ selectedGroup.popoutGroup.dispose();
2318
+ this._groups.delete(group.id);
2319
+ this._onDidRemoveGroup.fire(group);
1777
2320
  }
1778
- throw new Error('dockview: failed to find popout group');
2321
+ (_e = this._popoutWindowService) === null || _e === void 0 ? void 0 : _e.remove(selectedGroup);
2322
+ const removedGroup = selectedGroup.disposable.dispose();
2323
+ if (!(options === null || options === void 0 ? void 0 : options.skipPopoutReturn) && removedGroup) {
2324
+ this.doAddGroup(removedGroup, [0]);
2325
+ this.doSetGroupAndPanelActive(removedGroup);
2326
+ }
2327
+ this.activateFallbackGroupIfRemoved(group, options === null || options === void 0 ? void 0 : options.skipActive);
2328
+ return selectedGroup.popoutGroup;
2329
+ }
2330
+ // A `grid`-location group whose element isn't actually in the gridview
2331
+ // is an orphan — e.g. a popout-destined group created during fromJSON
2332
+ // whose window hasn't opened yet, swept up by clear()/a re-entrant
2333
+ // fromJSON. `gridview.remove()` would throw "Invalid grid element", so
2334
+ // dispose it directly.
2335
+ if (!this.gridview.element.contains(group.element)) {
2336
+ if (!(options === null || options === void 0 ? void 0 : options.skipDispose)) {
2337
+ const item = this._groups.get(group.id);
2338
+ item === null || item === void 0 ? void 0 : item.disposable.dispose();
2339
+ this.disposeGroupRecord(group);
2340
+ }
2341
+ this.activateFallbackGroupIfRemoved(group, options === null || options === void 0 ? void 0 : options.skipActive);
2342
+ return group;
1779
2343
  }
1780
2344
  const re = super.doRemoveGroup(group, options);
1781
2345
  if (!(options === null || options === void 0 ? void 0 : options.skipActive)) {
1782
2346
  if (this.activePanel !== activePanel) {
1783
- this._onDidActivePanelChange.fire(this.activePanel);
2347
+ this.fireActivePanelChange(this.activePanel);
1784
2348
  }
1785
2349
  }
1786
2350
  return re;
@@ -1790,8 +2354,15 @@ export class DockviewComponent extends BaseGrid {
1790
2354
  cancelAnimationFrame(this._updatePositionsFrameId);
1791
2355
  }
1792
2356
  this._updatePositionsFrameId = requestAnimationFrame(() => {
2357
+ var _a, _b;
1793
2358
  this._updatePositionsFrameId = undefined;
1794
2359
  this.overlayRenderContainer.updateAllPositions();
2360
+ // Popout windows have their own render containers; reposition those
2361
+ // too so panels moved/split within a popout are laid out (the main
2362
+ // container only covers grid + floating, which share it).
2363
+ for (const entry of (_b = (_a = this._popoutWindowService) === null || _a === void 0 ? void 0 : _a.entries) !== null && _b !== void 0 ? _b : []) {
2364
+ entry.overlayRenderContainer.updateAllPositions();
2365
+ }
1795
2366
  });
1796
2367
  }
1797
2368
  movingLock(func) {
@@ -1804,8 +2375,74 @@ export class DockviewComponent extends BaseGrid {
1804
2375
  this._moving = isMoving;
1805
2376
  }
1806
2377
  }
2378
+ /**
2379
+ * Bracket a structural mutation with `onWillMutateLayout` /
2380
+ * `onDidMutateLayout`. Re-entrant: nested calls (a compound operation such
2381
+ * as a drag that relocates a panel) join the outermost transaction, so the
2382
+ * events fire exactly once around the whole operation. `kind` reflects the
2383
+ * outermost mutation.
2384
+ */
2385
+ mutation(kind, func) {
2386
+ const outer = this._mutationDepth === 0;
2387
+ const origin = this._origin;
2388
+ if (outer) {
2389
+ this._onWillMutateLayout.fire({ kind, origin });
2390
+ }
2391
+ this._mutationDepth++;
2392
+ try {
2393
+ return func();
2394
+ }
2395
+ finally {
2396
+ this._mutationDepth--;
2397
+ if (outer) {
2398
+ this._onDidMutateLayout.fire({ kind, origin });
2399
+ }
2400
+ }
2401
+ }
2402
+ /**
2403
+ * The origin of the operation currently in progress (`'user'` by default).
2404
+ * Read inside a `mutation()` or active-panel change to learn whether the
2405
+ * change was driven by application code (via the {@link DockviewApi}) or a
2406
+ * user gesture.
2407
+ */
2408
+ currentOrigin() {
2409
+ return this._origin;
2410
+ }
2411
+ /**
2412
+ * Run `func` with the operation origin set to `origin`, restoring the
2413
+ * previous value afterwards. Used by the DockviewApi boundary to tag
2414
+ * programmatic operations as `'api'`, and by user-gesture handlers to tag
2415
+ * `'user'`. Only the outermost caller sets the origin — a nested call (or a
2416
+ * call made while a mutation is already in flight) keeps whatever the
2417
+ * enclosing operation established, so the trigger always wins.
2418
+ */
2419
+ withOrigin(origin, func) {
2420
+ if (this._originDepth > 0 || this._mutationDepth > 0) {
2421
+ return func();
2422
+ }
2423
+ const previous = this._origin;
2424
+ this._origin = origin;
2425
+ this._originDepth++;
2426
+ try {
2427
+ return func();
2428
+ }
2429
+ finally {
2430
+ this._originDepth--;
2431
+ this._origin = previous;
2432
+ }
2433
+ }
2434
+ /**
2435
+ * Fire `onDidActivePanelChange` with the panel and the current operation
2436
+ * {@link DockviewOrigin}. Callers keep their own dedupe guards.
2437
+ */
2438
+ fireActivePanelChange(panel) {
2439
+ this._onDidActivePanelChange.fire({ panel, origin: this._origin });
2440
+ }
1807
2441
  moveGroupOrPanel(options) {
1808
- var _a;
2442
+ this.mutation('move', () => this._doMoveGroupOrPanel(options));
2443
+ }
2444
+ _doMoveGroupOrPanel(options) {
2445
+ var _a, _b;
1809
2446
  const destinationGroup = options.to.group;
1810
2447
  const sourceGroupId = options.from.groupId;
1811
2448
  const sourceItemId = options.from.panelId;
@@ -1886,15 +2523,20 @@ export class DockviewComponent extends BaseGrid {
1886
2523
  * Dropping a panel to the extremities of a group which will place that panel
1887
2524
  * into an adjacent group
1888
2525
  */
2526
+ // The destination group may live in the main grid or in a floating
2527
+ // window's nested gridview — resolve which root we are dropping
2528
+ // into so locations/orientation are computed against it.
2529
+ const destinationGridview = this.getGridviewForGroup(destinationGroup);
1889
2530
  const referenceLocation = getGridLocation(destinationGroup.element);
1890
- const targetLocation = getRelativeLocation(this.gridview.orientation, referenceLocation, destinationTarget);
2531
+ const targetLocation = getRelativeLocation(destinationGridview.orientation, referenceLocation, destinationTarget);
1891
2532
  if (sourceGroup.size < 2) {
1892
2533
  /**
1893
2534
  * If we are moving from a group which only has one panel left we will consider
1894
2535
  * moving the group itself rather than moving the panel into a newly created group
1895
2536
  */
1896
2537
  const [targetParentLocation, to] = tail(targetLocation);
1897
- if (sourceGroup.api.location.type === 'grid') {
2538
+ if (sourceGroup.api.location.type === 'grid' &&
2539
+ destinationGridview === this.gridview) {
1898
2540
  const sourceLocation = getGridLocation(sourceGroup.element);
1899
2541
  const [sourceParentLocation, from] = tail(sourceLocation);
1900
2542
  if (sequenceEquals(sourceParentLocation, targetParentLocation)) {
@@ -1909,9 +2551,11 @@ export class DockviewComponent extends BaseGrid {
1909
2551
  return;
1910
2552
  }
1911
2553
  }
1912
- if (sourceGroup.api.location.type === 'popout') {
2554
+ if (sourceGroup.api.location.type === 'popout' &&
2555
+ this.nestedWindowMembers(sourceGroup).length <= 1) {
1913
2556
  /**
1914
- * the source group is a popout group with a single panel
2557
+ * the source group is the only group in a popout window and
2558
+ * has a single panel
1915
2559
  *
1916
2560
  * 1. remove the panel from the group without triggering any events
1917
2561
  * 2. remove the popout group — this may cascade-remove the empty
@@ -1919,15 +2563,21 @@ export class DockviewComponent extends BaseGrid {
1919
2563
  * doRemoveGroup for popout groups), which can shift grid indices
1920
2564
  * 3. recompute the target location now that the grid is stable
1921
2565
  * 4. create a new group at the recomputed location and add that panel
2566
+ *
2567
+ * Multi-group popout windows fall through to the generic
2568
+ * detach-and-re-add path so the window stays alive.
1922
2569
  */
1923
- const popoutGroup = this._popoutGroups.find((group) => group.popoutGroup === sourceGroup);
2570
+ const popoutGroup = (_b = this._popoutWindowService) === null || _b === void 0 ? void 0 : _b.findByGroup(sourceGroup);
2571
+ if (!popoutGroup) {
2572
+ return;
2573
+ }
1924
2574
  const removedPanel = this.movingLock(() => popoutGroup.popoutGroup.model.removePanel(popoutGroup.popoutGroup.panels[0], {
1925
2575
  skipSetActive: true,
1926
2576
  skipSetActiveGroup: true,
1927
2577
  }));
1928
2578
  this.doRemoveGroup(sourceGroup, { skipActive: true });
1929
- const updatedTargetLocation = getRelativeLocation(this.gridview.orientation, getGridLocation(destinationGroup.element), destinationTarget);
1930
- const newGroup = this.createGroupAtLocation(updatedTargetLocation);
2579
+ const updatedTargetLocation = getRelativeLocation(destinationGridview.orientation, getGridLocation(destinationGroup.element), destinationTarget);
2580
+ const newGroup = this.createGroupAtLocation(updatedTargetLocation, undefined, undefined, destinationGridview);
1931
2581
  this.movingLock(() => newGroup.model.openPanel(removedPanel, {
1932
2582
  skipSetActive: true,
1933
2583
  }));
@@ -1951,7 +2601,7 @@ export class DockviewComponent extends BaseGrid {
1951
2601
  if (!removedPanel) {
1952
2602
  throw new Error(`dockview: No panel with id ${sourceItemId}`);
1953
2603
  }
1954
- const newGroup = this.createGroupAtLocation(targetLocation);
2604
+ const newGroup = this.createGroupAtLocation(targetLocation, undefined, undefined, destinationGridview);
1955
2605
  this.movingLock(() => newGroup.model.openPanel(removedPanel, {
1956
2606
  skipSetGroupActive: true,
1957
2607
  }));
@@ -1969,8 +2619,9 @@ export class DockviewComponent extends BaseGrid {
1969
2619
  }));
1970
2620
  // after deleting the group we need to re-evaulate the ref location
1971
2621
  const updatedReferenceLocation = getGridLocation(destinationGroup.element);
1972
- const location = getRelativeLocation(this.gridview.orientation, updatedReferenceLocation, destinationTarget);
1973
- this.movingLock(() => this.doAddGroup(targetGroup, location));
2622
+ const location = getRelativeLocation(destinationGridview.orientation, updatedReferenceLocation, destinationTarget);
2623
+ this.movingLock(() => this.doAddGroup(targetGroup, location, undefined, destinationGridview));
2624
+ this.setGroupLocationForRoot(targetGroup, destinationGridview);
1974
2625
  this.doSetGroupAndPanelActive(targetGroup);
1975
2626
  this._onDidMovePanel.fire({
1976
2627
  panel: this.getGroupPanel(sourceItemId),
@@ -1989,8 +2640,8 @@ export class DockviewComponent extends BaseGrid {
1989
2640
  if (!removedPanel) {
1990
2641
  throw new Error(`dockview: No panel with id ${sourceItemId}`);
1991
2642
  }
1992
- const dropLocation = getRelativeLocation(this.gridview.orientation, referenceLocation, destinationTarget);
1993
- const group = this.createGroupAtLocation(dropLocation);
2643
+ const dropLocation = getRelativeLocation(destinationGridview.orientation, referenceLocation, destinationTarget);
2644
+ const group = this.createGroupAtLocation(dropLocation, undefined, undefined, destinationGridview);
1994
2645
  this.movingLock(() => group.model.openPanel(removedPanel, {
1995
2646
  skipSetGroupActive: true,
1996
2647
  }));
@@ -2085,6 +2736,21 @@ export class DockviewComponent extends BaseGrid {
2085
2736
  addPanelsToGroup(targetGroup);
2086
2737
  }
2087
2738
  moveGroup(options) {
2739
+ this.mutation('move', () => this._doMoveGroup(options));
2740
+ }
2741
+ // Bracket maximize/restore as a 'maximize' transaction. The maximized node
2742
+ // is serialized by the gridview (`SerializedGridview.maximizedNode`), so
2743
+ // the state round-trips through toJSON/fromJSON and is restorable on undo.
2744
+ // When the exit is a side-effect of another bracketed operation (e.g. a
2745
+ // move that activates a different group) the depth counter folds it in.
2746
+ maximizeGroup(panel) {
2747
+ this.mutation('maximize', () => super.maximizeGroup(panel));
2748
+ }
2749
+ exitMaximizedGroup() {
2750
+ this.mutation('maximize', () => super.exitMaximizedGroup());
2751
+ }
2752
+ _doMoveGroup(options) {
2753
+ var _a, _b, _c;
2088
2754
  const from = options.from.group;
2089
2755
  const to = options.to.group;
2090
2756
  const target = options.to.position;
@@ -2165,6 +2831,13 @@ export class DockviewComponent extends BaseGrid {
2165
2831
  }));
2166
2832
  const movedPanels = this.movingLock(() => [...from.panels].map((p) => from.model.removePanel(p.id, { skipSetActive: true })));
2167
2833
  source = this.createGroup();
2834
+ // The new source group enters the layout via gridview.addView
2835
+ // below, which bypasses doAddGroup and so doesn't fire
2836
+ // BaseGrid._onDidAdd. Modules (TabGroupChips, etc.) drive
2837
+ // per-group attachment off _onDidAddGroup, so we fire it
2838
+ // explicitly here — matches the pattern in addFloatingGroup
2839
+ // and addEdgeGroup.
2840
+ this._onDidAddGroup.fire(source);
2168
2841
  this.movingLock(() => {
2169
2842
  for (const panel of movedPanels) {
2170
2843
  source.model.openPanel(panel, {
@@ -2191,23 +2864,34 @@ export class DockviewComponent extends BaseGrid {
2191
2864
  this.gridview.removeView(getGridLocation(from.element));
2192
2865
  break;
2193
2866
  case 'floating': {
2194
- const selectedFloatingGroup = this._floatingGroups.find((x) => x.group === from);
2867
+ const selectedFloatingGroup = (_a = this._floatingGroupService) === null || _a === void 0 ? void 0 : _a.findByGroup(from);
2195
2868
  if (!selectedFloatingGroup) {
2196
2869
  throw new Error('dockview: failed to find floating group');
2197
2870
  }
2198
- selectedFloatingGroup.dispose();
2871
+ // Detach just this group from the floating window's
2872
+ // nested gridview, keeping the window (and its other
2873
+ // groups) alive. If it was the only member, dispose the
2874
+ // whole window.
2875
+ if (!this.detachFromNestedWindow(from)) {
2876
+ selectedFloatingGroup.dispose();
2877
+ }
2199
2878
  break;
2200
2879
  }
2201
2880
  case 'popout': {
2202
- const selectedPopoutGroup = this._popoutGroups.find((x) => x.popoutGroup === from);
2881
+ const selectedPopoutGroup = (_b = this._popoutWindowService) === null || _b === void 0 ? void 0 : _b.findByGroup(from);
2203
2882
  if (!selectedPopoutGroup) {
2204
2883
  throw new Error('dockview: failed to find popout group');
2205
2884
  }
2206
- // Remove from popout groups list to prevent automatic restoration
2207
- const index = this._popoutGroups.indexOf(selectedPopoutGroup);
2208
- if (index >= 0) {
2209
- this._popoutGroups.splice(index, 1);
2885
+ // Detach just this group from the popout window's
2886
+ // nested gridview, keeping the window + its other groups
2887
+ // alive. Destination containers/location are applied by
2888
+ // the placement block below.
2889
+ if (this.detachFromNestedWindow(from)) {
2890
+ break;
2210
2891
  }
2892
+ // Last group leaving — tear the window down. Remove from
2893
+ // the service first to prevent automatic restoration.
2894
+ (_c = this._popoutWindowService) === null || _c === void 0 ? void 0 : _c.remove(selectedPopoutGroup);
2211
2895
  // Clean up the reference group (ghost) if it exists and is hidden
2212
2896
  if (selectedPopoutGroup.referenceGroup) {
2213
2897
  const referenceGroup = this.getPanel(selectedPopoutGroup.referenceGroup);
@@ -2218,34 +2902,25 @@ export class DockviewComponent extends BaseGrid {
2218
2902
  });
2219
2903
  }
2220
2904
  }
2221
- // Manually dispose the window without triggering restoration
2905
+ // Dispose the window without triggering restoration. The
2906
+ // placement block below applies the destination
2907
+ // location and containers to `from`.
2222
2908
  selectedPopoutGroup.window.dispose();
2223
- // Update group's location and containers for target
2224
- if (to.api.location.type === 'grid') {
2225
- from.model.renderContainer =
2226
- this.overlayRenderContainer;
2227
- from.model.dropTargetContainer =
2228
- this.rootDropTargetContainer;
2229
- from.model.location = { type: 'grid' };
2230
- }
2231
- else if (to.api.location.type === 'floating') {
2232
- from.model.renderContainer =
2233
- this.overlayRenderContainer;
2234
- from.model.dropTargetContainer =
2235
- this.rootDropTargetContainer;
2236
- from.model.location = { type: 'floating' };
2237
- }
2238
2909
  break;
2239
2910
  }
2240
2911
  }
2241
2912
  }
2242
- // For moves to grid locations
2243
- if (to.api.location.type === 'grid') {
2913
+ // Place `source` next to `to`, in whichever gridview root `to`
2914
+ // lives in. When `to` is inside a floating / popout window this
2915
+ // splits that window's nested layout rather than spawning a new one.
2916
+ if (to.api.location.type === 'grid' ||
2917
+ to.api.location.type === 'floating' ||
2918
+ to.api.location.type === 'popout') {
2919
+ const destGridview = this.getGridviewForGroup(to);
2244
2920
  const referenceLocation = getGridLocation(to.element);
2245
- const dropLocation = getRelativeLocation(this.gridview.orientation, referenceLocation, target);
2246
- // Add to grid for all moves targeting grid location
2921
+ const dropLocation = getRelativeLocation(destGridview.orientation, referenceLocation, target);
2247
2922
  let size;
2248
- switch (this.gridview.orientation) {
2923
+ switch (destGridview.orientation) {
2249
2924
  case Orientation.VERTICAL:
2250
2925
  size =
2251
2926
  referenceLocation.length % 2 == 0
@@ -2259,43 +2934,8 @@ export class DockviewComponent extends BaseGrid {
2259
2934
  : from.api.width;
2260
2935
  break;
2261
2936
  }
2262
- this.gridview.addView(source, size, dropLocation);
2263
- }
2264
- else if (to.api.location.type === 'floating') {
2265
- // For moves to floating locations, add as floating group
2266
- // Get the position/size from the target floating group
2267
- const targetFloatingGroup = this._floatingGroups.find((x) => x.group === to);
2268
- if (targetFloatingGroup) {
2269
- const box = targetFloatingGroup.overlay.toJSON();
2270
- // Calculate position based on available properties
2271
- let left, top;
2272
- if ('left' in box) {
2273
- left = box.left + 50;
2274
- }
2275
- else if ('right' in box) {
2276
- left = Math.max(0, box.right - box.width - 50);
2277
- }
2278
- else {
2279
- left = 50; // Default fallback
2280
- }
2281
- if ('top' in box) {
2282
- top = box.top + 50;
2283
- }
2284
- else if ('bottom' in box) {
2285
- top = Math.max(0, box.bottom - box.height - 50);
2286
- }
2287
- else {
2288
- top = 50; // Default fallback
2289
- }
2290
- this.addFloatingGroup(source, {
2291
- height: box.height,
2292
- width: box.width,
2293
- position: {
2294
- left,
2295
- top,
2296
- },
2297
- });
2298
- }
2937
+ destGridview.addView(source, size, dropLocation);
2938
+ this.setGroupLocationForRoot(source, destGridview);
2299
2939
  }
2300
2940
  }
2301
2941
  source.panels.forEach((panel) => {
@@ -2316,14 +2956,16 @@ export class DockviewComponent extends BaseGrid {
2316
2956
  }
2317
2957
  }
2318
2958
  doSetGroupActive(group) {
2959
+ var _a;
2319
2960
  super.doSetGroupActive(group);
2320
2961
  const activePanel = this.activePanel;
2321
2962
  if (!this._moving &&
2322
- activePanel !== this._onDidActivePanelChange.value) {
2323
- this._onDidActivePanelChange.fire(activePanel);
2963
+ activePanel !== ((_a = this._onDidActivePanelChange.value) === null || _a === void 0 ? void 0 : _a.panel)) {
2964
+ this.fireActivePanelChange(activePanel);
2324
2965
  }
2325
2966
  }
2326
2967
  doSetGroupAndPanelActive(group) {
2968
+ var _a;
2327
2969
  super.doSetGroupActive(group);
2328
2970
  const activePanel = this.activePanel;
2329
2971
  if (group &&
@@ -2332,8 +2974,8 @@ export class DockviewComponent extends BaseGrid {
2332
2974
  this.exitMaximizedGroup();
2333
2975
  }
2334
2976
  if (!this._moving &&
2335
- activePanel !== this._onDidActivePanelChange.value) {
2336
- this._onDidActivePanelChange.fire(activePanel);
2977
+ activePanel !== ((_a = this._onDidActivePanelChange.value) === null || _a === void 0 ? void 0 : _a.panel)) {
2978
+ this.fireActivePanelChange(activePanel);
2337
2979
  }
2338
2980
  }
2339
2981
  getNextGroupId() {
@@ -2344,9 +2986,7 @@ export class DockviewComponent extends BaseGrid {
2344
2986
  return id;
2345
2987
  }
2346
2988
  createGroup(options) {
2347
- if (!options) {
2348
- options = {};
2349
- }
2989
+ options !== null && options !== void 0 ? options : (options = {});
2350
2990
  let id = options === null || options === void 0 ? void 0 : options.id;
2351
2991
  if (id && this._groups.has(options.id)) {
2352
2992
  console.warn(`dockview: Duplicate group id ${options === null || options === void 0 ? void 0 : options.id}. reassigning group id to avoid errors`);
@@ -2362,9 +3002,11 @@ export class DockviewComponent extends BaseGrid {
2362
3002
  view.init({ params: {}, accessor: this });
2363
3003
  if (!this._groups.has(view.id)) {
2364
3004
  const disposable = new CompositeDisposable(view.model.onTabDragStart((event) => {
2365
- this._onWillDragPanel.fire(event);
3005
+ var _a;
3006
+ (_a = this._advancedDnDService) === null || _a === void 0 ? void 0 : _a.dispatchWillDragPanel(event);
2366
3007
  }), view.model.onGroupDragStart((event) => {
2367
- this._onWillDragGroup.fire(event);
3008
+ var _a;
3009
+ (_a = this._advancedDnDService) === null || _a === void 0 ? void 0 : _a.dispatchWillDragGroup(event);
2368
3010
  }), view.model.onMove((event) => {
2369
3011
  const { groupId, itemId, target, index, tabGroupId } = event;
2370
3012
  this.moveGroupOrPanel({
@@ -2382,15 +3024,19 @@ export class DockviewComponent extends BaseGrid {
2382
3024
  }), view.model.onDidDrop((event) => {
2383
3025
  this._onDidDrop.fire(event);
2384
3026
  }), view.model.onWillDrop((event) => {
2385
- this._onWillDrop.fire(event);
3027
+ var _a;
3028
+ (_a = this._advancedDnDService) === null || _a === void 0 ? void 0 : _a.dispatchWillDrop(event);
2386
3029
  }), view.model.onWillShowOverlay((event) => {
3030
+ var _a;
2387
3031
  if (this.options.disableDnd) {
3032
+ // Engine policy — stays in core, ahead of any
3033
+ // customisation dispatch.
2388
3034
  event.preventDefault();
2389
3035
  return;
2390
3036
  }
2391
- this._onWillShowOverlay.fire(event);
2392
- }), view.model.onUnhandledDragOverEvent((event) => {
2393
- this._onUnhandledDragOverEvent.fire(event);
3037
+ (_a = this._advancedDnDService) === null || _a === void 0 ? void 0 : _a.dispatchWillShowOverlay(event);
3038
+ }), view.model.onUnhandledDragOver((event) => {
3039
+ this._onUnhandledDragOver.fire(event);
2394
3040
  }), view.model.onDidAddPanel((event) => {
2395
3041
  if (this._moving) {
2396
3042
  return;
@@ -2402,27 +3048,17 @@ export class DockviewComponent extends BaseGrid {
2402
3048
  }
2403
3049
  this._onDidRemovePanel.fire(event.panel);
2404
3050
  }), view.model.onDidActivePanelChange((event) => {
3051
+ var _a;
2405
3052
  if (this._moving) {
2406
3053
  return;
2407
3054
  }
2408
3055
  if (event.panel !== this.activePanel) {
2409
3056
  return;
2410
3057
  }
2411
- if (this._onDidActivePanelChange.value !== event.panel) {
2412
- this._onDidActivePanelChange.fire(event.panel);
2413
- }
2414
- }), view.model.onDidCreateTabGroup((e) => {
2415
- this._onDidCreateTabGroup.fire(e);
2416
- }), view.model.onDidDestroyTabGroup((e) => {
2417
- this._onDidDestroyTabGroup.fire(e);
2418
- }), view.model.onDidAddPanelToTabGroup((e) => {
2419
- this._onDidAddPanelToTabGroup.fire(e);
2420
- }), view.model.onDidRemovePanelFromTabGroup((e) => {
2421
- this._onDidRemovePanelFromTabGroup.fire(e);
2422
- }), view.model.onDidTabGroupChange((e) => {
2423
- this._onDidTabGroupChange.fire(e);
2424
- }), view.model.onDidTabGroupCollapsedChange((e) => {
2425
- this._onDidTabGroupCollapsedChange.fire(e);
3058
+ if (((_a = this._onDidActivePanelChange.value) === null || _a === void 0 ? void 0 : _a.panel) !==
3059
+ event.panel) {
3060
+ this.fireActivePanelChange(event.panel);
3061
+ }
2426
3062
  }), Event.any(view.model.onDidPanelTitleChange, view.model.onDidPanelParametersChange)(() => {
2427
3063
  this._bufferOnDidLayoutChange.fire();
2428
3064
  }));
@@ -2450,11 +3086,73 @@ export class DockviewComponent extends BaseGrid {
2450
3086
  });
2451
3087
  return panel;
2452
3088
  }
2453
- createGroupAtLocation(location, size, options) {
3089
+ createGroupAtLocation(location, size, options, gridview = this.gridview) {
2454
3090
  const group = this.createGroup(options);
2455
- this.doAddGroup(group, location, size);
3091
+ this.doAddGroup(group, location, size, gridview);
3092
+ this.setGroupLocationForRoot(group, gridview);
2456
3093
  return group;
2457
3094
  }
3095
+ /**
3096
+ * Tag a group with the location and render / drop-target containers
3097
+ * matching the gridview root it now lives in: the main grid, a floating
3098
+ * window (shares the main containers), or a popout window (uses its own
3099
+ * window-local containers).
3100
+ */
3101
+ setGroupLocationForRoot(group, gridview) {
3102
+ var _a;
3103
+ const popout = (_a = this._popoutWindowService) === null || _a === void 0 ? void 0 : _a.entries.find((entry) => entry.gridview === gridview);
3104
+ if (popout) {
3105
+ if (group.model.renderContainer !== popout.overlayRenderContainer) {
3106
+ group.model.renderContainer = popout.overlayRenderContainer;
3107
+ }
3108
+ group.model.dropTargetContainer = popout.dropTargetContainer;
3109
+ group.model.location = {
3110
+ type: 'popout',
3111
+ getWindow: popout.getWindow,
3112
+ popoutUrl: popout.popoutUrl,
3113
+ };
3114
+ return;
3115
+ }
3116
+ // grid / floating both render through the main containers
3117
+ if (group.model.renderContainer !== this.overlayRenderContainer) {
3118
+ group.model.renderContainer = this.overlayRenderContainer;
3119
+ }
3120
+ group.model.dropTargetContainer = this.rootDropTargetContainer;
3121
+ group.model.location =
3122
+ gridview === this.gridview
3123
+ ? { type: 'grid' }
3124
+ : { type: 'floating' };
3125
+ }
3126
+ /**
3127
+ * Resolve which gridview root currently owns a group: the main grid, or
3128
+ * the nested gridview of the floating / popout window it lives in.
3129
+ */
3130
+ getGridviewForGroup(group) {
3131
+ var _a, _b;
3132
+ const floating = (_a = this._floatingGroupService) === null || _a === void 0 ? void 0 : _a.findByGroup(group);
3133
+ if (floating) {
3134
+ return floating.gridview;
3135
+ }
3136
+ // Use findByGroup (anchor-identity OR containment) for symmetry with
3137
+ // the floating branch — it also resolves an anchor whose element is
3138
+ // briefly detached from the gridview during a move/restore.
3139
+ const popout = (_b = this._popoutWindowService) === null || _b === void 0 ? void 0 : _b.findByGroup(group);
3140
+ if (popout) {
3141
+ return popout.gridview;
3142
+ }
3143
+ return this.gridview;
3144
+ }
3145
+ /**
3146
+ * The groups that live within the same floating / popout window as `group`
3147
+ * (including `group` itself). Empty when `group` is in the main grid.
3148
+ */
3149
+ nestedWindowMembers(group) {
3150
+ const gridview = this.getGridviewForGroup(group);
3151
+ if (gridview === this.gridview) {
3152
+ return [];
3153
+ }
3154
+ return this.groups.filter((candidate) => gridview.element.contains(candidate.element));
3155
+ }
2458
3156
  findGroup(panel) {
2459
3157
  var _a;
2460
3158
  return (_a = Array.from(this._groups.values()).find((group) => group.value.model.containsPanel(panel))) === null || _a === void 0 ? void 0 : _a.value;
@@ -2465,44 +3163,33 @@ export class DockviewComponent extends BaseGrid {
2465
3163
  ? rootOrientation
2466
3164
  : orthogonal(rootOrientation);
2467
3165
  }
2468
- updateDropTargetModel(options) {
2469
- if ('dndEdges' in options) {
2470
- const disabled = typeof options.dndEdges === 'boolean' &&
2471
- options.dndEdges === false;
2472
- this._rootDropTarget.disabled = disabled;
2473
- this._rootPointerDropTarget.disabled = disabled;
2474
- if (typeof options.dndEdges === 'object' &&
2475
- options.dndEdges !== null) {
2476
- this._rootDropTarget.setOverlayModel(options.dndEdges);
2477
- this._rootPointerDropTarget.setOverlayModel(options.dndEdges);
2478
- }
2479
- else {
2480
- this._rootDropTarget.setOverlayModel(DEFAULT_ROOT_OVERLAY_MODEL);
2481
- this._rootPointerDropTarget.setOverlayModel(DEFAULT_ROOT_OVERLAY_MODEL);
2482
- }
2483
- }
2484
- if ('rootOverlayModel' in options) {
2485
- this.updateDropTargetModel({ dndEdges: options.dndEdges });
2486
- }
2487
- }
2488
3166
  updateTheme() {
2489
- var _a, _b, _c, _d, _e, _f, _g, _h, _j;
3167
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
2490
3168
  const theme = (_a = this._options.theme) !== null && _a !== void 0 ? _a : themeAbyss;
2491
3169
  // Apply the theme class only to the shell so edge groups and the
2492
3170
  // main grid both inherit its CSS custom properties via the cascade.
2493
3171
  // Re-declaring it on `.dv-dockview` would block consumer overrides
2494
3172
  // set on the shell from reaching the dockview subtree.
2495
3173
  (_b = this._shellThemeClassnames) === null || _b === void 0 ? void 0 : _b.setClassNames(theme.className);
2496
- this.gridview.margin = (_c = theme.gap) !== null && _c !== void 0 ? _c : 0;
2497
- (_d = this._shellManager) === null || _d === void 0 ? void 0 : _d.updateTheme((_e = theme.gap) !== null && _e !== void 0 ? _e : 0, (_f = theme.edgeGroupCollapsedSize) !== null && _f !== void 0 ? _f : 35);
2498
- if (theme.dndOverlayBorder !== undefined) {
2499
- this.element.style.setProperty('--dv-drag-over-border', theme.dndOverlayBorder);
2500
- (_g = this._shellManager) === null || _g === void 0 ? void 0 : _g.element.style.setProperty('--dv-drag-over-border', theme.dndOverlayBorder);
2501
- }
2502
- else {
3174
+ const gap = (_c = theme.gap) !== null && _c !== void 0 ? _c : 0;
3175
+ this.gridview.margin = gap;
3176
+ // Floating / popout windows host their own nested gridviews; keep their
3177
+ // gap in sync with the main grid when the theme changes at runtime.
3178
+ for (const floating of this.floatingGroups) {
3179
+ floating.gridview.margin = gap;
3180
+ }
3181
+ for (const entry of (_e = (_d = this._popoutWindowService) === null || _d === void 0 ? void 0 : _d.entries) !== null && _e !== void 0 ? _e : []) {
3182
+ entry.gridview.margin = gap;
3183
+ }
3184
+ (_f = this._shellManager) === null || _f === void 0 ? void 0 : _f.updateTheme(gap, (_g = theme.edgeGroupCollapsedSize) !== null && _g !== void 0 ? _g : 35);
3185
+ if (theme.dndOverlayBorder === undefined) {
2503
3186
  this.element.style.removeProperty('--dv-drag-over-border');
2504
3187
  (_h = this._shellManager) === null || _h === void 0 ? void 0 : _h.element.style.removeProperty('--dv-drag-over-border');
2505
3188
  }
3189
+ else {
3190
+ this.element.style.setProperty('--dv-drag-over-border', theme.dndOverlayBorder);
3191
+ (_j = this._shellManager) === null || _j === void 0 ? void 0 : _j.element.style.setProperty('--dv-drag-over-border', theme.dndOverlayBorder);
3192
+ }
2506
3193
  switch (theme.dndOverlayMounting) {
2507
3194
  case 'absolute':
2508
3195
  this.rootDropTargetContainer.disabled = false;
@@ -2514,7 +3201,7 @@ export class DockviewComponent extends BaseGrid {
2514
3201
  }
2515
3202
  // Toggle a CSS class so theme stylesheets can scope pure-CSS
2516
3203
  // tab group indicator rules to the 'none' mode only.
2517
- const indicatorNone = ((_j = theme.tabGroupIndicator) !== null && _j !== void 0 ? _j : 'wrap') === 'none';
3204
+ const indicatorNone = ((_k = theme.tabGroupIndicator) !== null && _k !== void 0 ? _k : 'wrap') === 'none';
2518
3205
  toggleClass(this.element, 'dv-tab-group-indicator-none', indicatorNone);
2519
3206
  if (this._shellManager) {
2520
3207
  toggleClass(this._shellManager.element, 'dv-tab-group-indicator-none', indicatorNone);