dockview-react 8.1.0 → 8.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * dockview-react
3
- * @version 8.1.0
3
+ * @version 8.2.0
4
4
  * @link https://github.com/dockview/dockview
5
5
  * @license MIT
6
6
  */
@@ -51,7 +51,7 @@ react_dom = __toESM(react_dom);
51
51
  //#region ../dockview-core/dist/package/main.esm.mjs
52
52
  /**
53
53
  * dockview-core
54
- * @version 8.1.0
54
+ * @version 8.2.0
55
55
  * @link https://github.com/dockview/dockview
56
56
  * @license MIT
57
57
  */
@@ -10395,7 +10395,11 @@ react_dom = __toESM(react_dom);
10395
10395
  this.panels.forEach((panel) => {
10396
10396
  this.rerender(panel);
10397
10397
  });
10398
- if (this._activePanel) this.contentContainer.renderPanel(this._activePanel, { asActive: true });
10398
+ if (this._activePanel) {
10399
+ this.contentContainer.renderPanel(this._activePanel, { asActive: true });
10400
+ const { width, height } = this.contentDimensions();
10401
+ this._activePanel.layout(width, height);
10402
+ }
10399
10403
  }
10400
10404
  get renderContainer() {
10401
10405
  var _this$_overwriteRende;
@@ -11342,7 +11346,7 @@ react_dom = __toESM(react_dom);
11342
11346
  this._group = value;
11343
11347
  this._onDidGroupChange.fire({});
11344
11348
  this.setupGroupEventListeners(oldGroup);
11345
- this._onDidLocationChange.fire({ location: this.group.api.location });
11349
+ this.fireLocationChange();
11346
11350
  }
11347
11351
  }
11348
11352
  get group() {
@@ -11418,6 +11422,23 @@ react_dom = __toESM(react_dom);
11418
11422
  exitMaximized() {
11419
11423
  this.group.api.exitMaximized();
11420
11424
  }
11425
+ /**
11426
+ * Report that this panel's location may have moved.
11427
+ *
11428
+ * A relocation touches the location more than once - the panel is
11429
+ * reparented into the destination group, then that group is tagged with
11430
+ * the location it ends up at - so the event is coalesced to the end of the
11431
+ * enclosing layout mutation and reports where the panel actually settled.
11432
+ * Firing each signal as it happened surfaced an intermediate `grid`
11433
+ * location the panel was never in, at a point where it was in neither
11434
+ * group's panel list. Outside a mutation this fires straight away.
11435
+ */
11436
+ fireLocationChange() {
11437
+ this.accessor.deferLocationChange(this, () => {
11438
+ if (this.isDisposed) return;
11439
+ this._onDidLocationChange.fire({ location: this.location });
11440
+ });
11441
+ }
11421
11442
  setupGroupEventListeners(previousGroup) {
11422
11443
  var _previousGroup$isActi;
11423
11444
  let _trackGroupActive = (_previousGroup$isActi = previousGroup === null || previousGroup === void 0 ? void 0 : previousGroup.isActive) !== null && _previousGroup$isActi !== void 0 ? _previousGroup$isActi : false;
@@ -11426,9 +11447,9 @@ react_dom = __toESM(react_dom);
11426
11447
  const hasBecomeVisible = event.isVisible && !this.isVisible;
11427
11448
  const isActivePanel = this.group.model.isPanelActive(this.panel);
11428
11449
  if (hasBecomeHidden || hasBecomeVisible && isActivePanel) this._onDidVisibilityChange.fire(event);
11429
- }), this.group.api.onDidLocationChange((event) => {
11450
+ }), this.group.api.onDidLocationChange(() => {
11430
11451
  if (this.group !== this.panel.group) return;
11431
- this._onDidLocationChange.fire(event);
11452
+ this.fireLocationChange();
11432
11453
  }), this.group.api.onDidActiveChange(() => {
11433
11454
  if (this.group !== this.panel.group) return;
11434
11455
  if (_trackGroupActive !== this.isGroupActive) {
@@ -13388,6 +13409,13 @@ react_dom = __toESM(react_dom);
13388
13409
  });
13389
13410
  }
13390
13411
  };
13412
+ /**
13413
+ * Placeholder handle marking "a reposition frame has been requested but the
13414
+ * real handle is not known yet". Never passed to `cancelAnimationFrame` in
13415
+ * practice — it is replaced within the same synchronous block — and harmless
13416
+ * if it were, since no frame carries this id.
13417
+ */
13418
+ const SCHEDULED = -1;
13391
13419
  function createFocusableElement() {
13392
13420
  const element = document.createElement("div");
13393
13421
  element.tabIndex = -1;
@@ -13400,12 +13428,13 @@ react_dom = __toESM(react_dom);
13400
13428
  this.accessor = accessor;
13401
13429
  this.map = {};
13402
13430
  this._disposed = false;
13431
+ this._generation = 0;
13403
13432
  this.positionCache = new PositionCache();
13404
- this.pendingUpdates = /* @__PURE__ */ new Set();
13405
13433
  this.addDisposables(Disposable.from(() => {
13406
13434
  for (const value of Object.values(this.map)) {
13407
13435
  value.disposable.dispose();
13408
13436
  value.destroy.dispose();
13437
+ this.cancelPendingUpdate(value);
13409
13438
  }
13410
13439
  this._disposed = true;
13411
13440
  }));
@@ -13432,11 +13461,24 @@ react_dom = __toESM(react_dom);
13432
13461
  this.positionCache.invalidate();
13433
13462
  (_entry$resize = entry.resize) === null || _entry$resize === void 0 || _entry$resize.call(entry);
13434
13463
  }
13464
+ /**
13465
+ * Drop the reposition frame queued for a panel, if any. A queued frame is
13466
+ * bound to the reference container that was current when it was scheduled,
13467
+ * so it must be discarded whenever that container stops being the one the
13468
+ * overlay should track.
13469
+ */
13470
+ cancelPendingUpdate(entry) {
13471
+ if (entry.pendingUpdate !== void 0) {
13472
+ cancelAnimationFrame(entry.pendingUpdate);
13473
+ entry.pendingUpdate = void 0;
13474
+ }
13475
+ }
13435
13476
  detatch(panel) {
13436
13477
  if (this.map[panel.api.id]) {
13437
- const { disposable, destroy } = this.map[panel.api.id];
13438
- disposable.dispose();
13439
- destroy.dispose();
13478
+ const entry = this.map[panel.api.id];
13479
+ entry.disposable.dispose();
13480
+ entry.destroy.dispose();
13481
+ this.cancelPendingUpdate(entry);
13440
13482
  delete this.map[panel.api.id];
13441
13483
  return true;
13442
13484
  }
@@ -13452,22 +13494,73 @@ react_dom = __toESM(react_dom);
13452
13494
  panel,
13453
13495
  disposable: Disposable.NONE,
13454
13496
  destroy: Disposable.NONE,
13455
- element
13497
+ element,
13498
+ retainPreviousGeometry: false,
13499
+ positioned: false,
13500
+ generation: ++this._generation
13456
13501
  };
13457
13502
  }
13458
- const focusContainer = this.map[panel.api.id].element;
13503
+ const mapEntry = this.map[panel.api.id];
13504
+ /**
13505
+ * A *change* of reference container supersedes the previous `attach`:
13506
+ * take a fresh generation so its `resize` closure can no longer run,
13507
+ * and drop the frame it queued against the old container. During
13508
+ * `fromJSON({ reuseExistingPanels: true })` that old container is a
13509
+ * detached staging group measuring 0x0, so leaving its frame in flight
13510
+ * both wastes the update and delays the reposition against the real one.
13511
+ *
13512
+ * Re-attaching over the *same* container (re-open, active panel change)
13513
+ * deliberately leaves scheduled work alone: `repositionPanelOverlay`
13514
+ * (the auto-hide peek) schedules a frame carrying the sticky
13515
+ * `forceVisible`/`clip` state which `attach` does not re-apply, and a
13516
+ * peeked panel's `api.isVisible` is false — so discarding that frame
13517
+ * leaves `visibilityChanged` to hide the overlay and the peek renders
13518
+ * nothing. A newly created entry already holds a fresh generation, so
13519
+ * the stale closures a `detatch` left behind are fenced off either way.
13520
+ */
13521
+ if (mapEntry.referenceContainer !== referenceContainer) {
13522
+ mapEntry.generation = ++this._generation;
13523
+ this.cancelPendingUpdate(mapEntry);
13524
+ mapEntry.referenceContainer = referenceContainer;
13525
+ /**
13526
+ * Arm the retain flag only here: it means "a replacement reference
13527
+ * container is awaiting layout", so the 0x0 early-return below
13528
+ * holds the last good box rather than blanking the panel mid-move.
13529
+ * Arming it on every re-attach instead would make a container that
13530
+ * genuinely collapses to zero keep its stale geometry forever.
13531
+ *
13532
+ * Gated on `positioned` because an overlay that has never been
13533
+ * positioned has no left/top/width/height, and `.dv-render-overlay`
13534
+ * defaults to 100%/100% — retaining "previous" geometry there would
13535
+ * pin the content over the whole container.
13536
+ */
13537
+ mapEntry.retainPreviousGeometry = mapEntry.positioned;
13538
+ }
13539
+ const generation = mapEntry.generation;
13540
+ const focusContainer = mapEntry.element;
13459
13541
  const contentElement = panel.view.content.element;
13460
13542
  if (contentElement.parentElement !== focusContainer) focusContainer.appendChild(contentElement);
13461
13543
  if (focusContainer.parentElement !== this.element) this.element.appendChild(focusContainer);
13462
13544
  const resize = () => {
13463
13545
  const panelId = panel.api.id;
13464
- if (this.pendingUpdates.has(panelId)) return;
13465
- this.pendingUpdates.add(panelId);
13466
- requestAnimationFrame(() => {
13546
+ const scheduling = this.map[panelId];
13547
+ if ((scheduling === null || scheduling === void 0 ? void 0 : scheduling.generation) !== generation) return;
13548
+ if (scheduling.pendingUpdate !== void 0) return;
13549
+ /**
13550
+ * Claim the slot *before* scheduling. `requestAnimationFrame` is
13551
+ * shimmed to run inline by some hosts (and by several suites here),
13552
+ * in which case the callback below clears `pendingUpdate` while
13553
+ * this call is still on the stack — assigning the handle afterwards
13554
+ * would resurrect a slot that is already spent and block every
13555
+ * later reposition. The sentinel is replaced by the real handle
13556
+ * only if the frame has not already run.
13557
+ */
13558
+ scheduling.pendingUpdate = SCHEDULED;
13559
+ const handle = requestAnimationFrame(() => {
13467
13560
  var _entry$forceVisible;
13468
- this.pendingUpdates.delete(panelId);
13469
13561
  const entry = this.map[panelId];
13470
- if (this.isDisposed || !entry) return;
13562
+ if (entry) entry.pendingUpdate = void 0;
13563
+ if (this.isDisposed || (entry === null || entry === void 0 ? void 0 : entry.generation) !== generation) return;
13471
13564
  const forceVisible = (_entry$forceVisible = entry.forceVisible) !== null && _entry$forceVisible !== void 0 ? _entry$forceVisible : false;
13472
13565
  const clip = entry.clip;
13473
13566
  const box = this.positionCache.getPosition(referenceContainer.element);
@@ -13476,6 +13569,15 @@ react_dom = __toESM(react_dom);
13476
13569
  const top = box.top - box2.top;
13477
13570
  const width = box.width;
13478
13571
  const height = box.height;
13572
+ if (entry.retainPreviousGeometry && (width === 0 || height === 0)) {
13573
+ if (!panel.api.isVisible && !forceVisible) {
13574
+ focusContainer.style.visibility = "hidden";
13575
+ focusContainer.style.pointerEvents = "none";
13576
+ }
13577
+ return;
13578
+ }
13579
+ entry.retainPreviousGeometry = false;
13580
+ entry.positioned = true;
13479
13581
  focusContainer.style.left = `${left}px`;
13480
13582
  focusContainer.style.top = `${top}px`;
13481
13583
  focusContainer.style.width = `${width}px`;
@@ -13504,11 +13606,30 @@ react_dom = __toESM(react_dom);
13504
13606
  } else focusContainer.style.clipPath = "";
13505
13607
  toggleClass(focusContainer, "dv-render-overlay-float", panel.group.api.location.type === "floating");
13506
13608
  });
13609
+ if (scheduling.pendingUpdate === SCHEDULED) scheduling.pendingUpdate = handle;
13507
13610
  };
13508
13611
  const visibilityChanged = () => {
13509
13612
  if (panel.api.isVisible) {
13613
+ var _this$map$panel$api$i;
13510
13614
  this.positionCache.invalidate();
13511
13615
  resize();
13616
+ /**
13617
+ * Existing geometry is safe to show while its replacement lays
13618
+ * out. `retainPreviousGeometry` is only set when geometry has
13619
+ * actually been written (see `attach`), so this can never
13620
+ * un-hide an overlay whose left/top/width/height are unset —
13621
+ * `.dv-render-overlay` is 100%/100% by default and would
13622
+ * otherwise cover the whole dock.
13623
+ *
13624
+ * The retained geometry is the panel's position in the previous
13625
+ * layout, so restoring a layout that moves the panel does paint
13626
+ * it at stale coordinates until the reposition lands. That is
13627
+ * bounded to the next frame now that a superseded `attach` can
13628
+ * no longer swallow the reposition (see `attach`); showing
13629
+ * stale-but-real geometry for a frame is the deliberate
13630
+ * trade-off against blanking the panel for the whole rebuild.
13631
+ */
13632
+ if ((_this$map$panel$api$i = this.map[panel.api.id]) === null || _this$map$panel$api$i === void 0 ? void 0 : _this$map$panel$api$i.retainPreviousGeometry) focusContainer.style.visibility = "";
13512
13633
  focusContainer.style.pointerEvents = "";
13513
13634
  } else {
13514
13635
  focusContainer.style.visibility = "hidden";
@@ -13725,7 +13846,17 @@ react_dom = __toESM(react_dom);
13725
13846
  window: externalWindow
13726
13847
  });
13727
13848
  return new Promise((resolve, reject) => {
13728
- externalWindow.addEventListener("unload", (e) => {});
13849
+ externalWindow.addEventListener("unload", () => {});
13850
+ /**
13851
+ * `load` is the only event that resolves this promise with a
13852
+ * container, so a window that goes away first would leave the
13853
+ * caller awaiting a promise that never settles - and
13854
+ * `addPopoutGroup` holds its layout transaction open until it does.
13855
+ * Settle with `null` on close, the same signal a blocked popup
13856
+ * gives and one the caller already handles. `resolve` after the
13857
+ * fact is a no-op, so a `load` that arrived first still wins.
13858
+ */
13859
+ disposable.addDisposables(_this.onWillClose(() => resolve(null)));
13729
13860
  externalWindow.addEventListener("load", () => {
13730
13861
  /**
13731
13862
  * @see https://developer.mozilla.org/en-US/docs/Web/API/Window/load_event
@@ -15125,6 +15256,7 @@ react_dom = __toESM(react_dom);
15125
15256
  this._onWillDrop = new Emitter();
15126
15257
  this.onWillDrop = this._onWillDrop.event;
15127
15258
  this._mutationDepth = 0;
15259
+ this._pendingLocationChanges = /* @__PURE__ */ new Map();
15128
15260
  this._origin = "user";
15129
15261
  this._originDepth = 0;
15130
15262
  this._onWillMutateLayout = new Emitter();
@@ -15222,7 +15354,7 @@ react_dom = __toESM(react_dom);
15222
15354
  toggleClass(this.element, "dv-debug", !!options.debug);
15223
15355
  this.updateTheme();
15224
15356
  if (options.debug) this.addDisposables(new StrictEventsSequencing(this));
15225
- this.addDisposables(this.rootDropTargetContainer, this.floatingDropTargetContainer, addDisposableListener(this._shellManager.element, "dragend", () => {
15357
+ this.addDisposables(Disposable.from(() => this._pendingLocationChanges.clear()), this.rootDropTargetContainer, this.floatingDropTargetContainer, addDisposableListener(this._shellManager.element, "dragend", () => {
15226
15358
  var _this$rootDropTargetC2, _this$floatingDropTar;
15227
15359
  (_this$rootDropTargetC2 = this.rootDropTargetContainer.model) === null || _this$rootDropTargetC2 === void 0 || _this$rootDropTargetC2.clear();
15228
15360
  (_this$floatingDropTar = this.floatingDropTargetContainer.model) === null || _this$floatingDropTar === void 0 || _this$floatingDropTar.clear();
@@ -15296,7 +15428,7 @@ react_dom = __toESM(react_dom);
15296
15428
  return (_this$_popoutWindowSe3 = (_this$_popoutWindowSe4 = this._popoutWindowService) === null || _this$_popoutWindowSe4 === void 0 || (_this$_popoutWindowSe4 = _this$_popoutWindowSe4.findByGroup(group)) === null || _this$_popoutWindowSe4 === void 0 ? void 0 : _this$_popoutWindowSe4.popupService) !== null && _this$_popoutWindowSe3 !== void 0 ? _this$_popoutWindowSe3 : this.popupService;
15297
15429
  }
15298
15430
  addPopoutGroup(itemToPopout, options) {
15299
- return this.mutation("popout", () => this._doAddPopoutGroup(itemToPopout, options));
15431
+ return this.mutationAsync("popout", () => this._doAddPopoutGroup(itemToPopout, options));
15300
15432
  }
15301
15433
  /** Enumerate the popout groups currently open in their own windows. */
15302
15434
  getPopouts() {
@@ -15393,15 +15525,32 @@ react_dom = __toESM(react_dom);
15393
15525
  }
15394
15526
  };
15395
15527
  let floatingBox;
15396
- if (!(options === null || options === void 0 ? void 0 : options.overridePopoutGroup) && !(options === null || options === void 0 ? void 0 : options.overridePopoutGridview) && isGroupAddedToDom) if (itemToPopout instanceof DockviewPanel) this.movingLock(() => {
15397
- const panel = referenceGroup.model.removePanel(itemToPopout);
15398
- group.model.openPanel(panel);
15399
- });
15400
- else {
15528
+ /**
15529
+ * Popping out rehomes panels into `group`, so like the floating
15530
+ * case it owes the caller an `onDidMovePanel` per panel. The
15531
+ * events are deferred until the window is wired up so listeners
15532
+ * observe the panels' final `popout` location.
15533
+ */
15534
+ let movedPanels = [];
15535
+ if (!(options === null || options === void 0 ? void 0 : options.overridePopoutGroup) && !(options === null || options === void 0 ? void 0 : options.overridePopoutGridview) && isGroupAddedToDom) if (itemToPopout instanceof DockviewPanel) {
15536
+ const sourceGroup = itemToPopout.group;
15537
+ this.movingLock(() => {
15538
+ const panel = referenceGroup.model.removePanel(itemToPopout);
15539
+ group.model.openPanel(panel);
15540
+ });
15541
+ movedPanels = [{
15542
+ panel: itemToPopout,
15543
+ from: sourceGroup
15544
+ }];
15545
+ } else {
15401
15546
  this.movingLock(() => moveGroupWithoutDestroying({
15402
15547
  from: referenceGroup,
15403
15548
  to: group
15404
15549
  }));
15550
+ movedPanels = group.panels.map((panel) => ({
15551
+ panel,
15552
+ from: referenceGroup
15553
+ }));
15405
15554
  switch (referenceLocation) {
15406
15555
  case "grid":
15407
15556
  referenceGroup.api.setVisible(false);
@@ -15510,6 +15659,7 @@ react_dom = __toESM(react_dom);
15510
15659
  group: value.popoutGroup,
15511
15660
  window: value.getWindow()
15512
15661
  });
15662
+ for (const { panel, from } of movedPanels) this.fireDidMovePanel(panel, from);
15513
15663
  return true;
15514
15664
  }).catch((err) => {
15515
15665
  console.error("dockview: failed to create popout.", err);
@@ -15587,6 +15737,16 @@ react_dom = __toESM(react_dom);
15587
15737
  const members = this.groups.filter((candidate) => popoutGridview.element.contains(candidate.element));
15588
15738
  const anchorPresent = members.includes(group);
15589
15739
  const anchorIsSoleMember = anchorPresent && members.length === 1;
15740
+ /**
15741
+ * Popping a group out fires `onDidMovePanel` per panel, so closing the
15742
+ * window owes the caller the same: every panel in it is rehomed, into
15743
+ * the reference group the popout left behind or into a fresh grid slot.
15744
+ * `movingLock` suppresses onDidAddPanel / onDidRemovePanel on these
15745
+ * paths, so `onDidMovePanel` is the only event that can carry it. The
15746
+ * events are deferred to the end so listeners observe the panels at
15747
+ * their final group and location.
15748
+ */
15749
+ const movedPanels = [];
15590
15750
  if (genuineClose) for (const member of members) {
15591
15751
  if (member === group) continue;
15592
15752
  this.movingLock(() => {
@@ -15597,12 +15757,21 @@ react_dom = __toESM(react_dom);
15597
15757
  });
15598
15758
  this.redockGroupToMainGrid(member);
15599
15759
  });
15760
+ for (const panel of member.panels) movedPanels.push({
15761
+ panel,
15762
+ from: member
15763
+ });
15600
15764
  }
15601
15765
  if (anchorPresent && isGroupAddedToDom && this.getPanel(referenceGroup.id)) {
15766
+ const rehomed = [...group.panels];
15602
15767
  this.movingLock(() => moveGroupWithoutDestroying({
15603
15768
  from: group,
15604
15769
  to: referenceGroup
15605
15770
  }));
15771
+ for (const panel of rehomed) movedPanels.push({
15772
+ panel,
15773
+ from: group
15774
+ });
15606
15775
  if (!referenceGroup.api.isVisible) referenceGroup.api.setVisible(true);
15607
15776
  if (this.getPanel(group.id)) this.doRemoveGroup(group, { skipPopoutAssociated: true });
15608
15777
  } else if (anchorPresent && this.getPanel(group.id)) {
@@ -15633,10 +15802,15 @@ react_dom = __toESM(react_dom);
15633
15802
  this.movingLock(() => {
15634
15803
  this.doAddGroup(group, [0]);
15635
15804
  });
15805
+ for (const panel of group.panels) movedPanels.push({
15806
+ panel,
15807
+ from: group
15808
+ });
15636
15809
  }
15637
15810
  this.doSetGroupAndPanelActive(group);
15638
15811
  }
15639
15812
  disposePopoutGridview();
15813
+ for (const { panel, from } of movedPanels) this.fireDidMovePanel(panel, from);
15640
15814
  }
15641
15815
  addFloatingGroup(item, options) {
15642
15816
  this.mutation("float", () => this._doAddFloatingGroup(item, options));
@@ -15645,7 +15819,15 @@ react_dom = __toESM(react_dom);
15645
15819
  if (!assertModule(this._floatingGroupService, "FloatingGroup", "api.addFloatingGroup")) return;
15646
15820
  if (item instanceof DockviewGroupPanel && item.model.location.type === "edge") return;
15647
15821
  let group;
15822
+ /**
15823
+ * Floating relocates panels exactly like a drop onto the grid does, so
15824
+ * it owes the caller an `onDidMovePanel` per panel. The events are
15825
+ * deferred until the window is mounted below: only then do the panels
15826
+ * report their final `floating` location.
15827
+ */
15828
+ let movedPanels = [];
15648
15829
  if (item instanceof DockviewPanel) {
15830
+ const sourceGroup = item.group;
15649
15831
  group = this.createGroup();
15650
15832
  this._onDidAddGroup.fire(group);
15651
15833
  this.movingLock(() => this.removePanel(item, {
@@ -15654,6 +15836,10 @@ react_dom = __toESM(react_dom);
15654
15836
  skipSetActiveGroup: true
15655
15837
  }));
15656
15838
  this.movingLock(() => group.model.openPanel(item, { skipSetGroupActive: true }));
15839
+ movedPanels = [{
15840
+ panel: item,
15841
+ from: sourceGroup
15842
+ }];
15657
15843
  } else {
15658
15844
  var _this$_popoutWindowSe8;
15659
15845
  group = item;
@@ -15670,11 +15856,21 @@ react_dom = __toESM(react_dom);
15670
15856
  });
15671
15857
  this.doRemoveGroup(popoutReferenceGroup, { skipDispose: true });
15672
15858
  group = popoutReferenceGroup;
15673
- } else this.doRemoveGroup(item, {
15674
- skipDispose: true,
15675
- skipPopoutReturn: true,
15676
- skipPopoutAssociated: false
15677
- });
15859
+ movedPanels = group.panels.map((panel) => ({
15860
+ panel,
15861
+ from: item
15862
+ }));
15863
+ } else {
15864
+ this.doRemoveGroup(item, {
15865
+ skipDispose: true,
15866
+ skipPopoutReturn: true,
15867
+ skipPopoutAssociated: false
15868
+ });
15869
+ movedPanels = group.panels.map((panel) => ({
15870
+ panel,
15871
+ from: group
15872
+ }));
15873
+ }
15678
15874
  }
15679
15875
  function getAnchoredBox() {
15680
15876
  if (options === null || options === void 0 ? void 0 : options.position) {
@@ -15707,6 +15903,7 @@ react_dom = __toESM(react_dom);
15707
15903
  skipActiveGroup: options === null || options === void 0 ? void 0 : options.skipActiveGroup,
15708
15904
  disableSmartGuides: options === null || options === void 0 ? void 0 : options.disableSmartGuides
15709
15905
  });
15906
+ for (const { panel, from } of movedPanels) this.fireDidMovePanel(panel, from);
15710
15907
  }
15711
15908
  /**
15712
15909
  * Build an empty gridview configured to match the main grid's styling, for
@@ -16138,35 +16335,119 @@ react_dom = __toESM(react_dom);
16138
16335
  if (typeof data !== "object" || data === null) throw new Error("dockview: serialized layout must be a non-null object");
16139
16336
  if (!data.grid || ((_data$grid$root = data.grid.root) === null || _data$grid$root === void 0 ? void 0 : _data$grid$root.type) !== "branch" || !Array.isArray(data.grid.root.data)) throw new Error("dockview: root must be of type branch");
16140
16337
  const existingPanels = /* @__PURE__ */ new Map();
16141
- let tempGroup;
16338
+ const temporaryGroups = /* @__PURE__ */ new Map();
16339
+ const stagedPanels = [];
16340
+ const temporaryGroupDisposables = [];
16341
+ /**
16342
+ * The staging groups are detached from `_groups` and never enter the
16343
+ * grid, so nothing else will ever tear them down.
16344
+ *
16345
+ * `dispose()` reaches consumer `IContentRenderer.dispose()`, so one
16346
+ * throwing renderer must not abort the rest of the cleanup — that would
16347
+ * re-introduce the very leak this reclaims — nor replace the
16348
+ * deserialization error the caller is being given. Draining the list
16349
+ * also makes this safe to call more than once.
16350
+ */
16351
+ const disposeTemporaryGroups = () => {
16352
+ for (const disposable of temporaryGroupDisposables) try {
16353
+ disposable.dispose();
16354
+ } catch (err) {
16355
+ console.error("dockview: failed to dispose a temporary group created for reuseExistingPanels", err);
16356
+ }
16357
+ temporaryGroupDisposables.length = 0;
16358
+ };
16142
16359
  if (options === null || options === void 0 ? void 0 : options.reuseExistingPanels) {
16143
16360
  /**
16144
- * What are we doing here?
16145
- *
16146
- * 1. Create a temporary group to hold any panels that currently exist and that also exist in the new layout
16147
- * 2. Remove that temporary group from the group mapping so that it doesn't get cleared when we clear the layout
16361
+ * Visible, always-rendered panels need individual staging groups
16362
+ * to remain active. Other reused panels can share a staging group.
16363
+ * The staging groups are excluded from the layout clear below.
16148
16364
  */
16149
- tempGroup = this.createGroup();
16150
- this._groups.delete(tempGroup.api.id);
16151
16365
  const newPanels = Object.keys(data.panels);
16152
- for (const panel of this.panels) if (newPanels.includes(panel.api.id)) existingPanels.set(panel.api.id, panel);
16153
- this.movingLock(() => {
16154
- Array.from(existingPanels.values()).forEach((panel) => {
16155
- this.moveGroupOrPanel({
16156
- from: {
16157
- groupId: panel.api.group.api.id,
16158
- panelId: panel.api.id
16159
- },
16160
- to: {
16161
- group: tempGroup,
16162
- position: "center"
16163
- },
16164
- keepEmptyGroups: true
16366
+ let sharedTemporaryGroup;
16367
+ const createTemporaryGroup = () => {
16368
+ const temporaryGroup = this.createGroup();
16369
+ /**
16370
+ * Removing the group from `_groups` also drops the record's
16371
+ * `CompositeDisposable`, so capture it first and tear both it
16372
+ * and the group down in the `finally` below. Left undisposed a
16373
+ * staging group leaks its `ResizeObserver`, its
16374
+ * `onDidOptionsChange` subscription (retained by this
16375
+ * component's emitter) and the watermark its model mounts when
16376
+ * the group empties — once per reused panel, per `fromJSON`.
16377
+ */
16378
+ const record = this._groups.get(temporaryGroup.api.id);
16379
+ this._groups.delete(temporaryGroup.api.id);
16380
+ temporaryGroupDisposables.push(Disposable.from(() => {
16381
+ /**
16382
+ * Reclaim the group's own resources, never its panels.
16383
+ * Staging is driven by the ids in `data.panels` while
16384
+ * reclaiming is driven by the group `views` that
16385
+ * reference them, so a panel whose state is present but
16386
+ * unreferenced is still staged here — and `dispose()`
16387
+ * on a non-empty group reaches the consumer's
16388
+ * `IContentRenderer.dispose()`. Emptying the group
16389
+ * first leaves those panels exactly as they were before
16390
+ * this teardown existed, which also keeps this safe
16391
+ * against any future path that rebuilds a panel under
16392
+ * an id that is live in the new layout.
16393
+ */
16394
+ this.movingLock(() => {
16395
+ for (const panel of temporaryGroup.panels.slice()) temporaryGroup.model.removePanel(panel);
16396
+ });
16397
+ record === null || record === void 0 || record.disposable.dispose();
16398
+ temporaryGroup.dispose();
16399
+ }));
16400
+ return temporaryGroup;
16401
+ };
16402
+ /**
16403
+ * Staging and the clear below run before the deserialization
16404
+ * `try`, so a throw here (a consumer `onDidRemovePanel` handler, a
16405
+ * renderer teardown during `clear`) would escape without reclaiming
16406
+ * the staging groups already created.
16407
+ *
16408
+ * The creation loop is inside the guard too, not just the moves and
16409
+ * the clear: `createGroup()` reaches consumer code of its own —
16410
+ * `initialize()` mounts the watermark through
16411
+ * `createWatermarkComponent()` — so a throw on the n-th panel would
16412
+ * otherwise strand the n-1 staging groups already built.
16413
+ */
16414
+ try {
16415
+ for (const panel of this.panels) if (newPanels.includes(panel.api.id)) {
16416
+ existingPanels.set(panel.api.id, panel);
16417
+ let temporaryGroup;
16418
+ if (panel.api.renderer === "always" && panel.api.isVisible) temporaryGroup = createTemporaryGroup();
16419
+ else {
16420
+ var _sharedTemporaryGroup;
16421
+ (_sharedTemporaryGroup = sharedTemporaryGroup) !== null && _sharedTemporaryGroup !== void 0 || (sharedTemporaryGroup = createTemporaryGroup());
16422
+ temporaryGroup = sharedTemporaryGroup;
16423
+ }
16424
+ temporaryGroups.set(panel.api.id, temporaryGroup);
16425
+ stagedPanels.push({
16426
+ panel,
16427
+ temporaryGroup
16428
+ });
16429
+ }
16430
+ this.movingLock(() => {
16431
+ stagedPanels.forEach(({ panel, temporaryGroup }) => {
16432
+ this.moveGroupOrPanel({
16433
+ from: {
16434
+ groupId: panel.api.group.api.id,
16435
+ panelId: panel.api.id
16436
+ },
16437
+ to: {
16438
+ group: temporaryGroup,
16439
+ position: "center"
16440
+ },
16441
+ keepEmptyGroups: true
16442
+ });
16165
16443
  });
16166
16444
  });
16167
- });
16168
- }
16169
- this.clear();
16445
+ this.clear();
16446
+ } catch (err) {
16447
+ disposeTemporaryGroups();
16448
+ throw err;
16449
+ }
16450
+ } else this.clear();
16170
16451
  const { grid, panels, activeGroup } = data;
16171
16452
  try {
16172
16453
  var _data$floatingGroups, _data$popoutGroups, _this$_popoutWindowSe12, _this$_floatingGroupS5, _this$_watermarkServi2;
@@ -16198,9 +16479,10 @@ react_dom = __toESM(react_dom);
16198
16479
  * due to a corruption of input data.
16199
16480
  */
16200
16481
  const existingPanel = existingPanels.get(child);
16201
- if (tempGroup && existingPanel) {
16482
+ const temporaryGroup = temporaryGroups.get(child);
16483
+ if (temporaryGroup && existingPanel) {
16202
16484
  this.movingLock(() => {
16203
- tempGroup.model.removePanel(existingPanel);
16485
+ temporaryGroup.model.removePanel(existingPanel);
16204
16486
  });
16205
16487
  createdPanels.push(existingPanel);
16206
16488
  existingPanel.updateFromStateModel(panels[child]);
@@ -16230,7 +16512,7 @@ react_dom = __toESM(react_dom);
16230
16512
  return createGroupFromSerializedState(node.data);
16231
16513
  } });
16232
16514
  this._layoutFromShell(width, height);
16233
- if (data.edgeGroups) this.deserializeEdgeGroups(data.edgeGroups, panels);
16515
+ if (data.edgeGroups) this.deserializeEdgeGroups(data.edgeGroups, panels, existingPanels, temporaryGroups);
16234
16516
  this.deserializeFloatingWindows((_data$floatingGroups = data.floatingGroups) !== null && _data$floatingGroups !== void 0 ? _data$floatingGroups : [], createGroupFromSerializedState);
16235
16517
  const popoutPromises = this.deserializePopoutWindows((_data$popoutGroups = data.popoutGroups) !== null && _data$popoutGroups !== void 0 ? _data$popoutGroups : [], createGroupFromSerializedState);
16236
16518
  (_this$_popoutWindowSe12 = this._popoutWindowService) === null || _this$_popoutWindowSe12 === void 0 || _this$_popoutWindowSe12.finishRestoration(popoutPromises);
@@ -16267,6 +16549,8 @@ react_dom = __toESM(react_dom);
16267
16549
  * expect trying to load a corrupted layout to result in an error and not silently fail...
16268
16550
  */
16269
16551
  throw err;
16552
+ } finally {
16553
+ disposeTemporaryGroups();
16270
16554
  }
16271
16555
  this.debouncedUpdateAllPositions();
16272
16556
  this._onDidLayoutFromJSON.fire();
@@ -16289,7 +16573,7 @@ react_dom = __toESM(react_dom);
16289
16573
  members
16290
16574
  };
16291
16575
  }
16292
- deserializeEdgeGroups(edgeGroups, panels) {
16576
+ deserializeEdgeGroups(edgeGroups, panels, existingPanels, temporaryGroups) {
16293
16577
  const edgeService = assertModule(this._edgeGroupService, "EdgeGroup", "fromJSON edge restoration");
16294
16578
  if (!edgeService) return;
16295
16579
  for (const _position of [
@@ -16319,13 +16603,40 @@ react_dom = __toESM(react_dom);
16319
16603
  if (groupState) {
16320
16604
  const { views, activeView } = groupState;
16321
16605
  const createdPanels = [];
16322
- for (const panelId of views) if (panels[panelId]) {
16323
- const panel = this._deserializer.fromJSON(panels[panelId], edgeGroup);
16324
- createdPanels.push(panel);
16606
+ for (const panelId of views) {
16607
+ if (!panels[panelId]) continue;
16608
+ /**
16609
+ * Reclaim a staged panel rather than rebuilding it, the
16610
+ * same way the grid path does. Deserializing here instead
16611
+ * would honour `reuseExistingPanels` everywhere except edge
16612
+ * groups: the live panel keeps sitting in its staging group
16613
+ * while a second panel is built under the same id, so the
16614
+ * consumer's renderer for the original is orphaned — never
16615
+ * disposed, and its element left inside the id-keyed
16616
+ * overlay entry the replacement now shares.
16617
+ */
16618
+ const existingPanel = existingPanels.get(panelId);
16619
+ const temporaryGroup = temporaryGroups.get(panelId);
16620
+ if (temporaryGroup && existingPanel) {
16621
+ this.movingLock(() => {
16622
+ temporaryGroup.model.removePanel(existingPanel);
16623
+ });
16624
+ createdPanels.push(existingPanel);
16625
+ existingPanel.updateFromStateModel(panels[panelId]);
16626
+ } else {
16627
+ const panel = this._deserializer.fromJSON(panels[panelId], edgeGroup);
16628
+ createdPanels.push(panel);
16629
+ }
16325
16630
  }
16326
16631
  for (const panel of createdPanels) {
16327
16632
  const isActive = activeView === panel.id;
16328
- edgeGroup.model.openPanel(panel, {
16633
+ if (existingPanels.has(panel.api.id)) this.movingLock(() => {
16634
+ edgeGroup.model.openPanel(panel, {
16635
+ skipSetActive: !isActive,
16636
+ skipSetGroupActive: true
16637
+ });
16638
+ });
16639
+ else edgeGroup.model.openPanel(panel, {
16329
16640
  skipSetActive: !isActive,
16330
16641
  skipSetGroupActive: true
16331
16642
  });
@@ -16749,22 +17060,100 @@ react_dom = __toESM(react_dom);
16749
17060
  * outermost mutation.
16750
17061
  */
16751
17062
  mutation(kind, func) {
16752
- const outer = this._mutationDepth === 0;
17063
+ const close = this.openMutation(kind);
17064
+ try {
17065
+ return func();
17066
+ } finally {
17067
+ close();
17068
+ }
17069
+ }
17070
+ /**
17071
+ * `mutation()` for an operation whose work continues after the synchronous
17072
+ * call returns. The transaction stays open until the returned promise
17073
+ * settles, so everything the operation does - the group it adds, the panels
17074
+ * it relocates - lands inside the bracket, matching the synchronous
17075
+ * move / float paths.
17076
+ */
17077
+ mutationAsync(kind, func) {
17078
+ var _this = this;
17079
+ return _asyncToGenerator(function* () {
17080
+ const close = _this.openMutation(kind);
17081
+ try {
17082
+ return yield func();
17083
+ } finally {
17084
+ close();
17085
+ }
17086
+ })();
17087
+ }
17088
+ /**
17089
+ * Open a transaction and return the function that closes it. Shared by the
17090
+ * synchronous and asynchronous brackets; the returned function must be
17091
+ * called exactly once.
17092
+ *
17093
+ * Both ends key off the depth counter reaching zero rather than off which
17094
+ * bracket opened first. For synchronous nesting the two are the same thing
17095
+ * - brackets close in the order they opened - but asynchronous transactions
17096
+ * can *overlap* rather than nest (two `addPopoutGroup` calls in flight at
17097
+ * once, or a restore staggering several), and there the first to open is
17098
+ * not the last to close. Reporting on the opener would fire `didMutate`
17099
+ * while the other transaction was still doing structural work, which is the
17100
+ * very thing the async bracket exists to prevent. Overlapping transactions
17101
+ * therefore report as one, tagged with the kind of the last to finish.
17102
+ */
17103
+ openMutation(kind) {
16753
17104
  const origin = this._origin;
16754
- if (outer) this._onWillMutateLayout.fire({
17105
+ if (this._mutationDepth === 0) this._onWillMutateLayout.fire({
16755
17106
  kind,
16756
17107
  origin
16757
17108
  });
16758
17109
  this._mutationDepth++;
16759
- try {
16760
- return func();
16761
- } finally {
17110
+ return () => {
16762
17111
  this._mutationDepth--;
16763
- if (outer) this._onDidMutateLayout.fire({
16764
- kind,
16765
- origin
16766
- });
17112
+ if (this._mutationDepth === 0) {
17113
+ this.flushLocationChanges();
17114
+ this._onDidMutateLayout.fire({
17115
+ kind,
17116
+ origin
17117
+ });
17118
+ }
17119
+ };
17120
+ }
17121
+ /**
17122
+ * Coalesce a panel's `onDidLocationChange` to the end of the enclosing
17123
+ * transaction, or fire it immediately when no transaction is in flight.
17124
+ *
17125
+ * Relocating a panel touches its location twice: once as it is reparented
17126
+ * into the destination group - which, when that group has just been created
17127
+ * for a floating or popout window, has not been told where it lives yet -
17128
+ * and once as the group is tagged with its final location. Reporting each
17129
+ * signal as it happens therefore leaks an intermediate `grid` location the
17130
+ * panel was never in, at a moment when it belongs to neither group's panel
17131
+ * list and so is missing from `api.panels`.
17132
+ *
17133
+ * Deferring collapses those signals into a single event carrying the
17134
+ * settled location. Note the signals are deliberately *not* compared
17135
+ * against the panel's previous location: a panel can move between two
17136
+ * floating windows without its location type changing, and this event is
17137
+ * the only signal `OverlayRenderContainer` has to re-resolve the panel's
17138
+ * z-index against its new host window.
17139
+ */
17140
+ deferLocationChange(key, fire) {
17141
+ if (this._mutationDepth === 0) {
17142
+ fire();
17143
+ return;
16767
17144
  }
17145
+ this._pendingLocationChanges.set(key, fire);
17146
+ }
17147
+ /**
17148
+ * Report every panel whose location moved during the transaction that has
17149
+ * just closed. Each callback reads the panel's location at this point, so
17150
+ * what it reports is where the panel ended up.
17151
+ */
17152
+ flushLocationChanges() {
17153
+ if (this._pendingLocationChanges.size === 0) return;
17154
+ const pending = Array.from(this._pendingLocationChanges.values());
17155
+ this._pendingLocationChanges.clear();
17156
+ for (const fire of pending) fire();
16768
17157
  }
16769
17158
  /**
16770
17159
  * The origin of the operation currently in progress (`'user'` by default).
@@ -16805,6 +17194,18 @@ react_dom = __toESM(react_dom);
16805
17194
  origin: this._origin
16806
17195
  });
16807
17196
  }
17197
+ /**
17198
+ * Announce that `panel` has finished relocating. Must be called once the
17199
+ * move has settled: `to` is read from the panel's current group, so an
17200
+ * early call would report the source group as the destination.
17201
+ */
17202
+ fireDidMovePanel(panel, from) {
17203
+ this._onDidMovePanel.fire({
17204
+ panel,
17205
+ from,
17206
+ to: panel.group
17207
+ });
17208
+ }
16808
17209
  moveGroupOrPanel(options) {
16809
17210
  this.mutation("move", () => this._doMoveGroupOrPanel(options));
16810
17211
  }
@@ -16865,10 +17266,7 @@ react_dom = __toESM(react_dom);
16865
17266
  });
16866
17267
  });
16867
17268
  if (!options.skipSetActive) this.doSetGroupAndPanelActive(destinationGroup);
16868
- this._onDidMovePanel.fire({
16869
- panel: removedPanel,
16870
- from: sourceGroup
16871
- });
17269
+ this.fireDidMovePanel(removedPanel, sourceGroup);
16872
17270
  } else {
16873
17271
  /**
16874
17272
  * Dropping a panel to the extremities of a group which will place that panel
@@ -16887,10 +17285,7 @@ react_dom = __toESM(react_dom);
16887
17285
  const [sourceParentLocation, from] = tail(getGridLocation(sourceGroup.element));
16888
17286
  if (sequenceEquals(sourceParentLocation, targetParentLocation)) {
16889
17287
  this.gridview.moveView(sourceParentLocation, from, to);
16890
- this._onDidMovePanel.fire({
16891
- panel: this.getGroupPanel(sourceItemId),
16892
- from: sourceGroup
16893
- });
17288
+ this.fireDidMovePanel(this.getGroupPanel(sourceItemId), sourceGroup);
16894
17289
  return;
16895
17290
  }
16896
17291
  }
@@ -16921,10 +17316,7 @@ react_dom = __toESM(react_dom);
16921
17316
  const newGroup = this.createGroupAtLocation(updatedTargetLocation, void 0, void 0, destinationGridview);
16922
17317
  this.movingLock(() => newGroup.model.openPanel(removedPanel, { skipSetActive: true }));
16923
17318
  this.doSetGroupAndPanelActive(newGroup);
16924
- this._onDidMovePanel.fire({
16925
- panel: this.getGroupPanel(sourceItemId),
16926
- from: sourceGroup
16927
- });
17319
+ this.fireDidMovePanel(this.getGroupPanel(sourceItemId), sourceGroup);
16928
17320
  return;
16929
17321
  }
16930
17322
  if (sourceGroup.api.location.type === "edge") {
@@ -16941,10 +17333,7 @@ react_dom = __toESM(react_dom);
16941
17333
  const newGroup = this.createGroupAtLocation(targetLocation, void 0, void 0, destinationGridview);
16942
17334
  this.movingLock(() => newGroup.model.openPanel(removedPanel, { skipSetGroupActive: true }));
16943
17335
  this.doSetGroupAndPanelActive(newGroup);
16944
- this._onDidMovePanel.fire({
16945
- panel: removedPanel,
16946
- from: sourceGroup
16947
- });
17336
+ this.fireDidMovePanel(removedPanel, sourceGroup);
16948
17337
  return;
16949
17338
  }
16950
17339
  const targetGroup = this.movingLock(() => this.doRemoveGroup(sourceGroup, {
@@ -16956,10 +17345,7 @@ react_dom = __toESM(react_dom);
16956
17345
  this.movingLock(() => this.doAddGroup(targetGroup, location, void 0, destinationGridview));
16957
17346
  this.setGroupLocationForRoot(targetGroup, destinationGridview);
16958
17347
  this.doSetGroupAndPanelActive(targetGroup);
16959
- this._onDidMovePanel.fire({
16960
- panel: this.getGroupPanel(sourceItemId),
16961
- from: sourceGroup
16962
- });
17348
+ this.fireDidMovePanel(this.getGroupPanel(sourceItemId), sourceGroup);
16963
17349
  } else {
16964
17350
  /**
16965
17351
  * The group we are removing from has many panels, we need to remove the panels we are moving,
@@ -16974,10 +17360,7 @@ react_dom = __toESM(react_dom);
16974
17360
  const group = this.createGroupAtLocation(dropLocation, void 0, void 0, destinationGridview);
16975
17361
  this.movingLock(() => group.model.openPanel(removedPanel, { skipSetGroupActive: true }));
16976
17362
  this.doSetGroupAndPanelActive(group);
16977
- this._onDidMovePanel.fire({
16978
- panel: removedPanel,
16979
- from: sourceGroup
16980
- });
17363
+ this.fireDidMovePanel(removedPanel, sourceGroup);
16981
17364
  }
16982
17365
  }
16983
17366
  }
@@ -17012,10 +17395,7 @@ react_dom = __toESM(react_dom);
17012
17395
  });
17013
17396
  for (const panel of removedPanels) targetGroup.model.addPanelToTabGroup(newTabGroup.id, panel.id);
17014
17397
  if (!options.skipSetActive) this.doSetGroupAndPanelActive(targetGroup);
17015
- for (const panel of removedPanels) this._onDidMovePanel.fire({
17016
- panel,
17017
- from: sourceGroup
17018
- });
17398
+ for (const panel of removedPanels) this.fireDidMovePanel(panel, sourceGroup);
17019
17399
  };
17020
17400
  let targetGroup;
17021
17401
  if (!destinationTarget || destinationTarget === "center" || !referenceLocation) targetGroup = destinationGroup;
@@ -17036,10 +17416,12 @@ react_dom = __toESM(react_dom);
17036
17416
  this.mutation("maximize", () => super.exitMaximizedGroup());
17037
17417
  }
17038
17418
  _doMoveGroup(options) {
17419
+ var _mergedPanels;
17039
17420
  const from = options.from.group;
17040
17421
  const to = options.to.group;
17041
17422
  const target = options.to.position;
17042
17423
  let source = from;
17424
+ let mergedPanels;
17043
17425
  if (target === "center") {
17044
17426
  const activePanel = from.activePanel;
17045
17427
  const tabGroupSnapshots = from.model.getTabGroups().map((tg) => ({
@@ -17057,6 +17439,7 @@ react_dom = __toESM(react_dom);
17057
17439
  skipSetGroupActive: true
17058
17440
  });
17059
17441
  });
17442
+ mergedPanels = panels;
17060
17443
  for (const snapshot of tabGroupSnapshots) {
17061
17444
  const newTabGroup = to.model.createTabGroup({
17062
17445
  label: snapshot.label,
@@ -17146,11 +17529,8 @@ react_dom = __toESM(react_dom);
17146
17529
  this.setGroupLocationForRoot(source, destGridview);
17147
17530
  }
17148
17531
  }
17149
- source.panels.forEach((panel) => {
17150
- this._onDidMovePanel.fire({
17151
- panel,
17152
- from
17153
- });
17532
+ ((_mergedPanels = mergedPanels) !== null && _mergedPanels !== void 0 ? _mergedPanels : source.panels).forEach((panel) => {
17533
+ this.fireDidMovePanel(panel, from);
17154
17534
  });
17155
17535
  this.debouncedUpdateAllPositions();
17156
17536
  if (options.skipSetActive === false) {