bitboss-ui 3.0.0-beta.12 → 3.0.0-beta.14
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.
- package/dist/ai/changelog.json +1 -1
- package/dist/ai/components.json +2 -2
- package/dist/ai/source/BbTable.md +224 -118
- package/dist/components/BbTable/BbTable.vue_vue_type_script_setup_true_lang.js +595 -613
- package/dist/components/BbTable/BbTableDataRow.d.ts +15 -4
- package/dist/components/BbTable/BbTableDataRow.js +2 -4
- package/dist/components/BbTable/BbTableExpandRow.d.ts +73 -0
- package/dist/components/BbTable/BbTableExpandRow.js +52 -0
- package/package.json +1 -1
package/dist/ai/changelog.json
CHANGED
package/dist/ai/components.json
CHANGED
|
@@ -471,42 +471,17 @@
|
|
|
471
471
|
:selected="isSelected(row.item)"
|
|
472
472
|
:virtual-index="row.virtualIndex"
|
|
473
473
|
/>
|
|
474
|
-
|
|
474
|
+
<!-- Behind a component boundary for the same reason the data row
|
|
475
|
+
is: the table re-renders its body on every window move, and this
|
|
476
|
+
row holds the CONSUMER's `#expand` slot — usually a whole nested
|
|
477
|
+
table. See the note at the top of `BbTableExpandRow`. -->
|
|
478
|
+
<BbTableExpandRow
|
|
475
479
|
v-else
|
|
476
|
-
:
|
|
477
|
-
:
|
|
478
|
-
:
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
: undefined
|
|
482
|
-
"
|
|
483
|
-
class="bb-table-expand__row"
|
|
484
|
-
:class="{
|
|
485
|
-
'bb-table-expand__row--highlighted': row.item.highlighted,
|
|
486
|
-
}"
|
|
487
|
-
:data-index="row.virtualIndex"
|
|
488
|
-
role="row"
|
|
489
|
-
>
|
|
490
|
-
<div
|
|
491
|
-
:aria-colspan="replacementContentSpan"
|
|
492
|
-
class="bb-table-expand__cell"
|
|
493
|
-
role="cell"
|
|
494
|
-
>
|
|
495
|
-
<!-- @vue-ignore -->
|
|
496
|
-
<slot
|
|
497
|
-
:expand-props="row.item.expandProps"
|
|
498
|
-
:expanded="row.item.expanded"
|
|
499
|
-
:highlighted="row.item.highlighted"
|
|
500
|
-
:item="row.item.item"
|
|
501
|
-
name="expand"
|
|
502
|
-
:selected="isSelected(row.item)"
|
|
503
|
-
:toggle-expanded="row.item.toggleExpanded"
|
|
504
|
-
:toggle-highlighted="row.item.toggleHighlighted"
|
|
505
|
-
:toggle-selected="row.item.toggleSelected"
|
|
506
|
-
:value="row.item.value"
|
|
507
|
-
></slot>
|
|
508
|
-
</div>
|
|
509
|
-
</div>
|
|
480
|
+
:context="dataRowContext"
|
|
481
|
+
:item="row.item"
|
|
482
|
+
:selected="isSelected(row.item)"
|
|
483
|
+
:virtual-index="row.virtualIndex"
|
|
484
|
+
/>
|
|
510
485
|
</template>
|
|
511
486
|
</slot>
|
|
512
487
|
|
|
@@ -605,6 +580,7 @@ import {
|
|
|
605
580
|
} from '@/utilities/functions/parsePaginationNumber';
|
|
606
581
|
import BaseCheckbox from '../BbBaseCheckbox/BbBaseCheckbox.vue';
|
|
607
582
|
import BbTableDataRow from './BbTableDataRow';
|
|
583
|
+
import BbTableExpandRow from './BbTableExpandRow';
|
|
608
584
|
import type { DataRowContext } from './BbTableDataRow';
|
|
609
585
|
import type { Option as BaseOption } from '@/types/Option';
|
|
610
586
|
import type { Classes } from '@/types/Classes';
|
|
@@ -881,6 +857,18 @@ const orderedColumns = computed(() => {
|
|
|
881
857
|
: ordered;
|
|
882
858
|
});
|
|
883
859
|
|
|
860
|
+
/**
|
|
861
|
+
* The RENDERED column keys, in order — as distinct from `declaredKeys`, which
|
|
862
|
+
* is the same list in the order the consumer wrote it. Six places derived this
|
|
863
|
+
* independently (the roving header index, the two header-focus helpers, the
|
|
864
|
+
* resize handle's neighbour lookup, the drag's placement math and
|
|
865
|
+
* `columnOrderKey`); they now share one cached derivation and, more to the
|
|
866
|
+
* point, one name for the thing.
|
|
867
|
+
*/
|
|
868
|
+
const orderedColumnKeys = computed(() =>
|
|
869
|
+
orderedColumns.value.map((column) => String(column.key))
|
|
870
|
+
);
|
|
871
|
+
|
|
884
872
|
/**
|
|
885
873
|
* The next model for "move `key` to render index `toIndex`", or `null` when
|
|
886
874
|
* that changes nothing (the boundary key, a no-op drop): callers then emit
|
|
@@ -928,7 +916,7 @@ const announceColumnMove = (key: string, from: number) => {
|
|
|
928
916
|
*/
|
|
929
917
|
const activeHeaderKey = ref<string | null>(null);
|
|
930
918
|
const rovingHeaderKey = computed(() => {
|
|
931
|
-
const keys =
|
|
919
|
+
const keys = orderedColumnKeys.value;
|
|
932
920
|
const active = activeHeaderKey.value;
|
|
933
921
|
return active !== null && keys.includes(active) ? active : (keys[0] ?? null);
|
|
934
922
|
});
|
|
@@ -964,7 +952,7 @@ const headerCellFor = (key: string) =>
|
|
|
964
952
|
) ?? null;
|
|
965
953
|
|
|
966
954
|
const focusHeaderAt = (index: number) => {
|
|
967
|
-
const keys =
|
|
955
|
+
const keys = orderedColumnKeys.value;
|
|
968
956
|
const key = keys[Math.max(0, Math.min(keys.length - 1, index))];
|
|
969
957
|
if (!key) return;
|
|
970
958
|
activeHeaderKey.value = key;
|
|
@@ -1012,7 +1000,7 @@ const onHeaderKeydown = async (event: KeyboardEvent, key: string) => {
|
|
|
1012
1000
|
return;
|
|
1013
1001
|
}
|
|
1014
1002
|
if (event.ctrlKey || event.metaKey) return;
|
|
1015
|
-
const rendered =
|
|
1003
|
+
const rendered = orderedColumnKeys.value;
|
|
1016
1004
|
const from = rendered.indexOf(key);
|
|
1017
1005
|
if (from < 0) return;
|
|
1018
1006
|
let to: number;
|
|
@@ -1162,39 +1150,53 @@ const resizedKeys = computed(
|
|
|
1162
1150
|
);
|
|
1163
1151
|
|
|
1164
1152
|
/**
|
|
1165
|
-
*
|
|
1166
|
-
*
|
|
1167
|
-
*
|
|
1168
|
-
*
|
|
1169
|
-
*
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
*
|
|
1184
|
-
*
|
|
1185
|
-
*
|
|
1186
|
-
*
|
|
1187
|
-
*
|
|
1188
|
-
*
|
|
1189
|
-
*
|
|
1153
|
+
* The width-driven state classes a column's DATA CELLS carry, as one ready
|
|
1154
|
+
* string per column key. Columns in their default state are absent from the
|
|
1155
|
+
* map rather than mapped to `''`.
|
|
1156
|
+
*
|
|
1157
|
+
* This replaces three Sets the row used to interrogate separately, once per
|
|
1158
|
+
* CELL: three membership tests and a three-key object literal on the order of
|
|
1159
|
+
* 250 times per body render, to answer a question that is per COLUMN and moves
|
|
1160
|
+
* only when a resize commits, the first-batch freeze lands, or the columns are
|
|
1161
|
+
* reordered.
|
|
1162
|
+
*
|
|
1163
|
+
* Holding the rules together is the other half of it, because they overlap by
|
|
1164
|
+
* construction and reading any one of them told you only a third of what a
|
|
1165
|
+
* cell would do:
|
|
1166
|
+
*
|
|
1167
|
+
* - `--resized` is a user override. A squeezed track cannot wrap a single
|
|
1168
|
+
* word, so such a cell clips with an ellipsis rather than spilling into its
|
|
1169
|
+
* neighbour. Only those: every other cell keeps wrapping and keeps flooring
|
|
1170
|
+
* its `auto` track exactly as before.
|
|
1171
|
+
* - `--nowrap` is every undeclared column of a `virtual` table, from its FIRST
|
|
1172
|
+
* render, frozen yet or not. The first batch is what the freeze measures,
|
|
1173
|
+
* and a cell that may still wrap ("INV-10213" breaks at the hyphen, a phone
|
|
1174
|
+
* at its spaces) floors its `auto` track at the WRAPPED width — the clip
|
|
1175
|
+
* that follows then cuts the same value by a character. `nowrap` alone keeps
|
|
1176
|
+
* the cell flooring the track; an `overflow: hidden` would zero its
|
|
1177
|
+
* automatic minimum.
|
|
1178
|
+
* - `--clip` is layered onto such a column once it IS frozen: its floor came
|
|
1179
|
+
* from whichever rows happened to be there, so a later, longer value must
|
|
1180
|
+
* not overflow the cell. A declared `width` keeps wrapping — that is how a
|
|
1181
|
+
* virtual table gets prose.
|
|
1190
1182
|
*/
|
|
1191
|
-
const
|
|
1192
|
-
const
|
|
1193
|
-
|
|
1183
|
+
const cellStateClasses = computed(() => {
|
|
1184
|
+
const byKey = new Map<string, string>();
|
|
1185
|
+
const resized = resizedKeys.value;
|
|
1186
|
+
const virtual = virtualActive.value;
|
|
1187
|
+
const frozen = virtualWidths.value;
|
|
1194
1188
|
for (const column of orderedColumns.value) {
|
|
1195
|
-
|
|
1189
|
+
const key = String(column.key);
|
|
1190
|
+
const undeclared = virtual && column.width == null;
|
|
1191
|
+
const state: string[] = [];
|
|
1192
|
+
if (undeclared) state.push('bb-table-data__cell--nowrap');
|
|
1193
|
+
if (resized.has(key)) state.push('bb-table-data__cell--resized');
|
|
1194
|
+
if (resized.has(key) || (undeclared && key in frozen)) {
|
|
1195
|
+
state.push('bb-table-data__cell--clip');
|
|
1196
|
+
}
|
|
1197
|
+
if (state.length) byKey.set(key, state.join(' '));
|
|
1196
1198
|
}
|
|
1197
|
-
return
|
|
1199
|
+
return byKey;
|
|
1198
1200
|
});
|
|
1199
1201
|
|
|
1200
1202
|
/** True when any user override is live (feeds the `bb-table--fixed` hook). */
|
|
@@ -1372,7 +1374,7 @@ const onResizerPointerDown = (event: PointerEvent, key: string) => {
|
|
|
1372
1374
|
// see above
|
|
1373
1375
|
}
|
|
1374
1376
|
document.documentElement.classList.add(RESIZING_ROOT_CLASS);
|
|
1375
|
-
const rendered =
|
|
1377
|
+
const rendered = orderedColumnKeys.value;
|
|
1376
1378
|
const nextKey = rendered[rendered.indexOf(key) + 1];
|
|
1377
1379
|
const nextCell = nextKey ? headerCellFor(nextKey) : null;
|
|
1378
1380
|
resize = {
|
|
@@ -1559,7 +1561,7 @@ const placeDraggedColumn = (rawX: number) => {
|
|
|
1559
1561
|
...cells.filter((c) => !isStuck(c)),
|
|
1560
1562
|
];
|
|
1561
1563
|
const others = ordered.filter((cell) => cell.dataset.columnKey !== drag!.key);
|
|
1562
|
-
const rendered =
|
|
1564
|
+
const rendered = orderedColumnKeys.value;
|
|
1563
1565
|
const from = rendered.indexOf(drag.key);
|
|
1564
1566
|
if (from < 0) return;
|
|
1565
1567
|
|
|
@@ -2954,9 +2956,7 @@ const trackKeys = computed(() =>
|
|
|
2954
2956
|
* `columnTracks`' keys are positions, so they read `0 1 2 3` before and after a
|
|
2955
2957
|
* reorder; this changes whenever the columns move.
|
|
2956
2958
|
*/
|
|
2957
|
-
const columnOrderKey = computed(() =>
|
|
2958
|
-
orderedColumns.value.map((column) => String(column.key)).join(' ')
|
|
2959
|
-
);
|
|
2959
|
+
const columnOrderKey = computed(() => orderedColumnKeys.value.join(' '));
|
|
2960
2960
|
|
|
2961
2961
|
watch(
|
|
2962
2962
|
[trackKeys, columnOrderKey],
|
|
@@ -3685,25 +3685,39 @@ const flatRows = computed<FlatRow[]>(() => {
|
|
|
3685
3685
|
|
|
3686
3686
|
/**
|
|
3687
3687
|
* Wraps a flat row for rendering, with the projection behind a getter (see
|
|
3688
|
-
* `BodyRow.item`).
|
|
3689
|
-
*
|
|
3690
|
-
*
|
|
3688
|
+
* `BodyRow.item`).
|
|
3689
|
+
*
|
|
3690
|
+
* The getter resolves ONCE per entry. `mappedItemAt` still owns the memo — the
|
|
3691
|
+
* identity a row's slots depend on comes from there, not from here, and the
|
|
3692
|
+
* entries are rebuilt on every window move — but the body reads this getter
|
|
3693
|
+
* three times for every rendered row (`:item`, `isSelected(row.item)` and
|
|
3694
|
+
* `isSelectToggleBlocked(row.item)`), so without the local hold each of those
|
|
3695
|
+
* paid a computed read and a `Map` lookup for a value it had already resolved
|
|
3696
|
+
* a line earlier. Counted on the playground during a drag: 107 resolutions per
|
|
3697
|
+
* scroll step for ~33 rendered rows.
|
|
3698
|
+
*
|
|
3699
|
+
* It cannot go stale. Everything that invalidates the memo also invalidates
|
|
3700
|
+
* `bodyRows` — which reads `mappedMemo` for exactly that reason — and a new
|
|
3701
|
+
* `bodyRows` is a new set of these wrappers.
|
|
3691
3702
|
*/
|
|
3692
3703
|
const bodyEntry = (
|
|
3693
3704
|
row: FlatRow,
|
|
3694
3705
|
kind: BodyRow['kind'],
|
|
3695
3706
|
key: string,
|
|
3696
3707
|
height: number
|
|
3697
|
-
): BodyRow =>
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
|
|
3708
|
+
): BodyRow => {
|
|
3709
|
+
let item: MappedItem | undefined;
|
|
3710
|
+
return {
|
|
3711
|
+
kind,
|
|
3712
|
+
index: row.index,
|
|
3713
|
+
key,
|
|
3714
|
+
virtualIndex: row.virtualIndex,
|
|
3715
|
+
height,
|
|
3716
|
+
get item() {
|
|
3717
|
+
return (item ??= mappedItemAt(row));
|
|
3718
|
+
},
|
|
3719
|
+
};
|
|
3720
|
+
};
|
|
3707
3721
|
|
|
3708
3722
|
/**
|
|
3709
3723
|
* The vertical scroller: the container once its height is bounded (it is
|
|
@@ -3800,8 +3814,10 @@ const onBodyFocusOut = (event: FocusEvent) => {
|
|
|
3800
3814
|
};
|
|
3801
3815
|
|
|
3802
3816
|
/**
|
|
3803
|
-
*
|
|
3804
|
-
*
|
|
3817
|
+
* The list's total height, published by `flushVirtual` — NOT read off the
|
|
3818
|
+
* wrapper's reactive handle. (The window itself is `virtualSlots`, below; a
|
|
3819
|
+
* `virtualItems` ref used to be published alongside this one and nothing ever
|
|
3820
|
+
* read it.) A scroll-driven change
|
|
3805
3821
|
* (`sync`) is published at once; a measurement-driven one (a mounted row
|
|
3806
3822
|
* measured taller or shorter than its estimate, which is the browser's
|
|
3807
3823
|
* ResizeObserver callback) is published on the next frame. The offsets and
|
|
@@ -3811,7 +3827,6 @@ const onBodyFocusOut = (event: FocusEvent) => {
|
|
|
3811
3827
|
* same delivery loop, which the browser reports as "ResizeObserver loop
|
|
3812
3828
|
* completed with undelivered notifications".
|
|
3813
3829
|
*/
|
|
3814
|
-
const virtualItems = shallowRef<VirtualItem[]>([]);
|
|
3815
3830
|
const virtualTotal = ref(0);
|
|
3816
3831
|
let virtualFlushFrame = 0;
|
|
3817
3832
|
|
|
@@ -3925,6 +3940,11 @@ const noteVirtualScroll = (offset: number) => {
|
|
|
3925
3940
|
const virtualRowMean = ref(0);
|
|
3926
3941
|
/** Entry count of the last cache we summed — see `syncVirtualMean`. */
|
|
3927
3942
|
let virtualMeanCacheSize = -1;
|
|
3943
|
+
/**
|
|
3944
|
+
* Whether the estimate has settled. See the latch in `syncVirtualMean`: it is
|
|
3945
|
+
* released only by a new row list, where the reset below clears it.
|
|
3946
|
+
*/
|
|
3947
|
+
let virtualEstimateLatched = false;
|
|
3928
3948
|
const syncVirtualMean = () => {
|
|
3929
3949
|
const cache = (
|
|
3930
3950
|
virtualizerHandle as { itemSizeCache?: Map<string, number> } | null
|
|
@@ -3959,7 +3979,29 @@ const syncVirtualMean = () => {
|
|
|
3959
3979
|
mode = size;
|
|
3960
3980
|
}
|
|
3961
3981
|
}
|
|
3962
|
-
|
|
3982
|
+
// LATCHED once enough rows have been measured to choose well, and then
|
|
3983
|
+
// left alone until the row list itself changes.
|
|
3984
|
+
//
|
|
3985
|
+
// The estimate stands in for every row NOT yet measured, so moving it
|
|
3986
|
+
// resizes the scrollport by (rows still unmeasured × the delta). Measuring
|
|
3987
|
+
// a row does not do that — the virtualizer swaps that one row's estimate
|
|
3988
|
+
// for its real height and adjusts the total by the difference, which is
|
|
3989
|
+
// smooth and invisible. Only re-estimating the whole tail at once can jump,
|
|
3990
|
+
// and on a list whose rows fall into two clusters (most short, some
|
|
3991
|
+
// wrapping) a freely-moving mode does exactly that: the two tallies trade
|
|
3992
|
+
// places as measurement proceeds, and each swap moved the scrollport
|
|
3993
|
+
// between 161,000px and 671,000px — the thumb skipping backwards while the
|
|
3994
|
+
// reader scrolled forwards. Hysteresis was not enough; a 50/50 split
|
|
3995
|
+
// crosses any margin repeatedly.
|
|
3996
|
+
//
|
|
3997
|
+
// Twenty rows is about one full window, so the sample is a real one rather
|
|
3998
|
+
// than the first five rows of a sorted list. Being wrong afterwards costs
|
|
3999
|
+
// only correction passes nobody sees; being unstable costs the reader
|
|
4000
|
+
// their place, which is the one thing a scrollbar exists to keep.
|
|
4001
|
+
if (mode > 0 && !virtualEstimateLatched) {
|
|
4002
|
+
virtualRowMean.value = mode;
|
|
4003
|
+
if (count >= 20) virtualEstimateLatched = true;
|
|
4004
|
+
}
|
|
3963
4005
|
}
|
|
3964
4006
|
// The spacers tile their skeleton on this cadence.
|
|
3965
4007
|
syncPlaceholderRowHeight();
|
|
@@ -4009,7 +4051,6 @@ const flushVirtual = (scrolled = false) => {
|
|
|
4009
4051
|
}
|
|
4010
4052
|
if (!virtualizerHandle) return;
|
|
4011
4053
|
const items = virtualActive.value ? virtualizerHandle.getVirtualItems() : [];
|
|
4012
|
-
virtualItems.value = items;
|
|
4013
4054
|
virtualTotal.value = virtualizerHandle.getTotalSize();
|
|
4014
4055
|
const slots: VirtualSlot[] = [];
|
|
4015
4056
|
let span: { start: number; end: number } | null = null;
|
|
@@ -4027,14 +4068,19 @@ const flushVirtual = (scrolled = false) => {
|
|
|
4027
4068
|
).range;
|
|
4028
4069
|
const first = range ? range.startIndex : items[0].index;
|
|
4029
4070
|
const last = range ? range.endIndex : items[items.length - 1].index;
|
|
4071
|
+
// The item IS its measurement — `getVirtualItems` pushes the entries of
|
|
4072
|
+
// `measurementsCache` themselves, it does not copy them — so looking the
|
|
4073
|
+
// same index up again bought a second read and a `continue` branch that
|
|
4074
|
+
// could not be taken. Still copied into our own shape rather than
|
|
4075
|
+
// forwarded: a slot is what the body renders against and what the next
|
|
4076
|
+
// flush compares itself to, and it should not be an object the
|
|
4077
|
+
// virtualizer owns.
|
|
4030
4078
|
for (const item of items) {
|
|
4031
|
-
const measurement = measurements?.[item.index];
|
|
4032
|
-
if (!measurement) continue;
|
|
4033
4079
|
slots.push({
|
|
4034
4080
|
index: item.index,
|
|
4035
|
-
start:
|
|
4036
|
-
end:
|
|
4037
|
-
size:
|
|
4081
|
+
start: item.start,
|
|
4082
|
+
end: item.end,
|
|
4083
|
+
size: item.size,
|
|
4038
4084
|
});
|
|
4039
4085
|
}
|
|
4040
4086
|
// The scroll range the WINDOW covers — the visible range plus its
|
|
@@ -4083,30 +4129,61 @@ const scheduleVirtualFlush = () => {
|
|
|
4083
4129
|
});
|
|
4084
4130
|
};
|
|
4085
4131
|
|
|
4132
|
+
/**
|
|
4133
|
+
* The three virtualizer callbacks that must keep ONE IDENTITY for the table's
|
|
4134
|
+
* life. They read their inputs when they are called, so a closure per options
|
|
4135
|
+
* evaluation bought nothing — and cost a great deal.
|
|
4136
|
+
*
|
|
4137
|
+
* `getItemKey` is the memo key of tanstack's `getMeasurementOptions`
|
|
4138
|
+
* (`virtual-core`: the deps are `[count, paddingStart, scrollMargin,
|
|
4139
|
+
* getItemKey, enabled, …]`, compared by IDENTITY). A new function there clears
|
|
4140
|
+
* `pendingMeasuredCacheIndexes` and invalidates `getMeasurements`, which then
|
|
4141
|
+
* rebuilds EVERY measurement from index 0 — one object, one `getItemKey` and
|
|
4142
|
+
* one `estimateSize` call per row in the whole list, not per row on screen.
|
|
4143
|
+
*
|
|
4144
|
+
* Built inside the options computed, they were re-created whenever anything
|
|
4145
|
+
* that computed reads changed — and one of those is `pinnedVirtualIndex`,
|
|
4146
|
+
* which is written on every `focusin`/`focusout` in the body. So tabbing
|
|
4147
|
+
* between two buttons in two rows, or arrowing down the keyboard grammar, paid
|
|
4148
|
+
* a full-list measurement rebuild: measured on the playground at 50,000 rows,
|
|
4149
|
+
* 94,076 `getItemKey` calls across twenty focus moves. Hoisted, focus moves
|
|
4150
|
+
* the range and nothing else.
|
|
4151
|
+
*
|
|
4152
|
+
* `rangeExtractor` deliberately STAYS in the computed: it is the memo key of
|
|
4153
|
+
* `getVirtualIndexes`, so the pinned row would never be added to the range if
|
|
4154
|
+
* its identity did not move with `pinnedVirtualIndex`. That memo is over one
|
|
4155
|
+
* window, not over the list.
|
|
4156
|
+
*/
|
|
4157
|
+
const virtualOnChange = (
|
|
4158
|
+
instance: { getScrollOffset: () => number },
|
|
4159
|
+
sync: boolean
|
|
4160
|
+
) => {
|
|
4161
|
+
if (sync) {
|
|
4162
|
+
noteVirtualScroll(instance.getScrollOffset());
|
|
4163
|
+
flushVirtual(true);
|
|
4164
|
+
} else {
|
|
4165
|
+
scheduleVirtualFlush();
|
|
4166
|
+
}
|
|
4167
|
+
};
|
|
4168
|
+
|
|
4169
|
+
const virtualItemKey = (index: number) => flatRows.value[index]?.key ?? index;
|
|
4170
|
+
|
|
4171
|
+
const virtualEstimateSize = (index: number) =>
|
|
4172
|
+
(flatRows.value[index]?.kind === 'expand' ? 3 : 1) *
|
|
4173
|
+
(virtualRowMean.value || virtualRowEstimate.value);
|
|
4174
|
+
|
|
4086
4175
|
const rowVirtualizer = useVirtualizer(
|
|
4087
4176
|
computed(() => {
|
|
4088
4177
|
const scroller = virtualScroller.value;
|
|
4089
4178
|
const isWindow = scroller === window;
|
|
4090
4179
|
const pinned = pinnedVirtualIndex.value;
|
|
4091
4180
|
return {
|
|
4092
|
-
onChange:
|
|
4093
|
-
instance: { getScrollOffset: () => number },
|
|
4094
|
-
sync: boolean
|
|
4095
|
-
) => {
|
|
4096
|
-
if (sync) {
|
|
4097
|
-
noteVirtualScroll(instance.getScrollOffset());
|
|
4098
|
-
flushVirtual(true);
|
|
4099
|
-
} else {
|
|
4100
|
-
scheduleVirtualFlush();
|
|
4101
|
-
}
|
|
4102
|
-
},
|
|
4181
|
+
onChange: virtualOnChange,
|
|
4103
4182
|
count: virtualActive.value ? flatRows.value.length : 0,
|
|
4104
4183
|
enabled: virtualActive.value,
|
|
4105
4184
|
getScrollElement: () => scroller as any,
|
|
4106
|
-
estimateSize:
|
|
4107
|
-
|
|
4108
|
-
(virtualRowMean.value || virtualRowEstimate.value),
|
|
4109
|
-
getItemKey: (index: number) => flatRows.value[index]?.key ?? index,
|
|
4185
|
+
estimateSize: virtualEstimateSize,
|
|
4186
|
+
getItemKey: virtualItemKey,
|
|
4110
4187
|
overscan: VIRTUAL_OVERSCAN,
|
|
4111
4188
|
scrollMargin: virtualScrollMargin.value,
|
|
4112
4189
|
// Before the scroller is known (first render, SSR): a window's worth
|
|
@@ -4185,7 +4262,6 @@ const bodyRows = computed<BodyRow[]>(() => {
|
|
|
4185
4262
|
const out: BodyRow[] = [];
|
|
4186
4263
|
const margin = virtualScrollMargin.value;
|
|
4187
4264
|
const fast = virtualFast.value;
|
|
4188
|
-
const real = new Set<string>();
|
|
4189
4265
|
let cursor = margin;
|
|
4190
4266
|
let last: FlatRow | undefined;
|
|
4191
4267
|
for (const slot of slots) {
|
|
@@ -4201,13 +4277,10 @@ const bodyRows = computed<BodyRow[]>(() => {
|
|
|
4201
4277
|
out.push(bodyEntry(row, 'placeholder', `p:${row.key}`, slot.size));
|
|
4202
4278
|
} else {
|
|
4203
4279
|
out.push(bodyEntry(row, row.kind, row.key, 0));
|
|
4204
|
-
real.add(row.key);
|
|
4205
4280
|
}
|
|
4206
4281
|
cursor = slot.end;
|
|
4207
4282
|
last = row;
|
|
4208
4283
|
}
|
|
4209
|
-
// Bookkeeping, not state: remember what has been rendered real.
|
|
4210
|
-
for (const key of real) virtualRealKeys.add(key);
|
|
4211
4284
|
const tail = virtualTotal.value - (cursor - margin);
|
|
4212
4285
|
if (tail > 0.5 && last) {
|
|
4213
4286
|
out.push(bodyEntry(last, 'spacer', 's:end', tail));
|
|
@@ -4215,6 +4288,39 @@ const bodyRows = computed<BodyRow[]>(() => {
|
|
|
4215
4288
|
return out;
|
|
4216
4289
|
});
|
|
4217
4290
|
|
|
4291
|
+
/**
|
|
4292
|
+
* "Rows you have already seen stay real" — recorded AFTER the patch, not
|
|
4293
|
+
* inside `bodyRows`.
|
|
4294
|
+
*
|
|
4295
|
+
* It used to be a `virtualRealKeys.add` at the end of that computed, and a
|
|
4296
|
+
* computed that writes to the world outside it is one whose result depends on
|
|
4297
|
+
* how many times it has run. Vue re-evaluates a computed whenever a dependency
|
|
4298
|
+
* moves and somebody asks for the value, which is NOT the same event as "the
|
|
4299
|
+
* body rendered this": an evaluation whose output never reached the DOM still
|
|
4300
|
+
* promoted its rows out of the skeleton, and the next fast scroll then mounted
|
|
4301
|
+
* a full row — sixteen cells and the consumer's slots — for a reader who is
|
|
4302
|
+
* nowhere near it. Post-flush the set says exactly what it claims: the keys of
|
|
4303
|
+
* rows that are, or have been, on screen.
|
|
4304
|
+
*
|
|
4305
|
+
* Both real kinds are recorded, as they were before. Only `row` is ever
|
|
4306
|
+
* consulted (an expand row is never drawn as a placeholder), but keeping
|
|
4307
|
+
* `expand` in makes the set answer the question its name asks.
|
|
4308
|
+
*
|
|
4309
|
+
* This watcher forces no extra work: the pin pass below already subscribes to
|
|
4310
|
+
* `bodyRows` post-flush, so the value is computed for that tick regardless.
|
|
4311
|
+
*/
|
|
4312
|
+
watch(
|
|
4313
|
+
bodyRows,
|
|
4314
|
+
(rows) => {
|
|
4315
|
+
if (!virtualActive.value) return;
|
|
4316
|
+
for (const row of rows) {
|
|
4317
|
+
if (row.kind === 'row' || row.kind === 'expand')
|
|
4318
|
+
virtualRealKeys.add(row.key);
|
|
4319
|
+
}
|
|
4320
|
+
},
|
|
4321
|
+
{ flush: 'post' }
|
|
4322
|
+
);
|
|
4323
|
+
|
|
4218
4324
|
/**
|
|
4219
4325
|
* Column widths of a `virtual` table, measured once from the first rendered
|
|
4220
4326
|
* batch (key → px). A windowed body sizes its `auto` tracks from the rows it
|
|
@@ -4347,6 +4453,7 @@ watch(
|
|
|
4347
4453
|
// re-summed even if the size cache happens to hold the same count.
|
|
4348
4454
|
virtualRealKeys = new Set();
|
|
4349
4455
|
virtualMeanCacheSize = -1;
|
|
4456
|
+
virtualEstimateLatched = false;
|
|
4350
4457
|
// New rows (a refetch, a filter) render on this tick, not the next frame.
|
|
4351
4458
|
flushVirtual();
|
|
4352
4459
|
});
|
|
@@ -4645,11 +4752,10 @@ const dataRowContext = computed<DataRowContext>(() => ({
|
|
|
4645
4752
|
keyboardNavigation: !!props.keyboardNavigation,
|
|
4646
4753
|
contentInert: contentInert.value,
|
|
4647
4754
|
hasRowIndex: hasProvidedAccessibilityData.value,
|
|
4755
|
+
columnSpan: replacementContentSpan.value,
|
|
4648
4756
|
inputName: randomName,
|
|
4649
4757
|
tdClass: props.tdClass,
|
|
4650
|
-
|
|
4651
|
-
clippedKeys: clippedCellKeys.value,
|
|
4652
|
-
nowrapKeys: nowrapCellKeys.value,
|
|
4758
|
+
cellClasses: cellStateClasses.value,
|
|
4653
4759
|
sortOrders: sortOrders.value,
|
|
4654
4760
|
// A plain table has no virtualizer: the ref is still passed, and is a no-op.
|
|
4655
4761
|
measureRow: measureVirtualRow,
|