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
  */
@@ -2139,6 +2139,19 @@
2139
2139
  }
2140
2140
  throw new Error('invalid node');
2141
2141
  }
2142
+ function cloneNode(node, size, orthogonalSize) {
2143
+ if (node instanceof BranchNode) {
2144
+ const result = new BranchNode(node.orientation, node.proportionalLayout, node.styles, size, orthogonalSize, node.disabled, node.margin);
2145
+ for (let i = node.children.length - 1; i >= 0; i--) {
2146
+ const child = node.children[i];
2147
+ result.addChild(cloneNode(child, child.size, child.orthogonalSize), child.size, 0, true);
2148
+ }
2149
+ return result;
2150
+ }
2151
+ else {
2152
+ return new LeafNode(node.view, node.orientation, orthogonalSize);
2153
+ }
2154
+ }
2142
2155
  function flipNode(node, size, orthogonalSize) {
2143
2156
  if (node instanceof BranchNode) {
2144
2157
  const result = new BranchNode(orthogonal(node.orientation), node.proportionalLayout, node.styles, size, orthogonalSize, node.disabled, node.margin);
@@ -2489,6 +2502,29 @@
2489
2502
  this._onDidChange.fire(e);
2490
2503
  });
2491
2504
  }
2505
+ normalize() {
2506
+ if (!this._root) {
2507
+ return;
2508
+ }
2509
+ if (this._root.children.length !== 1) {
2510
+ return;
2511
+ }
2512
+ const oldRoot = this.root;
2513
+ // can remove one level of redundant branching if there is only a single child
2514
+ const childReference = oldRoot.children[0];
2515
+ if (childReference instanceof LeafNode) {
2516
+ return;
2517
+ }
2518
+ oldRoot.element.remove();
2519
+ const child = oldRoot.removeChild(0); // Remove child to prevent double disposal
2520
+ oldRoot.dispose(); // Dispose old root (won't dispose removed child)
2521
+ child.dispose(); // Dispose the removed child
2522
+ this._root = cloneNode(childReference, childReference.size, childReference.orthogonalSize);
2523
+ this.element.appendChild(this._root.element);
2524
+ this.disposable.value = this._root.onDidChange((e) => {
2525
+ this._onDidChange.fire(e);
2526
+ });
2527
+ }
2492
2528
  /**
2493
2529
  * If the root is orientated as a VERTICAL node then nest the existing root within a new HORIZIONTAL root node
2494
2530
  * If the root is orientated as a HORIZONTAL node then nest the existing root within a new VERITCAL root node
@@ -3880,6 +3916,48 @@
3880
3916
  }
3881
3917
  }
3882
3918
 
3919
+ function setGPUOptimizedBounds(element, bounds) {
3920
+ const { top, left, width, height } = bounds;
3921
+ const topPx = `${Math.round(top)}px`;
3922
+ const leftPx = `${Math.round(left)}px`;
3923
+ const widthPx = `${Math.round(width)}px`;
3924
+ const heightPx = `${Math.round(height)}px`;
3925
+ // Use traditional positioning but maintain GPU layer
3926
+ element.style.top = topPx;
3927
+ element.style.left = leftPx;
3928
+ element.style.width = widthPx;
3929
+ element.style.height = heightPx;
3930
+ element.style.visibility = 'visible';
3931
+ // Ensure GPU layer is maintained
3932
+ if (!element.style.transform || element.style.transform === '') {
3933
+ element.style.transform = 'translate3d(0, 0, 0)';
3934
+ }
3935
+ }
3936
+ function setGPUOptimizedBoundsFromStrings(element, bounds) {
3937
+ const { top, left, width, height } = bounds;
3938
+ // Use traditional positioning but maintain GPU layer
3939
+ element.style.top = top;
3940
+ element.style.left = left;
3941
+ element.style.width = width;
3942
+ element.style.height = height;
3943
+ element.style.visibility = 'visible';
3944
+ // Ensure GPU layer is maintained
3945
+ if (!element.style.transform || element.style.transform === '') {
3946
+ element.style.transform = 'translate3d(0, 0, 0)';
3947
+ }
3948
+ }
3949
+ function checkBoundsChanged(element, bounds) {
3950
+ const { top, left, width, height } = bounds;
3951
+ const topPx = `${Math.round(top)}px`;
3952
+ const leftPx = `${Math.round(left)}px`;
3953
+ const widthPx = `${Math.round(width)}px`;
3954
+ const heightPx = `${Math.round(height)}px`;
3955
+ // Check if position or size changed (back to traditional method)
3956
+ return element.style.top !== topPx ||
3957
+ element.style.left !== leftPx ||
3958
+ element.style.width !== widthPx ||
3959
+ element.style.height !== heightPx;
3960
+ }
3883
3961
  class WillShowOverlayEvent extends DockviewEvent {
3884
3962
  get nativeEvent() {
3885
3963
  return this.options.nativeEvent;
@@ -4161,21 +4239,11 @@
4161
4239
  box.left = rootLeft + width - 4;
4162
4240
  box.width = 4;
4163
4241
  }
4164
- const topPx = `${Math.round(box.top)}px`;
4165
- const leftPx = `${Math.round(box.left)}px`;
4166
- const widthPx = `${Math.round(box.width)}px`;
4167
- const heightPx = `${Math.round(box.height)}px`;
4168
- if (overlay.style.top === topPx &&
4169
- overlay.style.left === leftPx &&
4170
- overlay.style.width === widthPx &&
4171
- overlay.style.height === heightPx) {
4242
+ // Use GPU-optimized bounds checking and setting
4243
+ if (!checkBoundsChanged(overlay, box)) {
4172
4244
  return;
4173
4245
  }
4174
- overlay.style.top = topPx;
4175
- overlay.style.left = leftPx;
4176
- overlay.style.width = widthPx;
4177
- overlay.style.height = heightPx;
4178
- overlay.style.visibility = 'visible';
4246
+ setGPUOptimizedBounds(overlay, box);
4179
4247
  overlay.className = `dv-drop-target-anchor${this.options.className ? ` ${this.options.className}` : ''}`;
4180
4248
  toggleClass(overlay, 'dv-drop-target-left', isLeft);
4181
4249
  toggleClass(overlay, 'dv-drop-target-right', isRight);
@@ -4227,10 +4295,7 @@
4227
4295
  box.top = `${100 * (1 - size)}%`;
4228
4296
  box.height = `${100 * size}%`;
4229
4297
  }
4230
- this.overlayElement.style.top = box.top;
4231
- this.overlayElement.style.left = box.left;
4232
- this.overlayElement.style.width = box.width;
4233
- this.overlayElement.style.height = box.height;
4298
+ setGPUOptimizedBoundsFromStrings(this.overlayElement, box);
4234
4299
  toggleClass(this.overlayElement, 'dv-drop-target-small-vertical', isSmallY);
4235
4300
  toggleClass(this.overlayElement, 'dv-drop-target-small-horizontal', isSmallX);
4236
4301
  toggleClass(this.overlayElement, 'dv-drop-target-left', isLeft);
@@ -7673,7 +7738,36 @@
7673
7738
 
7674
7739
  const DEFAULT_FLOATING_GROUP_OVERFLOW_SIZE = 100;
7675
7740
  const DEFAULT_FLOATING_GROUP_POSITION = { left: 100, top: 100, width: 300, height: 300 };
7741
+ const DESERIALIZATION_POPOUT_DELAY_MS = 100;
7676
7742
 
7743
+ class PositionCache {
7744
+ constructor() {
7745
+ this.cache = new Map();
7746
+ this.currentFrameId = 0;
7747
+ this.rafId = null;
7748
+ }
7749
+ getPosition(element) {
7750
+ const cached = this.cache.get(element);
7751
+ if (cached && cached.frameId === this.currentFrameId) {
7752
+ return cached.rect;
7753
+ }
7754
+ this.scheduleFrameUpdate();
7755
+ const rect = getDomNodePagePosition(element);
7756
+ this.cache.set(element, { rect, frameId: this.currentFrameId });
7757
+ return rect;
7758
+ }
7759
+ invalidate() {
7760
+ this.currentFrameId++;
7761
+ }
7762
+ scheduleFrameUpdate() {
7763
+ if (this.rafId)
7764
+ return;
7765
+ this.rafId = requestAnimationFrame(() => {
7766
+ this.currentFrameId++;
7767
+ this.rafId = null;
7768
+ });
7769
+ }
7770
+ }
7677
7771
  function createFocusableElement() {
7678
7772
  const element = document.createElement('div');
7679
7773
  element.tabIndex = -1;
@@ -7686,6 +7780,8 @@
7686
7780
  this.accessor = accessor;
7687
7781
  this.map = {};
7688
7782
  this._disposed = false;
7783
+ this.positionCache = new PositionCache();
7784
+ this.pendingUpdates = new Set();
7689
7785
  this.addDisposables(exports.DockviewDisposable.from(() => {
7690
7786
  for (const value of Object.values(this.map)) {
7691
7787
  value.disposable.dispose();
@@ -7694,6 +7790,19 @@
7694
7790
  this._disposed = true;
7695
7791
  }));
7696
7792
  }
7793
+ updateAllPositions() {
7794
+ if (this._disposed) {
7795
+ return;
7796
+ }
7797
+ // Invalidate position cache to force recalculation
7798
+ this.positionCache.invalidate();
7799
+ // Call resize function directly for all visible panels
7800
+ for (const entry of Object.values(this.map)) {
7801
+ if (entry.panel.api.isVisible && entry.resize) {
7802
+ entry.resize();
7803
+ }
7804
+ }
7805
+ }
7697
7806
  detatch(panel) {
7698
7807
  if (this.map[panel.api.id]) {
7699
7808
  const { disposable, destroy } = this.map[panel.api.id];
@@ -7724,17 +7833,33 @@
7724
7833
  this.element.appendChild(focusContainer);
7725
7834
  }
7726
7835
  const resize = () => {
7727
- // TODO propagate position to avoid getDomNodePagePosition calls, possible performance bottleneck?
7728
- const box = getDomNodePagePosition(referenceContainer.element);
7729
- const box2 = getDomNodePagePosition(this.element);
7730
- focusContainer.style.left = `${box.left - box2.left}px`;
7731
- focusContainer.style.top = `${box.top - box2.top}px`;
7732
- focusContainer.style.width = `${box.width}px`;
7733
- focusContainer.style.height = `${box.height}px`;
7734
- toggleClass(focusContainer, 'dv-render-overlay-float', panel.group.api.location.type === 'floating');
7836
+ const panelId = panel.api.id;
7837
+ if (this.pendingUpdates.has(panelId)) {
7838
+ return; // Update already scheduled
7839
+ }
7840
+ this.pendingUpdates.add(panelId);
7841
+ requestAnimationFrame(() => {
7842
+ this.pendingUpdates.delete(panelId);
7843
+ if (this.isDisposed || !this.map[panelId]) {
7844
+ return;
7845
+ }
7846
+ const box = this.positionCache.getPosition(referenceContainer.element);
7847
+ const box2 = this.positionCache.getPosition(this.element);
7848
+ // Use traditional positioning for overlay containers
7849
+ const left = box.left - box2.left;
7850
+ const top = box.top - box2.top;
7851
+ const width = box.width;
7852
+ const height = box.height;
7853
+ focusContainer.style.left = `${left}px`;
7854
+ focusContainer.style.top = `${top}px`;
7855
+ focusContainer.style.width = `${width}px`;
7856
+ focusContainer.style.height = `${height}px`;
7857
+ toggleClass(focusContainer, 'dv-render-overlay-float', panel.group.api.location.type === 'floating');
7858
+ });
7735
7859
  };
7736
7860
  const visibilityChanged = () => {
7737
7861
  if (panel.api.isVisible) {
7862
+ this.positionCache.invalidate();
7738
7863
  resize();
7739
7864
  }
7740
7865
  focusContainer.style.display = panel.api.isVisible ? '' : 'none';
@@ -7829,6 +7954,8 @@
7829
7954
  this.map[panel.api.id].disposable.dispose();
7830
7955
  // and reset the disposable to the active reference-container
7831
7956
  this.map[panel.api.id].disposable = disposable;
7957
+ // store the resize function for direct access
7958
+ this.map[panel.api.id].resize = resize;
7832
7959
  return focusContainer;
7833
7960
  }
7834
7961
  }
@@ -8198,6 +8325,13 @@
8198
8325
  get floatingGroups() {
8199
8326
  return this._floatingGroups;
8200
8327
  }
8328
+ /**
8329
+ * Promise that resolves when all popout groups from the last fromJSON call are restored.
8330
+ * Useful for tests that need to wait for delayed popout creation.
8331
+ */
8332
+ get popoutRestorationPromise() {
8333
+ return this._popoutRestorationPromise;
8334
+ }
8201
8335
  constructor(container, options) {
8202
8336
  var _a, _b, _c;
8203
8337
  super(container, {
@@ -8246,6 +8380,7 @@
8246
8380
  this.onDidMaximizedGroupChange = this._onDidMaximizedGroupChange.event;
8247
8381
  this._floatingGroups = [];
8248
8382
  this._popoutGroups = [];
8383
+ this._popoutRestorationPromise = Promise.resolve();
8249
8384
  this._onDidRemoveGroup = new Emitter();
8250
8385
  this.onDidRemoveGroup = this._onDidRemoveGroup.event;
8251
8386
  this._onDidAddGroup = new Emitter();
@@ -8795,6 +8930,7 @@
8795
8930
  this.updateWatermark();
8796
8931
  }
8797
8932
  orthogonalize(position, options) {
8933
+ this.gridview.normalize();
8798
8934
  switch (position) {
8799
8935
  case 'top':
8800
8936
  case 'bottom':
@@ -9038,18 +9174,30 @@
9038
9174
  });
9039
9175
  }
9040
9176
  const serializedPopoutGroups = (_b = data.popoutGroups) !== null && _b !== void 0 ? _b : [];
9041
- for (const serializedPopoutGroup of serializedPopoutGroups) {
9177
+ // Create a promise that resolves when all popout groups are created
9178
+ const popoutPromises = [];
9179
+ // Queue popup group creation with delays to avoid browser blocking
9180
+ serializedPopoutGroups.forEach((serializedPopoutGroup, index) => {
9042
9181
  const { data, position, gridReferenceGroup, url } = serializedPopoutGroup;
9043
9182
  const group = createGroupFromSerializedState(data);
9044
- this.addPopoutGroup(group, {
9045
- position: position !== null && position !== void 0 ? position : undefined,
9046
- overridePopoutGroup: gridReferenceGroup ? group : undefined,
9047
- referenceGroup: gridReferenceGroup
9048
- ? this.getPanel(gridReferenceGroup)
9049
- : undefined,
9050
- popoutUrl: url,
9183
+ // Add a small delay for each popup after the first to avoid browser popup blocking
9184
+ const popoutPromise = new Promise((resolve) => {
9185
+ setTimeout(() => {
9186
+ this.addPopoutGroup(group, {
9187
+ position: position !== null && position !== void 0 ? position : undefined,
9188
+ overridePopoutGroup: gridReferenceGroup ? group : undefined,
9189
+ referenceGroup: gridReferenceGroup
9190
+ ? this.getPanel(gridReferenceGroup)
9191
+ : undefined,
9192
+ popoutUrl: url,
9193
+ });
9194
+ resolve();
9195
+ }, index * DESERIALIZATION_POPOUT_DELAY_MS); // 100ms delay between each popup
9051
9196
  });
9052
- }
9197
+ popoutPromises.push(popoutPromise);
9198
+ });
9199
+ // Store the promise for tests to wait on
9200
+ this._popoutRestorationPromise = Promise.all(popoutPromises).then(() => void 0);
9053
9201
  for (const floatingGroup of this._floatingGroups) {
9054
9202
  floatingGroup.overlay.setBounds();
9055
9203
  }
@@ -9096,6 +9244,10 @@
9096
9244
  throw err;
9097
9245
  }
9098
9246
  this.updateWatermark();
9247
+ // Force position updates for always visible panels after DOM layout is complete
9248
+ requestAnimationFrame(() => {
9249
+ this.overlayRenderContainer.updateAllPositions();
9250
+ });
9099
9251
  this._onDidLayoutFromJSON.fire();
9100
9252
  }
9101
9253
  clear() {
@@ -9596,7 +9748,6 @@
9596
9748
  const target = options.to.position;
9597
9749
  if (target === 'center') {
9598
9750
  const activePanel = from.activePanel;
9599
- const targetActivePanel = to.activePanel;
9600
9751
  const panels = this.movingLock(() => [...from.panels].map((p) => from.model.removePanel(p.id, {
9601
9752
  skipSetActive: true,
9602
9753
  })));
@@ -9606,22 +9757,21 @@
9606
9757
  this.movingLock(() => {
9607
9758
  for (const panel of panels) {
9608
9759
  to.model.openPanel(panel, {
9609
- skipSetActive: true, // Always skip setting panels active during move
9760
+ skipSetActive: panel !== activePanel,
9610
9761
  skipSetGroupActive: true,
9611
9762
  });
9612
9763
  }
9613
9764
  });
9614
- if (!options.skipSetActive) {
9615
- // Make the moved panel (from the source group) active
9616
- if (activePanel) {
9617
- this.doSetGroupAndPanelActive(to);
9618
- }
9765
+ // Ensure group becomes active after move
9766
+ if (options.skipSetActive !== true) {
9767
+ // For center moves (merges), we need to ensure the target group is active
9768
+ // unless explicitly told not to (skipSetActive: true)
9769
+ this.doSetGroupAndPanelActive(to);
9619
9770
  }
9620
- else if (targetActivePanel) {
9621
- // Ensure the target group's original active panel remains active
9622
- to.model.openPanel(targetActivePanel, {
9623
- skipSetGroupActive: true
9624
- });
9771
+ else if (!this.activePanel) {
9772
+ // Even with skipSetActive: true, ensure there's an active panel if none exists
9773
+ // This maintains basic functionality while respecting skipSetActive
9774
+ this.doSetGroupAndPanelActive(to);
9625
9775
  }
9626
9776
  }
9627
9777
  else {
@@ -9651,20 +9801,26 @@
9651
9801
  if (selectedPopoutGroup.referenceGroup) {
9652
9802
  const referenceGroup = this.getPanel(selectedPopoutGroup.referenceGroup);
9653
9803
  if (referenceGroup && !referenceGroup.api.isVisible) {
9654
- this.doRemoveGroup(referenceGroup, { skipActive: true });
9804
+ this.doRemoveGroup(referenceGroup, {
9805
+ skipActive: true,
9806
+ });
9655
9807
  }
9656
9808
  }
9657
9809
  // Manually dispose the window without triggering restoration
9658
9810
  selectedPopoutGroup.window.dispose();
9659
9811
  // Update group's location and containers for target
9660
9812
  if (to.api.location.type === 'grid') {
9661
- from.model.renderContainer = this.overlayRenderContainer;
9662
- from.model.dropTargetContainer = this.rootDropTargetContainer;
9813
+ from.model.renderContainer =
9814
+ this.overlayRenderContainer;
9815
+ from.model.dropTargetContainer =
9816
+ this.rootDropTargetContainer;
9663
9817
  from.model.location = { type: 'grid' };
9664
9818
  }
9665
9819
  else if (to.api.location.type === 'floating') {
9666
- from.model.renderContainer = this.overlayRenderContainer;
9667
- from.model.dropTargetContainer = this.rootDropTargetContainer;
9820
+ from.model.renderContainer =
9821
+ this.overlayRenderContainer;
9822
+ from.model.dropTargetContainer =
9823
+ this.rootDropTargetContainer;
9668
9824
  from.model.location = { type: 'floating' };
9669
9825
  }
9670
9826
  break;
@@ -9732,8 +9888,12 @@
9732
9888
  from.panels.forEach((panel) => {
9733
9889
  this._onDidMovePanel.fire({ panel, from });
9734
9890
  });
9735
- if (!options.skipSetActive) {
9736
- this.doSetGroupAndPanelActive(from);
9891
+ // Ensure group becomes active after move
9892
+ if (options.skipSetActive === false) {
9893
+ // Only activate when explicitly requested (skipSetActive: false)
9894
+ // Use 'to' group for non-center moves since 'from' may have been destroyed
9895
+ const targetGroup = to !== null && to !== void 0 ? to : from;
9896
+ this.doSetGroupAndPanelActive(targetGroup);
9737
9897
  }
9738
9898
  }
9739
9899
  doSetGroupActive(group) {