dockview-react 4.6.2 → 4.7.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 4.6.2
3
+ * @version 4.7.0
4
4
  * @link https://github.com/mathuo/dockview
5
5
  * @license MIT
6
6
  */
@@ -2135,6 +2135,19 @@ define(['exports', 'react', 'react-dom'], (function (exports, React, ReactDOM) {
2135
2135
  }
2136
2136
  throw new Error('invalid node');
2137
2137
  }
2138
+ function cloneNode(node, size, orthogonalSize) {
2139
+ if (node instanceof BranchNode) {
2140
+ const result = new BranchNode(node.orientation, node.proportionalLayout, node.styles, size, orthogonalSize, node.disabled, node.margin);
2141
+ for (let i = node.children.length - 1; i >= 0; i--) {
2142
+ const child = node.children[i];
2143
+ result.addChild(cloneNode(child, child.size, child.orthogonalSize), child.size, 0, true);
2144
+ }
2145
+ return result;
2146
+ }
2147
+ else {
2148
+ return new LeafNode(node.view, node.orientation, orthogonalSize);
2149
+ }
2150
+ }
2138
2151
  function flipNode(node, size, orthogonalSize) {
2139
2152
  if (node instanceof BranchNode) {
2140
2153
  const result = new BranchNode(orthogonal(node.orientation), node.proportionalLayout, node.styles, size, orthogonalSize, node.disabled, node.margin);
@@ -2485,6 +2498,29 @@ define(['exports', 'react', 'react-dom'], (function (exports, React, ReactDOM) {
2485
2498
  this._onDidChange.fire(e);
2486
2499
  });
2487
2500
  }
2501
+ normalize() {
2502
+ if (!this._root) {
2503
+ return;
2504
+ }
2505
+ if (this._root.children.length !== 1) {
2506
+ return;
2507
+ }
2508
+ const oldRoot = this.root;
2509
+ // can remove one level of redundant branching if there is only a single child
2510
+ const childReference = oldRoot.children[0];
2511
+ if (childReference instanceof LeafNode) {
2512
+ return;
2513
+ }
2514
+ oldRoot.element.remove();
2515
+ const child = oldRoot.removeChild(0); // Remove child to prevent double disposal
2516
+ oldRoot.dispose(); // Dispose old root (won't dispose removed child)
2517
+ child.dispose(); // Dispose the removed child
2518
+ this._root = cloneNode(childReference, childReference.size, childReference.orthogonalSize);
2519
+ this.element.appendChild(this._root.element);
2520
+ this.disposable.value = this._root.onDidChange((e) => {
2521
+ this._onDidChange.fire(e);
2522
+ });
2523
+ }
2488
2524
  /**
2489
2525
  * If the root is orientated as a VERTICAL node then nest the existing root within a new HORIZIONTAL root node
2490
2526
  * If the root is orientated as a HORIZONTAL node then nest the existing root within a new VERITCAL root node
@@ -3876,6 +3912,48 @@ define(['exports', 'react', 'react-dom'], (function (exports, React, ReactDOM) {
3876
3912
  }
3877
3913
  }
3878
3914
 
3915
+ function setGPUOptimizedBounds(element, bounds) {
3916
+ const { top, left, width, height } = bounds;
3917
+ const topPx = `${Math.round(top)}px`;
3918
+ const leftPx = `${Math.round(left)}px`;
3919
+ const widthPx = `${Math.round(width)}px`;
3920
+ const heightPx = `${Math.round(height)}px`;
3921
+ // Use traditional positioning but maintain GPU layer
3922
+ element.style.top = topPx;
3923
+ element.style.left = leftPx;
3924
+ element.style.width = widthPx;
3925
+ element.style.height = heightPx;
3926
+ element.style.visibility = 'visible';
3927
+ // Ensure GPU layer is maintained
3928
+ if (!element.style.transform || element.style.transform === '') {
3929
+ element.style.transform = 'translate3d(0, 0, 0)';
3930
+ }
3931
+ }
3932
+ function setGPUOptimizedBoundsFromStrings(element, bounds) {
3933
+ const { top, left, width, height } = bounds;
3934
+ // Use traditional positioning but maintain GPU layer
3935
+ element.style.top = top;
3936
+ element.style.left = left;
3937
+ element.style.width = width;
3938
+ element.style.height = height;
3939
+ element.style.visibility = 'visible';
3940
+ // Ensure GPU layer is maintained
3941
+ if (!element.style.transform || element.style.transform === '') {
3942
+ element.style.transform = 'translate3d(0, 0, 0)';
3943
+ }
3944
+ }
3945
+ function checkBoundsChanged(element, bounds) {
3946
+ const { top, left, width, height } = bounds;
3947
+ const topPx = `${Math.round(top)}px`;
3948
+ const leftPx = `${Math.round(left)}px`;
3949
+ const widthPx = `${Math.round(width)}px`;
3950
+ const heightPx = `${Math.round(height)}px`;
3951
+ // Check if position or size changed (back to traditional method)
3952
+ return element.style.top !== topPx ||
3953
+ element.style.left !== leftPx ||
3954
+ element.style.width !== widthPx ||
3955
+ element.style.height !== heightPx;
3956
+ }
3879
3957
  class WillShowOverlayEvent extends DockviewEvent {
3880
3958
  get nativeEvent() {
3881
3959
  return this.options.nativeEvent;
@@ -4157,21 +4235,11 @@ define(['exports', 'react', 'react-dom'], (function (exports, React, ReactDOM) {
4157
4235
  box.left = rootLeft + width - 4;
4158
4236
  box.width = 4;
4159
4237
  }
4160
- const topPx = `${Math.round(box.top)}px`;
4161
- const leftPx = `${Math.round(box.left)}px`;
4162
- const widthPx = `${Math.round(box.width)}px`;
4163
- const heightPx = `${Math.round(box.height)}px`;
4164
- if (overlay.style.top === topPx &&
4165
- overlay.style.left === leftPx &&
4166
- overlay.style.width === widthPx &&
4167
- overlay.style.height === heightPx) {
4238
+ // Use GPU-optimized bounds checking and setting
4239
+ if (!checkBoundsChanged(overlay, box)) {
4168
4240
  return;
4169
4241
  }
4170
- overlay.style.top = topPx;
4171
- overlay.style.left = leftPx;
4172
- overlay.style.width = widthPx;
4173
- overlay.style.height = heightPx;
4174
- overlay.style.visibility = 'visible';
4242
+ setGPUOptimizedBounds(overlay, box);
4175
4243
  overlay.className = `dv-drop-target-anchor${this.options.className ? ` ${this.options.className}` : ''}`;
4176
4244
  toggleClass(overlay, 'dv-drop-target-left', isLeft);
4177
4245
  toggleClass(overlay, 'dv-drop-target-right', isRight);
@@ -4223,10 +4291,7 @@ define(['exports', 'react', 'react-dom'], (function (exports, React, ReactDOM) {
4223
4291
  box.top = `${100 * (1 - size)}%`;
4224
4292
  box.height = `${100 * size}%`;
4225
4293
  }
4226
- this.overlayElement.style.top = box.top;
4227
- this.overlayElement.style.left = box.left;
4228
- this.overlayElement.style.width = box.width;
4229
- this.overlayElement.style.height = box.height;
4294
+ setGPUOptimizedBoundsFromStrings(this.overlayElement, box);
4230
4295
  toggleClass(this.overlayElement, 'dv-drop-target-small-vertical', isSmallY);
4231
4296
  toggleClass(this.overlayElement, 'dv-drop-target-small-horizontal', isSmallX);
4232
4297
  toggleClass(this.overlayElement, 'dv-drop-target-left', isLeft);
@@ -7669,7 +7734,36 @@ define(['exports', 'react', 'react-dom'], (function (exports, React, ReactDOM) {
7669
7734
 
7670
7735
  const DEFAULT_FLOATING_GROUP_OVERFLOW_SIZE = 100;
7671
7736
  const DEFAULT_FLOATING_GROUP_POSITION = { left: 100, top: 100, width: 300, height: 300 };
7737
+ const DESERIALIZATION_POPOUT_DELAY_MS = 100;
7672
7738
 
7739
+ class PositionCache {
7740
+ constructor() {
7741
+ this.cache = new Map();
7742
+ this.currentFrameId = 0;
7743
+ this.rafId = null;
7744
+ }
7745
+ getPosition(element) {
7746
+ const cached = this.cache.get(element);
7747
+ if (cached && cached.frameId === this.currentFrameId) {
7748
+ return cached.rect;
7749
+ }
7750
+ this.scheduleFrameUpdate();
7751
+ const rect = getDomNodePagePosition(element);
7752
+ this.cache.set(element, { rect, frameId: this.currentFrameId });
7753
+ return rect;
7754
+ }
7755
+ invalidate() {
7756
+ this.currentFrameId++;
7757
+ }
7758
+ scheduleFrameUpdate() {
7759
+ if (this.rafId)
7760
+ return;
7761
+ this.rafId = requestAnimationFrame(() => {
7762
+ this.currentFrameId++;
7763
+ this.rafId = null;
7764
+ });
7765
+ }
7766
+ }
7673
7767
  function createFocusableElement() {
7674
7768
  const element = document.createElement('div');
7675
7769
  element.tabIndex = -1;
@@ -7682,6 +7776,8 @@ define(['exports', 'react', 'react-dom'], (function (exports, React, ReactDOM) {
7682
7776
  this.accessor = accessor;
7683
7777
  this.map = {};
7684
7778
  this._disposed = false;
7779
+ this.positionCache = new PositionCache();
7780
+ this.pendingUpdates = new Set();
7685
7781
  this.addDisposables(exports.DockviewDisposable.from(() => {
7686
7782
  for (const value of Object.values(this.map)) {
7687
7783
  value.disposable.dispose();
@@ -7690,6 +7786,19 @@ define(['exports', 'react', 'react-dom'], (function (exports, React, ReactDOM) {
7690
7786
  this._disposed = true;
7691
7787
  }));
7692
7788
  }
7789
+ updateAllPositions() {
7790
+ if (this._disposed) {
7791
+ return;
7792
+ }
7793
+ // Invalidate position cache to force recalculation
7794
+ this.positionCache.invalidate();
7795
+ // Call resize function directly for all visible panels
7796
+ for (const entry of Object.values(this.map)) {
7797
+ if (entry.panel.api.isVisible && entry.resize) {
7798
+ entry.resize();
7799
+ }
7800
+ }
7801
+ }
7693
7802
  detatch(panel) {
7694
7803
  if (this.map[panel.api.id]) {
7695
7804
  const { disposable, destroy } = this.map[panel.api.id];
@@ -7720,17 +7829,33 @@ define(['exports', 'react', 'react-dom'], (function (exports, React, ReactDOM) {
7720
7829
  this.element.appendChild(focusContainer);
7721
7830
  }
7722
7831
  const resize = () => {
7723
- // TODO propagate position to avoid getDomNodePagePosition calls, possible performance bottleneck?
7724
- const box = getDomNodePagePosition(referenceContainer.element);
7725
- const box2 = getDomNodePagePosition(this.element);
7726
- focusContainer.style.left = `${box.left - box2.left}px`;
7727
- focusContainer.style.top = `${box.top - box2.top}px`;
7728
- focusContainer.style.width = `${box.width}px`;
7729
- focusContainer.style.height = `${box.height}px`;
7730
- toggleClass(focusContainer, 'dv-render-overlay-float', panel.group.api.location.type === 'floating');
7832
+ const panelId = panel.api.id;
7833
+ if (this.pendingUpdates.has(panelId)) {
7834
+ return; // Update already scheduled
7835
+ }
7836
+ this.pendingUpdates.add(panelId);
7837
+ requestAnimationFrame(() => {
7838
+ this.pendingUpdates.delete(panelId);
7839
+ if (this.isDisposed || !this.map[panelId]) {
7840
+ return;
7841
+ }
7842
+ const box = this.positionCache.getPosition(referenceContainer.element);
7843
+ const box2 = this.positionCache.getPosition(this.element);
7844
+ // Use traditional positioning for overlay containers
7845
+ const left = box.left - box2.left;
7846
+ const top = box.top - box2.top;
7847
+ const width = box.width;
7848
+ const height = box.height;
7849
+ focusContainer.style.left = `${left}px`;
7850
+ focusContainer.style.top = `${top}px`;
7851
+ focusContainer.style.width = `${width}px`;
7852
+ focusContainer.style.height = `${height}px`;
7853
+ toggleClass(focusContainer, 'dv-render-overlay-float', panel.group.api.location.type === 'floating');
7854
+ });
7731
7855
  };
7732
7856
  const visibilityChanged = () => {
7733
7857
  if (panel.api.isVisible) {
7858
+ this.positionCache.invalidate();
7734
7859
  resize();
7735
7860
  }
7736
7861
  focusContainer.style.display = panel.api.isVisible ? '' : 'none';
@@ -7825,6 +7950,8 @@ define(['exports', 'react', 'react-dom'], (function (exports, React, ReactDOM) {
7825
7950
  this.map[panel.api.id].disposable.dispose();
7826
7951
  // and reset the disposable to the active reference-container
7827
7952
  this.map[panel.api.id].disposable = disposable;
7953
+ // store the resize function for direct access
7954
+ this.map[panel.api.id].resize = resize;
7828
7955
  return focusContainer;
7829
7956
  }
7830
7957
  }
@@ -8194,6 +8321,13 @@ define(['exports', 'react', 'react-dom'], (function (exports, React, ReactDOM) {
8194
8321
  get floatingGroups() {
8195
8322
  return this._floatingGroups;
8196
8323
  }
8324
+ /**
8325
+ * Promise that resolves when all popout groups from the last fromJSON call are restored.
8326
+ * Useful for tests that need to wait for delayed popout creation.
8327
+ */
8328
+ get popoutRestorationPromise() {
8329
+ return this._popoutRestorationPromise;
8330
+ }
8197
8331
  constructor(container, options) {
8198
8332
  var _a, _b, _c;
8199
8333
  super(container, {
@@ -8242,6 +8376,7 @@ define(['exports', 'react', 'react-dom'], (function (exports, React, ReactDOM) {
8242
8376
  this.onDidMaximizedGroupChange = this._onDidMaximizedGroupChange.event;
8243
8377
  this._floatingGroups = [];
8244
8378
  this._popoutGroups = [];
8379
+ this._popoutRestorationPromise = Promise.resolve();
8245
8380
  this._onDidRemoveGroup = new Emitter();
8246
8381
  this.onDidRemoveGroup = this._onDidRemoveGroup.event;
8247
8382
  this._onDidAddGroup = new Emitter();
@@ -8791,6 +8926,7 @@ define(['exports', 'react', 'react-dom'], (function (exports, React, ReactDOM) {
8791
8926
  this.updateWatermark();
8792
8927
  }
8793
8928
  orthogonalize(position, options) {
8929
+ this.gridview.normalize();
8794
8930
  switch (position) {
8795
8931
  case 'top':
8796
8932
  case 'bottom':
@@ -9034,18 +9170,30 @@ define(['exports', 'react', 'react-dom'], (function (exports, React, ReactDOM) {
9034
9170
  });
9035
9171
  }
9036
9172
  const serializedPopoutGroups = (_b = data.popoutGroups) !== null && _b !== void 0 ? _b : [];
9037
- for (const serializedPopoutGroup of serializedPopoutGroups) {
9173
+ // Create a promise that resolves when all popout groups are created
9174
+ const popoutPromises = [];
9175
+ // Queue popup group creation with delays to avoid browser blocking
9176
+ serializedPopoutGroups.forEach((serializedPopoutGroup, index) => {
9038
9177
  const { data, position, gridReferenceGroup, url } = serializedPopoutGroup;
9039
9178
  const group = createGroupFromSerializedState(data);
9040
- this.addPopoutGroup(group, {
9041
- position: position !== null && position !== void 0 ? position : undefined,
9042
- overridePopoutGroup: gridReferenceGroup ? group : undefined,
9043
- referenceGroup: gridReferenceGroup
9044
- ? this.getPanel(gridReferenceGroup)
9045
- : undefined,
9046
- popoutUrl: url,
9179
+ // Add a small delay for each popup after the first to avoid browser popup blocking
9180
+ const popoutPromise = new Promise((resolve) => {
9181
+ setTimeout(() => {
9182
+ this.addPopoutGroup(group, {
9183
+ position: position !== null && position !== void 0 ? position : undefined,
9184
+ overridePopoutGroup: gridReferenceGroup ? group : undefined,
9185
+ referenceGroup: gridReferenceGroup
9186
+ ? this.getPanel(gridReferenceGroup)
9187
+ : undefined,
9188
+ popoutUrl: url,
9189
+ });
9190
+ resolve();
9191
+ }, index * DESERIALIZATION_POPOUT_DELAY_MS); // 100ms delay between each popup
9047
9192
  });
9048
- }
9193
+ popoutPromises.push(popoutPromise);
9194
+ });
9195
+ // Store the promise for tests to wait on
9196
+ this._popoutRestorationPromise = Promise.all(popoutPromises).then(() => void 0);
9049
9197
  for (const floatingGroup of this._floatingGroups) {
9050
9198
  floatingGroup.overlay.setBounds();
9051
9199
  }
@@ -9092,6 +9240,10 @@ define(['exports', 'react', 'react-dom'], (function (exports, React, ReactDOM) {
9092
9240
  throw err;
9093
9241
  }
9094
9242
  this.updateWatermark();
9243
+ // Force position updates for always visible panels after DOM layout is complete
9244
+ requestAnimationFrame(() => {
9245
+ this.overlayRenderContainer.updateAllPositions();
9246
+ });
9095
9247
  this._onDidLayoutFromJSON.fire();
9096
9248
  }
9097
9249
  clear() {
@@ -9592,7 +9744,6 @@ define(['exports', 'react', 'react-dom'], (function (exports, React, ReactDOM) {
9592
9744
  const target = options.to.position;
9593
9745
  if (target === 'center') {
9594
9746
  const activePanel = from.activePanel;
9595
- const targetActivePanel = to.activePanel;
9596
9747
  const panels = this.movingLock(() => [...from.panels].map((p) => from.model.removePanel(p.id, {
9597
9748
  skipSetActive: true,
9598
9749
  })));
@@ -9602,22 +9753,21 @@ define(['exports', 'react', 'react-dom'], (function (exports, React, ReactDOM) {
9602
9753
  this.movingLock(() => {
9603
9754
  for (const panel of panels) {
9604
9755
  to.model.openPanel(panel, {
9605
- skipSetActive: true, // Always skip setting panels active during move
9756
+ skipSetActive: panel !== activePanel,
9606
9757
  skipSetGroupActive: true,
9607
9758
  });
9608
9759
  }
9609
9760
  });
9610
- if (!options.skipSetActive) {
9611
- // Make the moved panel (from the source group) active
9612
- if (activePanel) {
9613
- this.doSetGroupAndPanelActive(to);
9614
- }
9761
+ // Ensure group becomes active after move
9762
+ if (options.skipSetActive !== true) {
9763
+ // For center moves (merges), we need to ensure the target group is active
9764
+ // unless explicitly told not to (skipSetActive: true)
9765
+ this.doSetGroupAndPanelActive(to);
9615
9766
  }
9616
- else if (targetActivePanel) {
9617
- // Ensure the target group's original active panel remains active
9618
- to.model.openPanel(targetActivePanel, {
9619
- skipSetGroupActive: true
9620
- });
9767
+ else if (!this.activePanel) {
9768
+ // Even with skipSetActive: true, ensure there's an active panel if none exists
9769
+ // This maintains basic functionality while respecting skipSetActive
9770
+ this.doSetGroupAndPanelActive(to);
9621
9771
  }
9622
9772
  }
9623
9773
  else {
@@ -9647,20 +9797,26 @@ define(['exports', 'react', 'react-dom'], (function (exports, React, ReactDOM) {
9647
9797
  if (selectedPopoutGroup.referenceGroup) {
9648
9798
  const referenceGroup = this.getPanel(selectedPopoutGroup.referenceGroup);
9649
9799
  if (referenceGroup && !referenceGroup.api.isVisible) {
9650
- this.doRemoveGroup(referenceGroup, { skipActive: true });
9800
+ this.doRemoveGroup(referenceGroup, {
9801
+ skipActive: true,
9802
+ });
9651
9803
  }
9652
9804
  }
9653
9805
  // Manually dispose the window without triggering restoration
9654
9806
  selectedPopoutGroup.window.dispose();
9655
9807
  // Update group's location and containers for target
9656
9808
  if (to.api.location.type === 'grid') {
9657
- from.model.renderContainer = this.overlayRenderContainer;
9658
- from.model.dropTargetContainer = this.rootDropTargetContainer;
9809
+ from.model.renderContainer =
9810
+ this.overlayRenderContainer;
9811
+ from.model.dropTargetContainer =
9812
+ this.rootDropTargetContainer;
9659
9813
  from.model.location = { type: 'grid' };
9660
9814
  }
9661
9815
  else if (to.api.location.type === 'floating') {
9662
- from.model.renderContainer = this.overlayRenderContainer;
9663
- from.model.dropTargetContainer = this.rootDropTargetContainer;
9816
+ from.model.renderContainer =
9817
+ this.overlayRenderContainer;
9818
+ from.model.dropTargetContainer =
9819
+ this.rootDropTargetContainer;
9664
9820
  from.model.location = { type: 'floating' };
9665
9821
  }
9666
9822
  break;
@@ -9728,8 +9884,12 @@ define(['exports', 'react', 'react-dom'], (function (exports, React, ReactDOM) {
9728
9884
  from.panels.forEach((panel) => {
9729
9885
  this._onDidMovePanel.fire({ panel, from });
9730
9886
  });
9731
- if (!options.skipSetActive) {
9732
- this.doSetGroupAndPanelActive(from);
9887
+ // Ensure group becomes active after move
9888
+ if (options.skipSetActive === false) {
9889
+ // Only activate when explicitly requested (skipSetActive: false)
9890
+ // Use 'to' group for non-center moves since 'from' may have been destroyed
9891
+ const targetGroup = to !== null && to !== void 0 ? to : from;
9892
+ this.doSetGroupAndPanelActive(targetGroup);
9733
9893
  }
9734
9894
  }
9735
9895
  doSetGroupActive(group) {