@shival99/z-ui 2.0.78 → 2.0.80

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.
@@ -6701,6 +6701,26 @@ class ZTableComponent {
6701
6701
  },
6702
6702
  ];
6703
6703
  _bulkBarExitDuration = 140;
6704
+ /** Stable identity key per virtual index (row/group id, not array index) so cached measured
6705
+ * heights follow row identity — prevents height jitter when scrolling fast. */
6706
+ _virtualItemKeys = computed(() => {
6707
+ if (!this.isVirtual()) {
6708
+ return [];
6709
+ }
6710
+ const flatItems = this.virtualGroupedFlatItems();
6711
+ if (flatItems.length > 0) {
6712
+ return flatItems.map((item, index) => item.type === 'group-header' ? `g:${item.groupItem.id}` : `r:${item.rows[0]?.id ?? index}`);
6713
+ }
6714
+ const groupRows = this.dynamicGroupRows();
6715
+ if (groupRows.length > 0) {
6716
+ return groupRows.map((rows, index) => `r:${rows[0]?.id ?? index}`);
6717
+ }
6718
+ return Array.from({ length: this._virtualGroupCount() }, (_, index) => index);
6719
+ }, ...(ngDevMode ? [{ debugName: "_virtualItemKeys" }] : []));
6720
+ /** Estimate height learned from real rendered rows (dynamicSize); `virtual.size` is the fallback
6721
+ * until a row is measured. Lets not-yet-measured rows estimate close to reality, reducing layout
6722
+ * shift. Plain field (non-reactive) so updating it won't rebuild the virtualizer. */
6723
+ _dynamicEstimateHeight = null;
6704
6724
  /** Virtualizer instance — operates on groups rather than individual rows */
6705
6725
  virtualizer = injectVirtualizer(() => {
6706
6726
  const groups = this.dynamicGroups();
@@ -6710,23 +6730,30 @@ class ZTableComponent {
6710
6730
  const virtualConfig = this._virtualConfig();
6711
6731
  const flatItems = this.virtualGroupedFlatItems();
6712
6732
  const isGrouping = flatItems.length > 0;
6733
+ const itemKeys = this._virtualItemKeys();
6734
+ const isDynamic = this.dynamicSize();
6735
+ // Estimate for not-yet-measured rows: learned height if available, else virtual.size.
6736
+ const dynamicRowEstimate = isDynamic && this._dynamicEstimateHeight !== null ? this._dynamicEstimateHeight : rowHeight;
6713
6737
  return {
6714
6738
  scrollElement,
6715
6739
  count: isGrouping ? flatItems.length : groups.length > 0 ? groups.length : this._virtualGroupCount(),
6740
+ // Stable per-row key so cached measured heights follow row identity, not the recycled
6741
+ // index slot — prevents the height jitter when scrolling fast (see _virtualItemKeys).
6742
+ getItemKey: (index) => itemKeys[index] ?? index,
6716
6743
  estimateSize: (index) => {
6717
6744
  if (isGrouping) {
6718
6745
  return flatItems[index]?.type === 'group-header'
6719
6746
  ? Math.max(rowHeight, Z_VIRTUAL_GROUP_HEADER_HEIGHT)
6720
- : rowHeight;
6747
+ : dynamicRowEstimate;
6721
6748
  }
6722
6749
  if (groups.length > 0 && groups[index]) {
6723
6750
  const groupHeight = groups[index].height;
6724
6751
  if (groupHeight !== null) {
6725
6752
  return groupHeight;
6726
6753
  }
6727
- return groups[index].rowCount * rowHeight;
6754
+ return groups[index].rowCount * dynamicRowEstimate;
6728
6755
  }
6729
- return groupSize * rowHeight;
6756
+ return groupSize * dynamicRowEstimate;
6730
6757
  },
6731
6758
  scrollToFn: (offset, options, instance) => {
6732
6759
  instance.scrollOffset = offset;
@@ -6755,14 +6782,45 @@ class ZTableComponent {
6755
6782
  if (!view) {
6756
6783
  return;
6757
6784
  }
6758
- // Gom phép đo vào frame kế tiếp để tránh cập nhật vị trí nhiều lần trong cùng một frame scroll.
6785
+ // Only sample heights until the estimate is locked; afterwards just measure (no extra reads).
6786
+ const needsEstimate = this._dynamicEstimateHeight === null;
6787
+ // Batch measurements into the next frame to avoid repositioning multiple times per scroll frame.
6759
6788
  const frameId = view.requestAnimationFrame(() => {
6789
+ let measuredSum = 0;
6790
+ let measuredCount = 0;
6760
6791
  for (const el of elements) {
6761
- this.virtualizer.measureElement(el.nativeElement);
6792
+ const node = el.nativeElement;
6793
+ this.virtualizer.measureElement(node);
6794
+ if (needsEstimate) {
6795
+ // Layout already flushed by measureElement above, so this read hits the cache (no reflow).
6796
+ const { height } = node.getBoundingClientRect();
6797
+ if (height > 0) {
6798
+ measuredSum += height;
6799
+ measuredCount++;
6800
+ }
6801
+ }
6802
+ }
6803
+ if (needsEstimate) {
6804
+ this._learnDynamicEstimate(measuredSum, measuredCount);
6762
6805
  }
6763
6806
  });
6764
6807
  onCleanup(() => view.cancelAnimationFrame(frameId));
6765
6808
  }, ...(ngDevMode ? [{ debugName: "_measureVirtualItems" }] : []));
6809
+ /** Learn estimate height from rendered rows. Locks ONCE so the estimate doesn't jump every scroll
6810
+ * frame (each row is still measured exactly on render). Re-flows via virtualizer.measure(). */
6811
+ _learnDynamicEstimate(measuredSum, measuredCount) {
6812
+ if (this._dynamicEstimateHeight !== null || measuredCount === 0) {
6813
+ return;
6814
+ }
6815
+ const average = Math.round(measuredSum / measuredCount);
6816
+ // Skip re-flow when already equal to the default size.
6817
+ if (average <= 0 || average === this.virtualRowHeight()) {
6818
+ this._dynamicEstimateHeight = average > 0 ? average : null;
6819
+ return;
6820
+ }
6821
+ this._dynamicEstimateHeight = average;
6822
+ this.virtualizer.measure();
6823
+ }
6766
6824
  _rowDragAutoScroll = effect(onCleanup => {
6767
6825
  const scrollbar = this.tbodyScrollbar();
6768
6826
  if (!scrollbar?.adapter.initialized()) {
@@ -7018,6 +7076,8 @@ class ZTableComponent {
7018
7076
  }
7019
7077
  });
7020
7078
  explicitEffect([this.columnFilters, this.globalFilter, this.pagination, this.sorting, this._data], () => {
7079
+ // New dataset may have different row heights -> relearn the estimate.
7080
+ this._dynamicEstimateHeight = null;
7021
7081
  queueMicrotask(() => {
7022
7082
  this._checkVerticalScroll();
7023
7083
  this._checkHorizontalScroll();