bitboss-ui 3.0.0-beta.20 → 3.0.0-beta.21
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/BbTable.md +206 -98
- package/dist/ai/changelog.json +21 -3
- package/dist/ai/components.json +40 -15
- package/dist/ai/guides/component-picker.md +18 -18
- package/dist/ai/guides/icons-policy.md +7 -3
- package/dist/ai/guides/installation-and-plugin-setup.md +11 -14
- package/dist/ai/guides/migration/components/bb-table.md +131 -17
- package/dist/ai/recipes/inertia/approvals-inbox.md +0 -1
- package/dist/ai/recipes/inertia/inline-edit-workspace.md +0 -1
- package/dist/ai/recipes/inertia/ownership-atlas.md +8 -9
- package/dist/ai/recipes/inertia/record-form.md +0 -1
- package/dist/ai/recipes/inertia/records-workspace.md +0 -1
- package/dist/ai/recipes/inertia/upload-center.md +0 -1
- package/dist/ai/recipes/nuxt/approvals-inbox.md +0 -1
- package/dist/ai/recipes/nuxt/inline-edit-workspace.md +0 -1
- package/dist/ai/recipes/nuxt/record-form.md +0 -1
- package/dist/ai/recipes/nuxt/records-workspace.md +0 -1
- package/dist/ai/recipes/nuxt/upload-center.md +0 -1
- package/dist/ai/recipes/vue/approvals-inbox.md +0 -1
- package/dist/ai/recipes/vue/inline-edit-workspace.md +0 -1
- package/dist/ai/recipes/vue/records-workspace.md +0 -1
- package/dist/ai/recipes/vue/upload-center.md +0 -1
- package/dist/ai/source/BbTable.md +564 -173
- package/dist/components/BbTable/BbTable.vue.d.ts +2 -0
- package/dist/components/BbTable/BbTable.vue_vue_type_script_setup_true_lang.js +865 -735
- package/dist/components/BbTable/types.d.ts +98 -46
- package/dist/components/BbTable/utils.d.ts +21 -16
- package/dist/components/BbTable/utils.js +8 -8
- package/dist/composables/useTableWidthContext.d.ts +24 -4
- package/dist/composables/useTableWidthContext.js +31 -28
- package/dist/deprecation/ai-deprecations.json.d.ts +22 -0
- package/dist/deprecation/ai-deprecations.json.js +1 -1
- package/dist/llms-full.txt +381 -173
- package/dist/llms-medium.txt +29 -32
- package/dist/vite.js +1 -1
- package/package.json +1 -1
- package/scripts/lib/eslint-plugin.mjs +9 -14
- package/scripts/lib/validate-bb-markup.mjs +0 -7
|
@@ -233,6 +233,7 @@
|
|
|
233
233
|
<!-- @vue-ignore -->
|
|
234
234
|
<slot
|
|
235
235
|
:classes="header.classes"
|
|
236
|
+
:hide-column="() => hideColumn(header.key)"
|
|
236
237
|
:items="internalItems"
|
|
237
238
|
:label="header.label"
|
|
238
239
|
:name="header.slotName"
|
|
@@ -295,7 +296,7 @@
|
|
|
295
296
|
/>
|
|
296
297
|
</div>
|
|
297
298
|
<div
|
|
298
|
-
v-if="
|
|
299
|
+
v-if="hasActions"
|
|
299
300
|
class="bb-table-header bb-table-header--actions"
|
|
300
301
|
:class="thClass"
|
|
301
302
|
role="columnheader"
|
|
@@ -405,7 +406,7 @@
|
|
|
405
406
|
/>
|
|
406
407
|
</div>
|
|
407
408
|
<div
|
|
408
|
-
v-if="
|
|
409
|
+
v-if="hasActions"
|
|
409
410
|
class="bb-table-skeleton__cell bb-table-skeleton__cell--actions"
|
|
410
411
|
role="cell"
|
|
411
412
|
>
|
|
@@ -525,12 +526,14 @@ import {
|
|
|
525
526
|
getCurrentInstance,
|
|
526
527
|
nextTick,
|
|
527
528
|
onBeforeUnmount,
|
|
529
|
+
onBeforeUpdate,
|
|
528
530
|
onMounted,
|
|
529
531
|
reactive,
|
|
530
532
|
ref,
|
|
531
533
|
shallowRef,
|
|
532
534
|
toRef,
|
|
533
535
|
watch,
|
|
536
|
+
watchEffect,
|
|
534
537
|
} from 'vue';
|
|
535
538
|
import {
|
|
536
539
|
defaultRangeExtractor,
|
|
@@ -598,6 +601,7 @@ import {
|
|
|
598
601
|
readResolvedTracks,
|
|
599
602
|
sameColumnOrder,
|
|
600
603
|
} from './utils';
|
|
604
|
+
import type { FixedColumnConfig } from './utils';
|
|
601
605
|
import { useBbTableContextHost } from '../../composables/useBbTableContext';
|
|
602
606
|
import { useLogger } from '@/composables/useLogger';
|
|
603
607
|
import {
|
|
@@ -643,7 +647,6 @@ const props = withDefaults(defineProps<InternalProps<T>>(), {
|
|
|
643
647
|
columns: () => [],
|
|
644
648
|
dependencies: () => [],
|
|
645
649
|
depsDebounceTime: 0,
|
|
646
|
-
fixedColumns: () => [],
|
|
647
650
|
items: () => [],
|
|
648
651
|
max: Infinity,
|
|
649
652
|
multiple: true,
|
|
@@ -685,6 +688,7 @@ const slots = defineSlots<
|
|
|
685
688
|
sortable?: boolean;
|
|
686
689
|
sortOrder?: 'asc' | 'desc' | null;
|
|
687
690
|
toggleSort?: () => void;
|
|
691
|
+
hideColumn?: () => void;
|
|
688
692
|
}) => any
|
|
689
693
|
>
|
|
690
694
|
> &
|
|
@@ -699,6 +703,21 @@ const slots = defineSlots<
|
|
|
699
703
|
if (process.env.NODE_ENV !== 'production')
|
|
700
704
|
warnRemovedAttrs('BbTable', removedPropsFor('BbTable'));
|
|
701
705
|
|
|
706
|
+
/**
|
|
707
|
+
* The actions column exists exactly when the `#actions` slot is provided —
|
|
708
|
+
* there is no prop. Slot presence is not reactive state (a `computed` reading
|
|
709
|
+
* `slots.actions` never re-runs when the parent toggles the template with a
|
|
710
|
+
* `v-if`), so it is sampled into a ref before every update: the parent's
|
|
711
|
+
* re-render delivers the new slots to this instance before `beforeUpdate`
|
|
712
|
+
* fires, and a conditional slot makes the child's children dynamic, so the
|
|
713
|
+
* child does update. The track computeds and the row context read the ref.
|
|
714
|
+
*/
|
|
715
|
+
const hasActions = ref(!!slots.actions);
|
|
716
|
+
onBeforeUpdate(() => {
|
|
717
|
+
const present = !!slots.actions;
|
|
718
|
+
if (present !== hasActions.value) hasActions.value = present;
|
|
719
|
+
});
|
|
720
|
+
|
|
702
721
|
const { getItemValue } = useItemValue();
|
|
703
722
|
|
|
704
723
|
/** Temporary name for selection inputs; hidden inputs handle real submission. */
|
|
@@ -758,7 +777,7 @@ if (process.env.NODE_ENV !== 'production') {
|
|
|
758
777
|
useLogger().warn(
|
|
759
778
|
`BbTable: \`{ key: '${String(column.key)}' }\` is a reserved column and must not appear in \`columns\`. ` +
|
|
760
779
|
(column.key === 'actions'
|
|
761
|
-
? '
|
|
780
|
+
? 'Fill the `#actions` slot instead — the slot creates the column.'
|
|
762
781
|
: 'Selection is controlled by the `selectable` prop.')
|
|
763
782
|
);
|
|
764
783
|
}
|
|
@@ -810,6 +829,81 @@ const headerInstructionsId = computed(
|
|
|
810
829
|
/** Header cells are keyboard targets when either header interaction is on. */
|
|
811
830
|
const headerKeyboard = computed(() => !!(props.reorderable || props.resizable));
|
|
812
831
|
|
|
832
|
+
/* --- Column visibility ---------------------------------------------------------
|
|
833
|
+
* `column.hidden` is the single declared truth (the `width` shape, not the
|
|
834
|
+
* `sort` one): a hidden column renders nothing and owns no track but stays
|
|
835
|
+
* DECLARED, so the order write-back completes around it and it returns to its
|
|
836
|
+
* slot when the flag clears. A header's `hideColumn()` sets a per-mount OVERRIDE the
|
|
837
|
+
* table keeps until that column's flag changes to anything but `true` — the
|
|
838
|
+
* consumer persisting the reported key writes `hidden: true`, which matches
|
|
839
|
+
* the override and keeps it; a panel showing the column writes `false`, which
|
|
840
|
+
* drops it. There is no "show" path on the table: a hidden column has no
|
|
841
|
+
* header to call it from. */
|
|
842
|
+
|
|
843
|
+
/** Keys hidden from their header (`hideColumn()`), pending the definition's word. */
|
|
844
|
+
const hiddenOverrides = ref<ReadonlySet<string>>(new Set());
|
|
845
|
+
|
|
846
|
+
/** Every key that renders nothing right now: the flag or a live override. */
|
|
847
|
+
const hiddenColumnKeys = computed(() => {
|
|
848
|
+
const keys = new Set(hiddenOverrides.value);
|
|
849
|
+
for (const column of props.columns) {
|
|
850
|
+
if (column.hidden) keys.add(String(column.key));
|
|
851
|
+
}
|
|
852
|
+
return keys;
|
|
853
|
+
});
|
|
854
|
+
|
|
855
|
+
/**
|
|
856
|
+
* The declared columns that render, in declaration order. Returns
|
|
857
|
+
* `props.columns` BY IDENTITY when nothing is hidden, so the identity
|
|
858
|
+
* shortcut in `orderedColumns` (and every "feature off" byte-parity
|
|
859
|
+
* guarantee behind it) survives untouched.
|
|
860
|
+
*/
|
|
861
|
+
const visibleColumns = computed(() => {
|
|
862
|
+
const hidden = hiddenColumnKeys.value;
|
|
863
|
+
if (!hidden.size) return props.columns;
|
|
864
|
+
return props.columns.filter((column) => !hidden.has(String(column.key)));
|
|
865
|
+
});
|
|
866
|
+
|
|
867
|
+
/** `hideColumn()` from a header slot: an override, reported once. */
|
|
868
|
+
const hideColumn = (key: string) => {
|
|
869
|
+
if (hiddenColumnKeys.value.has(key)) return;
|
|
870
|
+
// A hidden header unmounts under the keyboard user: keep focus on the
|
|
871
|
+
// header row (the same rendered index, i.e. the column that slides in).
|
|
872
|
+
const cell = headerCellFor(key);
|
|
873
|
+
const index = orderedColumnKeys.value.indexOf(key);
|
|
874
|
+
const refocus =
|
|
875
|
+
!!cell && cell.contains(document.activeElement) && headerKeyboard.value;
|
|
876
|
+
hiddenOverrides.value = new Set([...hiddenOverrides.value, key]);
|
|
877
|
+
emit('hide:column', key);
|
|
878
|
+
if (refocus) nextTick(() => focusHeaderAt(index));
|
|
879
|
+
};
|
|
880
|
+
|
|
881
|
+
// The definition wins whenever it changes to something ELSE than the override
|
|
882
|
+
// (the `resizedWidths` rule): `hidden: true` written back is the consumer
|
|
883
|
+
// persisting the hide and keeps the override; any other change drops it.
|
|
884
|
+
watch(
|
|
885
|
+
() =>
|
|
886
|
+
props.columns.map(
|
|
887
|
+
(column) => [String(column.key), !!column.hidden] as const
|
|
888
|
+
),
|
|
889
|
+
(now, before) => {
|
|
890
|
+
if (!before || !hiddenOverrides.value.size) return;
|
|
891
|
+
const previous = new Map(before);
|
|
892
|
+
const overridden = hiddenOverrides.value;
|
|
893
|
+
const cleared = now.filter(
|
|
894
|
+
([key, hidden]) =>
|
|
895
|
+
previous.has(key) &&
|
|
896
|
+
previous.get(key) !== hidden &&
|
|
897
|
+
overridden.has(key) &&
|
|
898
|
+
!hidden
|
|
899
|
+
);
|
|
900
|
+
if (!cleared.length) return;
|
|
901
|
+
const rest = new Set(overridden);
|
|
902
|
+
for (const [key] of cleared) rest.delete(key);
|
|
903
|
+
hiddenOverrides.value = rest;
|
|
904
|
+
}
|
|
905
|
+
);
|
|
906
|
+
|
|
813
907
|
/* --- Column order -----------------------------------------------------------
|
|
814
908
|
* `columns` is the declaration, `order` is the state (the sort split).
|
|
815
909
|
* ONE ordered list feeds every positional computation below; everything keyed
|
|
@@ -817,6 +911,12 @@ const headerKeyboard = computed(() => !!(props.reorderable || props.resizable));
|
|
|
817
911
|
* (`projectedItemAt`) maps `orderedColumns` directly — a reorder drops its memo
|
|
818
912
|
* and re-runs the formatters, but only for the rows on screen. */
|
|
819
913
|
|
|
914
|
+
/**
|
|
915
|
+
* EVERY declared key, hidden ones included — the set the order model is
|
|
916
|
+
* completed against. Built from the declaration and never from the rendered
|
|
917
|
+
* columns: that is what keeps a hidden key in the write-back even when the
|
|
918
|
+
* consumer's persisted model never listed it.
|
|
919
|
+
*/
|
|
820
920
|
const declaredKeys = computed(() => props.columns.map((c) => String(c.key)));
|
|
821
921
|
|
|
822
922
|
/**
|
|
@@ -827,18 +927,22 @@ const declaredKeys = computed(() => props.columns.map((c) => String(c.key)));
|
|
|
827
927
|
const dragOrder = ref<string[] | null>(null);
|
|
828
928
|
|
|
829
929
|
/**
|
|
830
|
-
* The
|
|
831
|
-
*
|
|
832
|
-
*
|
|
833
|
-
*
|
|
834
|
-
*
|
|
930
|
+
* The RENDERED columns in render order: the visible declaration, ordered.
|
|
931
|
+
* Returns `visibleColumns` BY IDENTITY (which is `props.columns` itself when
|
|
932
|
+
* nothing is hidden) whenever the resolved order is the declaration order
|
|
933
|
+
* (no model, or a model that changes nothing): every downstream computed
|
|
934
|
+
* then sees the same reference, which is what keeps the DOM, class strings
|
|
935
|
+
* and publish sequence byte-identical when the features are off. Ordering
|
|
936
|
+
* the visible subset is exact: a hidden key in the model is just a key that
|
|
937
|
+
* matches no rendered column, preserved in place like any other.
|
|
835
938
|
*/
|
|
836
939
|
const orderedColumns = computed(() => {
|
|
940
|
+
const columns = visibleColumns.value;
|
|
837
941
|
const model = dragOrder.value ?? order.value;
|
|
838
|
-
if (!model.length) return
|
|
839
|
-
const ordered = orderColumns(
|
|
840
|
-
return ordered.every((column, index) => column ===
|
|
841
|
-
?
|
|
942
|
+
if (!model.length) return columns;
|
|
943
|
+
const ordered = orderColumns(columns, model);
|
|
944
|
+
return ordered.every((column, index) => column === columns[index])
|
|
945
|
+
? columns
|
|
842
946
|
: ordered;
|
|
843
947
|
});
|
|
844
948
|
|
|
@@ -861,7 +965,13 @@ const orderedColumnKeys = computed(() =>
|
|
|
861
965
|
*/
|
|
862
966
|
const nextColumnOrder = (key: string, toIndex: number): string[] | null => {
|
|
863
967
|
const current = completeColumnOrder(order.value, declaredKeys.value);
|
|
864
|
-
const next = moveColumnKey(
|
|
968
|
+
const next = moveColumnKey(
|
|
969
|
+
order.value,
|
|
970
|
+
declaredKeys.value,
|
|
971
|
+
key,
|
|
972
|
+
toIndex,
|
|
973
|
+
hiddenColumnKeys.value
|
|
974
|
+
);
|
|
865
975
|
return sameColumnOrder(next, current) ? null : next;
|
|
866
976
|
};
|
|
867
977
|
|
|
@@ -1054,7 +1164,7 @@ if (process.env.NODE_ENV !== 'production') {
|
|
|
1054
1164
|
(conflict) => {
|
|
1055
1165
|
if (conflict) {
|
|
1056
1166
|
useLogger().warn(
|
|
1057
|
-
|
|
1167
|
+
'BbTable: `snap` names a PARENT column and the child renders in its own order — it does not compose with `order` / `reorderable` / a `hidden` column on the snapped table itself. Order and hide on the parent (a key-snapped child follows it), or drop the snaps.'
|
|
1058
1168
|
);
|
|
1059
1169
|
}
|
|
1060
1170
|
},
|
|
@@ -1580,7 +1690,13 @@ const placeDraggedColumn = (rawX: number) => {
|
|
|
1580
1690
|
const after = toIndex > from;
|
|
1581
1691
|
if (drag.target?.key === targetKey && drag.target.after === after) return;
|
|
1582
1692
|
|
|
1583
|
-
const next = moveColumnKey(
|
|
1693
|
+
const next = moveColumnKey(
|
|
1694
|
+
drag.base,
|
|
1695
|
+
declaredKeys.value,
|
|
1696
|
+
drag.key,
|
|
1697
|
+
toIndex,
|
|
1698
|
+
hiddenColumnKeys.value
|
|
1699
|
+
);
|
|
1584
1700
|
const current = dragOrder.value ?? drag.base;
|
|
1585
1701
|
if (sameColumnOrder(next, current)) {
|
|
1586
1702
|
drag.target = { key: targetKey, after };
|
|
@@ -1725,7 +1841,12 @@ const onDragPointerUp = (event: PointerEvent) => {
|
|
|
1725
1841
|
if (!drag || event.pointerId !== drag.pointerId) return;
|
|
1726
1842
|
const { key } = drag;
|
|
1727
1843
|
const from = drag.base
|
|
1728
|
-
? drag.base
|
|
1844
|
+
? drag.base
|
|
1845
|
+
.filter(
|
|
1846
|
+
(k) =>
|
|
1847
|
+
declaredKeys.value.includes(k) && !hiddenColumnKeys.value.has(k)
|
|
1848
|
+
)
|
|
1849
|
+
.indexOf(key)
|
|
1729
1850
|
: -1;
|
|
1730
1851
|
const committed =
|
|
1731
1852
|
dragPhase.value === 'dragging' &&
|
|
@@ -2078,7 +2199,7 @@ const fixedLayout = computed(
|
|
|
2078
2199
|
() =>
|
|
2079
2200
|
props.fixed ||
|
|
2080
2201
|
hasSnap.value ||
|
|
2081
|
-
|
|
2202
|
+
visibleColumns.value.some((column) => column.width != null) ||
|
|
2082
2203
|
hasResizedWidths.value ||
|
|
2083
2204
|
(inheritColumnWidths.value && !!parentId.value)
|
|
2084
2205
|
);
|
|
@@ -2088,6 +2209,12 @@ const isInheriting = computed(
|
|
|
2088
2209
|
() => inheritColumnWidths.value && !!parentId.value
|
|
2089
2210
|
);
|
|
2090
2211
|
|
|
2212
|
+
// Tell descendants whether THIS table's columns can move at runtime: a child's
|
|
2213
|
+
// key snaps are only sound against a parent whose order is static.
|
|
2214
|
+
watchEffect(() => {
|
|
2215
|
+
node.ordered = !!props.reorderable || order.value.length > 0;
|
|
2216
|
+
});
|
|
2217
|
+
|
|
2091
2218
|
/**
|
|
2092
2219
|
* `virtual` windows the body (see "Virtual rows" below). Root tables only —
|
|
2093
2220
|
* a nested table's scroller is its parent's and it inherits its tracks — and
|
|
@@ -2166,37 +2293,28 @@ const toLength = (width: number | string) => {
|
|
|
2166
2293
|
return /^-?\d*\.?\d+$/.test(trimmed) ? `${trimmed}px` : trimmed;
|
|
2167
2294
|
};
|
|
2168
2295
|
|
|
2169
|
-
/** Number of data columns the parent renders (numeric track keys). */
|
|
2170
|
-
const parentDataColumns = computed(() => {
|
|
2171
|
-
const tracks = parentNode.value?.tracks;
|
|
2172
|
-
return tracks
|
|
2173
|
-
? Object.keys(tracks).filter((key) => /^\d+$/.test(key)).length
|
|
2174
|
-
: 0;
|
|
2175
|
-
});
|
|
2176
|
-
|
|
2177
2296
|
/**
|
|
2178
|
-
* The parent's rendered columns in order — `select` (if any), data
|
|
2179
|
-
*
|
|
2180
|
-
* column. Cell `c` is the range `[c, c + 1]`. So a no-select parent
|
|
2181
|
-
* data at cell 0, while a selectable parent starts it at cell 1.
|
|
2297
|
+
* The parent's rendered columns in order — `select` (if any), its data
|
|
2298
|
+
* columns by KEY, then `actions` (if any) — form a gridline coordinate, one
|
|
2299
|
+
* cell per column. Cell `c` is the range `[c, c + 1]`. So a no-select parent
|
|
2300
|
+
* starts its data at cell 0, while a selectable parent starts it at cell 1.
|
|
2301
|
+
* Published by the parent with its tracks (`node.keys`): a record cannot
|
|
2302
|
+
* carry the order a child relies on.
|
|
2182
2303
|
*/
|
|
2183
|
-
const parentColumnKeys = computed(() =>
|
|
2184
|
-
const tracks = parentNode.value?.tracks ?? {};
|
|
2185
|
-
const keys: string[] = [];
|
|
2186
|
-
if ('select' in tracks) keys.push('select');
|
|
2187
|
-
for (let k = 0; k < parentDataColumns.value; k++) keys.push(String(k));
|
|
2188
|
-
if ('actions' in tracks) keys.push('actions');
|
|
2189
|
-
return keys;
|
|
2190
|
-
});
|
|
2304
|
+
const parentColumnKeys = computed(() => parentNode.value?.keys ?? []);
|
|
2191
2305
|
|
|
2192
2306
|
/** Gridline cells where the parent's data region begins/ends and the table ends. */
|
|
2193
2307
|
const firstDataCell = computed(() =>
|
|
2194
|
-
|
|
2195
|
-
);
|
|
2196
|
-
const dataEndCell = computed(
|
|
2197
|
-
() => firstDataCell.value + parentDataColumns.value
|
|
2308
|
+
parentColumnKeys.value[0] === 'select' ? 1 : 0
|
|
2198
2309
|
);
|
|
2199
2310
|
const tableEndCell = computed(() => parentColumnKeys.value.length);
|
|
2311
|
+
const dataEndCell = computed(() => {
|
|
2312
|
+
const keys = parentColumnKeys.value;
|
|
2313
|
+
return keys[keys.length - 1] === 'actions' ? keys.length - 1 : keys.length;
|
|
2314
|
+
});
|
|
2315
|
+
|
|
2316
|
+
/** The parent's gridline cell for a track key, `-1` when it renders none. */
|
|
2317
|
+
const parentCellOf = (key: string) => parentColumnKeys.value.indexOf(key);
|
|
2200
2318
|
|
|
2201
2319
|
/** Parent track custom property for the integer cell `[cell, cell + 1]`. */
|
|
2202
2320
|
const cellTrackVar = (cell: number) => {
|
|
@@ -2224,57 +2342,231 @@ const gridRangeWidth = (a: number, b: number): string | null => {
|
|
|
2224
2342
|
};
|
|
2225
2343
|
|
|
2226
2344
|
/**
|
|
2227
|
-
*
|
|
2228
|
-
*
|
|
2229
|
-
*
|
|
2230
|
-
*
|
|
2231
|
-
*
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
*
|
|
2345
|
+
* A snap point: a parent column key plus a fraction of that track. The
|
|
2346
|
+
* fraction is a trailing `.digits` on the raw string (`'amount.5'`); the key
|
|
2347
|
+
* part is matched RAW against the parent's published keys, like every other
|
|
2348
|
+
* key-taking field. A raw key that itself ends in `.digits` is ambiguous here
|
|
2349
|
+
* and is written in its slot-name spelling instead (documented).
|
|
2350
|
+
*/
|
|
2351
|
+
const parseSnapPoint = (raw: string): { key: string; fraction: number } => {
|
|
2352
|
+
const match = /^(.+)\.(\d+)$/.exec(raw);
|
|
2353
|
+
if (match) return { key: match[1], fraction: Number(`0.${match[2]}`) };
|
|
2354
|
+
return { key: raw, fraction: 0 };
|
|
2355
|
+
};
|
|
2356
|
+
|
|
2357
|
+
/**
|
|
2358
|
+
* A column's explicit `snap`, resolved to a parent gridline interval, or
|
|
2359
|
+
* `null` when any key it names is not a rendered parent column (hidden, or
|
|
2360
|
+
* absent) — the column then flows like an unsnapped one. `explicit` marks a
|
|
2361
|
+
* pair, which declines the trailing tracks the last column would otherwise
|
|
2362
|
+
* run across.
|
|
2363
|
+
*/
|
|
2364
|
+
const resolveSnap = (
|
|
2365
|
+
snap: string | [string, string]
|
|
2366
|
+
): { start: number; end: number; explicit: boolean } | null => {
|
|
2367
|
+
if (Array.isArray(snap)) {
|
|
2368
|
+
const a = parseSnapPoint(snap[0]);
|
|
2369
|
+
const b = parseSnapPoint(snap[1]);
|
|
2370
|
+
const cellA = parentCellOf(a.key);
|
|
2371
|
+
const cellB = parentCellOf(b.key);
|
|
2372
|
+
if (cellA < 0 || cellB < 0) return null;
|
|
2373
|
+
// Inclusive end: a whole key runs THROUGH its track, a fraction stops
|
|
2374
|
+
// partway into it.
|
|
2375
|
+
const start = cellA + a.fraction;
|
|
2376
|
+
const end = b.fraction > 0 ? cellB + b.fraction : cellB + 1;
|
|
2377
|
+
return end > start ? { start, end, explicit: true } : null;
|
|
2378
|
+
}
|
|
2379
|
+
const point = parseSnapPoint(snap);
|
|
2380
|
+
// A fraction on a single key has no second edge to apply to: ignored.
|
|
2381
|
+
const cell = parentCellOf(point.key);
|
|
2382
|
+
if (cell < 0) {
|
|
2383
|
+
// `.digits` may have been part of the key itself.
|
|
2384
|
+
const whole = parentCellOf(snap);
|
|
2385
|
+
return whole < 0 ? null : { start: whole, end: whole + 1, explicit: false };
|
|
2386
|
+
}
|
|
2387
|
+
return { start: cell, end: cell + 1, explicit: false };
|
|
2388
|
+
};
|
|
2389
|
+
|
|
2390
|
+
/**
|
|
2391
|
+
* True when the parent can reorder its data columns at runtime. A snap names
|
|
2392
|
+
* a parent column, but the child renders its own cells in its own order, so
|
|
2393
|
+
* a parent whose order can change under it can only be inherited by
|
|
2394
|
+
* POSITION — key targets are ignored there (dev builds warn; see the watcher
|
|
2395
|
+
* below). Snaps on the ROOT are a misconfiguration handled by `hasSnap`'s
|
|
2396
|
+
* implicit inherit + no parent → every interval is positional anyway.
|
|
2397
|
+
*/
|
|
2398
|
+
const parentOrdered = computed(() => !!parentNode.value?.ordered);
|
|
2399
|
+
|
|
2400
|
+
/**
|
|
2401
|
+
* Resolved parent interval for each data column, in render order. Every
|
|
2402
|
+
* interval TILES the parent's data region from left to right — cell `i`'s
|
|
2403
|
+
* left edge is the sum of the widths before it, so the intervals must be
|
|
2404
|
+
* contiguous and monotonic or the child drifts off the parent's rails.
|
|
2405
|
+
*
|
|
2406
|
+
* 1. Claims. An explicit `snap` claims its interval (unless the parent is
|
|
2407
|
+
* ordered, where snaps are ignored). An unsnapped column whose KEY the
|
|
2408
|
+
* parent renders claims that track (not under an ordered parent either —
|
|
2409
|
+
* pairing by key needs the parent's order to be stable, so there the
|
|
2410
|
+
* child inherits by position, exactly as before keys existed).
|
|
2411
|
+
* 2. Flow. Walking the columns in order with a cursor at the previous end,
|
|
2412
|
+
* an unclaimed column takes the first free parent data cell at or after
|
|
2413
|
+
* the cursor; with none left it OVERFLOWS (content-sized, past the
|
|
2414
|
+
* parent's edge). A claim that starts BEFORE the cursor cannot tile — the
|
|
2415
|
+
* child's order disagrees with the parent's — and is demoted to flow
|
|
2416
|
+
* (dev builds warn).
|
|
2417
|
+
* 3. Tiling. Each interval then runs up to the next one's start, so a free
|
|
2418
|
+
* parent cell between two claims widens the column before it rather than
|
|
2419
|
+
* opening a hole. The LAST column runs to the end of the parent's data
|
|
2420
|
+
* region — trailing tracks are never structural, the actions track
|
|
2421
|
+
* inherits only the parent's actions region — unless it declares a
|
|
2422
|
+
* `width` (the column pins its own extent) or an explicit pair, which
|
|
2423
|
+
* both decline them; they stay in the end region.
|
|
2241
2424
|
*/
|
|
2242
2425
|
const columnIntervals = computed(() => {
|
|
2243
2426
|
const cols = orderedColumns.value;
|
|
2244
|
-
|
|
2245
|
-
const
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2427
|
+
const dataStart = firstDataCell.value;
|
|
2428
|
+
const dataEnd = dataEndCell.value;
|
|
2429
|
+
const tableEnd = tableEndCell.value;
|
|
2430
|
+
const ordered = parentOrdered.value;
|
|
2431
|
+
|
|
2432
|
+
type Interval = {
|
|
2433
|
+
start: number;
|
|
2434
|
+
end: number;
|
|
2435
|
+
explicit: boolean;
|
|
2436
|
+
overflow: boolean;
|
|
2437
|
+
/** A claim that could not tile (out of the parent's order), flowed instead. */
|
|
2438
|
+
demoted?: string;
|
|
2439
|
+
};
|
|
2440
|
+
const claims: Array<Interval | null> = cols.map(() => null);
|
|
2441
|
+
const claimed = new Set<number>();
|
|
2442
|
+
const claim = (interval: Interval) => {
|
|
2443
|
+
for (let c = Math.floor(interval.start); c < interval.end - 1e-6; c++) {
|
|
2444
|
+
claimed.add(c);
|
|
2445
|
+
}
|
|
2446
|
+
};
|
|
2447
|
+
|
|
2448
|
+
// 1. Claims — explicit snaps first (they may span several cells), then keys.
|
|
2449
|
+
if (!ordered) {
|
|
2450
|
+
cols.forEach((column, i) => {
|
|
2451
|
+
if (column.snap == null) return;
|
|
2452
|
+
const resolved = resolveSnap(column.snap);
|
|
2453
|
+
if (!resolved) return;
|
|
2454
|
+
claims[i] = { ...resolved, overflow: false };
|
|
2455
|
+
claim(claims[i]!);
|
|
2456
|
+
});
|
|
2457
|
+
cols.forEach((column, i) => {
|
|
2458
|
+
if (claims[i] || column.snap != null) return;
|
|
2459
|
+
const cell = parentCellOf(String(column.key));
|
|
2460
|
+
if (cell < dataStart || cell >= dataEnd || claimed.has(cell)) return;
|
|
2461
|
+
claims[i] = {
|
|
2462
|
+
start: cell,
|
|
2463
|
+
end: cell + 1,
|
|
2464
|
+
explicit: false,
|
|
2465
|
+
overflow: false,
|
|
2466
|
+
};
|
|
2467
|
+
claimed.add(cell);
|
|
2468
|
+
});
|
|
2469
|
+
}
|
|
2470
|
+
|
|
2471
|
+
// 2. Flow. The cursor starts at the TABLE's edge (a `select` target is a
|
|
2472
|
+
// legitimate claim), but a flowing column only ever takes data cells.
|
|
2473
|
+
let cursor = 0;
|
|
2474
|
+
let overflowCount = 0;
|
|
2475
|
+
const intervals: Interval[] = cols.map((column, i) => {
|
|
2476
|
+
let interval = claims[i];
|
|
2477
|
+
let demoted: string | undefined;
|
|
2478
|
+
if (interval && interval.start + 1e-6 < cursor) {
|
|
2479
|
+
// Out of the parent's order: cannot tile. Demoted to flow.
|
|
2480
|
+
claims[i] = null;
|
|
2481
|
+
interval = null;
|
|
2482
|
+
demoted = String(column.key);
|
|
2483
|
+
}
|
|
2484
|
+
if (!interval) {
|
|
2485
|
+
// Chains from the previous column's end — a fraction included, so
|
|
2486
|
+
// the half of a track an explicit pair left behind is picked up
|
|
2487
|
+
// here — up to (never over) the next column's claim.
|
|
2488
|
+
const start = Math.max(cursor, dataStart);
|
|
2489
|
+
let limit = dataEnd;
|
|
2490
|
+
for (let j = i + 1; j < cols.length; j++) {
|
|
2491
|
+
const later = claims[j];
|
|
2492
|
+
if (later) limit = Math.min(limit, later.start);
|
|
2263
2493
|
}
|
|
2494
|
+
let cell = Math.max(Math.floor(start), dataStart);
|
|
2495
|
+
while (cell < limit && claimed.has(cell)) cell++;
|
|
2496
|
+
if (cell < limit && cell + 1e-6 >= start) {
|
|
2497
|
+
claimed.add(cell);
|
|
2498
|
+
interval = {
|
|
2499
|
+
start,
|
|
2500
|
+
end: cell + 1,
|
|
2501
|
+
explicit: false,
|
|
2502
|
+
overflow: false,
|
|
2503
|
+
};
|
|
2504
|
+
} else if (cell < limit) {
|
|
2505
|
+
// The free cell is the one the fraction sits in.
|
|
2506
|
+
claimed.add(cell);
|
|
2507
|
+
interval = { start, end: cell + 1, explicit: false, overflow: false };
|
|
2508
|
+
} else {
|
|
2509
|
+
// Past the parent's data region — or a root table, where the region
|
|
2510
|
+
// is empty and every column sits here: sized to its own content.
|
|
2511
|
+
const start = Math.max(tableEnd, dataEnd) + overflowCount++;
|
|
2512
|
+
interval = { start, end: start + 1, explicit: false, overflow: true };
|
|
2513
|
+
}
|
|
2514
|
+
if (demoted) interval.demoted = demoted;
|
|
2264
2515
|
}
|
|
2265
|
-
cursor = end;
|
|
2266
|
-
return
|
|
2516
|
+
cursor = Math.max(cursor, interval.end);
|
|
2517
|
+
return interval;
|
|
2267
2518
|
});
|
|
2519
|
+
|
|
2520
|
+
// 3. Tiling. An explicit pair pins its own extent and is never widened —
|
|
2521
|
+
// a gap after one is the consumer's (the old numeric pairs behaved the
|
|
2522
|
+
// same); everything else runs up to the next column's start.
|
|
2523
|
+
intervals.forEach((interval, i) => {
|
|
2524
|
+
if (interval.overflow || interval.explicit) return;
|
|
2525
|
+
const next = intervals[i + 1];
|
|
2526
|
+
if (next && !next.overflow && next.start > interval.end) {
|
|
2527
|
+
interval.end = next.start;
|
|
2528
|
+
}
|
|
2529
|
+
if (i === cols.length - 1 && cols[i].width == null) {
|
|
2530
|
+
interval.end = Math.max(interval.end, dataEnd);
|
|
2531
|
+
}
|
|
2532
|
+
});
|
|
2533
|
+
return intervals;
|
|
2268
2534
|
});
|
|
2269
2535
|
|
|
2270
2536
|
/**
|
|
2271
|
-
* A column
|
|
2272
|
-
*
|
|
2273
|
-
*
|
|
2274
|
-
*
|
|
2537
|
+
* A column with no matching parent track to inherit from — the table has more
|
|
2538
|
+
* columns than its parent, or nothing free was left for a displaced one. Such
|
|
2539
|
+
* columns are sized to their own measured content and scroll within the
|
|
2540
|
+
* expand row instead of inheriting a track.
|
|
2275
2541
|
*/
|
|
2276
|
-
const isOverflowColumn = (interval: {
|
|
2277
|
-
isInheriting.value && interval.
|
|
2542
|
+
const isOverflowColumn = (interval: { overflow: boolean }) =>
|
|
2543
|
+
isInheriting.value && interval.overflow;
|
|
2544
|
+
|
|
2545
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
2546
|
+
// A snap under a parent that can reorder: ignored (the child inherits by
|
|
2547
|
+
// position, as it did before keys). Warned once per parent, not per render.
|
|
2548
|
+
watch(
|
|
2549
|
+
() => hasSnap.value && parentOrdered.value,
|
|
2550
|
+
(ignored) => {
|
|
2551
|
+
if (ignored) {
|
|
2552
|
+
useLogger().warn(
|
|
2553
|
+
"BbTable: `snap` targets are ignored under a parent that uses `order` / `reorderable` — the parent's columns can move, so the child inherits by position. Keep the parent's order static (no `reorderable`, an empty `order`) to snap by key."
|
|
2554
|
+
);
|
|
2555
|
+
}
|
|
2556
|
+
},
|
|
2557
|
+
{ immediate: true }
|
|
2558
|
+
);
|
|
2559
|
+
watch(
|
|
2560
|
+
() => columnIntervals.value.find((i) => i.demoted)?.demoted ?? null,
|
|
2561
|
+
(key) => {
|
|
2562
|
+
if (key === null) return;
|
|
2563
|
+
useLogger().warn(
|
|
2564
|
+
`BbTable: column '${key}' snaps to a parent column that sits BEFORE the column preceding it — the child's order disagrees with the parent's, so the snap cannot tile and the column flows into the next free parent track instead. Declare the child's columns in the parent's order.`
|
|
2565
|
+
);
|
|
2566
|
+
},
|
|
2567
|
+
{ immediate: true }
|
|
2568
|
+
);
|
|
2569
|
+
}
|
|
2278
2570
|
|
|
2279
2571
|
/** True when any column overflows the parent (more columns than the parent). */
|
|
2280
2572
|
const overflowsParent = computed(() =>
|
|
@@ -2402,8 +2694,8 @@ const offsetEndValue = computed(() =>
|
|
|
2402
2694
|
? null
|
|
2403
2695
|
: edgeFold(
|
|
2404
2696
|
endRegion.value,
|
|
2405
|
-
|
|
2406
|
-
|
|
2697
|
+
hasActions.value ? ownStructuralTrack('actions') : null,
|
|
2698
|
+
hasActions.value
|
|
2407
2699
|
)
|
|
2408
2700
|
);
|
|
2409
2701
|
const offsetStartActive = computed(() => offsetStartValue.value !== null);
|
|
@@ -2508,10 +2800,10 @@ const columnTracks = computed(() => {
|
|
|
2508
2800
|
// `isResized`); the declared-only columns keep filling around it.
|
|
2509
2801
|
const allFrozen =
|
|
2510
2802
|
!isInheriting.value &&
|
|
2511
|
-
|
|
2512
|
-
|
|
2803
|
+
columns.length > 0 &&
|
|
2804
|
+
columns.every((column) => column.width != null);
|
|
2513
2805
|
columns.forEach((column, index) => {
|
|
2514
|
-
const key = String(
|
|
2806
|
+
const key = String(column.key);
|
|
2515
2807
|
const declared = effectiveWidth(column);
|
|
2516
2808
|
const frozen = declared != null ? toLength(declared) : null;
|
|
2517
2809
|
const interval = columnIntervals.value[index];
|
|
@@ -2591,7 +2883,7 @@ const columnTracks = computed(() => {
|
|
|
2591
2883
|
tracks.push({ key, width, contentSized: false });
|
|
2592
2884
|
});
|
|
2593
2885
|
|
|
2594
|
-
if (
|
|
2886
|
+
if (hasActions.value) {
|
|
2595
2887
|
tracks.push({
|
|
2596
2888
|
key: 'actions',
|
|
2597
2889
|
width: structuralTrack(endRegion.value, 'actions'),
|
|
@@ -2618,8 +2910,9 @@ const clippedKeyList = computed(() => {
|
|
|
2618
2910
|
const keys: string[] = [];
|
|
2619
2911
|
for (const track of columnTracks.value) {
|
|
2620
2912
|
if (track.contentSized) continue;
|
|
2621
|
-
|
|
2622
|
-
if (column)
|
|
2913
|
+
// Data tracks carry the column key itself; skip the structural two.
|
|
2914
|
+
if (columns.some((column) => String(column.key) === track.key))
|
|
2915
|
+
keys.push(track.key);
|
|
2623
2916
|
}
|
|
2624
2917
|
return keys.join(' ');
|
|
2625
2918
|
});
|
|
@@ -2864,18 +3157,29 @@ const measureTracks = () => {
|
|
|
2864
3157
|
// published unfolded — the pure content rail — and the region goes out
|
|
2865
3158
|
// under the structural key even though no cell renders it, so the next
|
|
2866
3159
|
// level reserves it exactly as this one did.
|
|
2867
|
-
|
|
3160
|
+
// Track keys are COLUMN KEYS in render order, so the edge data tracks are
|
|
3161
|
+
// the first and last RENDERED columns, not the first and last declared.
|
|
3162
|
+
const dataKeys = orderedColumnKeys.value;
|
|
3163
|
+
const firstKey = dataKeys[0];
|
|
3164
|
+
const lastKey = dataKeys[dataKeys.length - 1];
|
|
2868
3165
|
const startFold = offsetStartValue.value;
|
|
2869
3166
|
const endFold = offsetEndValue.value;
|
|
2870
|
-
if (startFold && tracks[
|
|
2871
|
-
tracks[
|
|
3167
|
+
if (startFold && firstKey && tracks[firstKey]) {
|
|
3168
|
+
tracks[firstKey] = `calc(${tracks[firstKey]} - (${startFold}))`;
|
|
2872
3169
|
if (!props.selectable && startRegion.value)
|
|
2873
3170
|
tracks.select = startRegion.value;
|
|
2874
3171
|
}
|
|
2875
|
-
if (endFold && tracks[lastKey]) {
|
|
3172
|
+
if (endFold && lastKey && tracks[lastKey]) {
|
|
2876
3173
|
tracks[lastKey] = `calc(${tracks[lastKey]} - (${endFold}))`;
|
|
2877
|
-
if (!
|
|
3174
|
+
if (!hasActions.value && endRegion.value) tracks.actions = endRegion.value;
|
|
2878
3175
|
}
|
|
3176
|
+
// The order a child turns keys into gridlines with: `select` region first
|
|
3177
|
+
// (own or reserved), the rendered data keys, the `actions` region last.
|
|
3178
|
+
const publishedKeys = [
|
|
3179
|
+
...('select' in tracks ? ['select'] : []),
|
|
3180
|
+
...dataKeys,
|
|
3181
|
+
...('actions' in tracks ? ['actions'] : []),
|
|
3182
|
+
];
|
|
2879
3183
|
freezeVirtualWidths(resolved);
|
|
2880
3184
|
syncPlaceholderBars(resolved);
|
|
2881
3185
|
// FLAT ROWS: the resolved list IS what a body row uses as its own tracks.
|
|
@@ -2883,10 +3187,10 @@ const measureTracks = () => {
|
|
|
2883
3187
|
// (the header cells' observer) carries the body along for free.
|
|
2884
3188
|
if (virtualActive.value) virtualRowTracks.value = joined;
|
|
2885
3189
|
lastResolvedTracks = resolved;
|
|
2886
|
-
setTracks(tracks);
|
|
3190
|
+
setTracks(tracks, publishedKeys);
|
|
2887
3191
|
// Sticky column offsets are prefix sums of these tracks: keep them current.
|
|
2888
|
-
if (
|
|
2889
|
-
applyFixedTableColumns(root,
|
|
3192
|
+
if (resolvedPins.value.length) {
|
|
3193
|
+
applyFixedTableColumns(root, resolvedPins.value, resolved);
|
|
2890
3194
|
}
|
|
2891
3195
|
};
|
|
2892
3196
|
|
|
@@ -3043,7 +3347,9 @@ const containerClass = computed(() => ({
|
|
|
3043
3347
|
|
|
3044
3348
|
const replacementContentSpan = computed(
|
|
3045
3349
|
() =>
|
|
3046
|
-
|
|
3350
|
+
visibleColumns.value.length +
|
|
3351
|
+
Number(!!props.selectable) +
|
|
3352
|
+
Number(hasActions.value)
|
|
3047
3353
|
);
|
|
3048
3354
|
|
|
3049
3355
|
const slotNamesByKey = computed(() =>
|
|
@@ -3278,6 +3584,7 @@ const headerAffixScope = (
|
|
|
3278
3584
|
sortable: header.sortable,
|
|
3279
3585
|
sortOrder: header.sortOrder,
|
|
3280
3586
|
toggleSort: () => toggleSortFor(header.key),
|
|
3587
|
+
hideColumn: () => hideColumn(header.key),
|
|
3281
3588
|
});
|
|
3282
3589
|
|
|
3283
3590
|
/**
|
|
@@ -4365,7 +4672,7 @@ const freezeVirtualWidths = (resolved: string[]) => {
|
|
|
4365
4672
|
structural;
|
|
4366
4673
|
return;
|
|
4367
4674
|
}
|
|
4368
|
-
const column = columns
|
|
4675
|
+
const column = columns.find((c) => String(c.key) === track.key);
|
|
4369
4676
|
if (!column || column.width != null) return;
|
|
4370
4677
|
const key = String(column.key);
|
|
4371
4678
|
if (key in virtualWidths.value || key in (next ?? {})) return;
|
|
@@ -4786,7 +5093,7 @@ const dataRowContext = computed<DataRowContext>(() => ({
|
|
|
4786
5093
|
selectable: props.selectable,
|
|
4787
5094
|
multiple: !!props.multiple,
|
|
4788
5095
|
readonly: !!props.readonly,
|
|
4789
|
-
actions:
|
|
5096
|
+
actions: hasActions.value,
|
|
4790
5097
|
keyboardNavigation: !!props.keyboardNavigation,
|
|
4791
5098
|
contentInert: contentInert.value,
|
|
4792
5099
|
hasRowIndex: hasProvidedAccessibilityData.value,
|
|
@@ -4803,23 +5110,49 @@ const dataRowContext = computed<DataRowContext>(() => ({
|
|
|
4803
5110
|
}));
|
|
4804
5111
|
|
|
4805
5112
|
/**
|
|
4806
|
-
* The pins
|
|
4807
|
-
*
|
|
4808
|
-
*
|
|
4809
|
-
*
|
|
4810
|
-
*
|
|
4811
|
-
*
|
|
4812
|
-
|
|
4813
|
-
|
|
4814
|
-
|
|
4815
|
-
|
|
5113
|
+
* The pins, resolved from keys to RENDERED positions for the sticky pass:
|
|
5114
|
+
* `column.fixed` on each rendered data column (hidden ones pin nothing), plus
|
|
5115
|
+
* `fixed-select` at slot 0 and `fixed-actions` at the last slot when those
|
|
5116
|
+
* structural columns exist. The pass itself stays positional — offsets are
|
|
5117
|
+
* prefix sums of the rendered tracks — but the CONFIG follows the column, so
|
|
5118
|
+
* a reorder moves the pin with it instead of leaving it in the slot.
|
|
5119
|
+
*/
|
|
5120
|
+
const resolvedPins = computed<FixedColumnConfig[]>(() => {
|
|
5121
|
+
const pins: FixedColumnConfig[] = [];
|
|
5122
|
+
const offset = props.selectable ? 1 : 0;
|
|
5123
|
+
if (props.selectable && props.fixedSelect) {
|
|
5124
|
+
pins.push({ index: 0, position: 'left' });
|
|
5125
|
+
}
|
|
5126
|
+
orderedColumns.value.forEach((column, index) => {
|
|
5127
|
+
if (column.fixed) {
|
|
5128
|
+
pins.push({ index: index + offset, position: column.fixed });
|
|
5129
|
+
}
|
|
5130
|
+
});
|
|
5131
|
+
if (hasActions.value && props.fixedActions) {
|
|
5132
|
+
pins.push({
|
|
5133
|
+
index: orderedColumns.value.length + offset,
|
|
5134
|
+
position: 'right',
|
|
5135
|
+
});
|
|
5136
|
+
}
|
|
5137
|
+
return pins;
|
|
5138
|
+
});
|
|
5139
|
+
|
|
5140
|
+
/**
|
|
5141
|
+
* The pins as a VALUE. `:columns` written inline in a template is a fresh
|
|
5142
|
+
* array on every render of the parent, and the parent of a table re-renders
|
|
5143
|
+
* for reasons of its own all the time; `resolvedPins` is a fresh array on
|
|
5144
|
+
* every one of those too. Watching it by identity made every such render look
|
|
5145
|
+
* like "the pins changed" — which ran a full reconcile pass AND, because the
|
|
5146
|
+
* reuse guard compared identities, threw away the tracks and read
|
|
5147
|
+
* `grid-template-columns` back off the grid root. That read is a forced
|
|
5148
|
+
* style+layout of a 5,000-row scrollport (~15ms measured on the bench
|
|
5149
|
+
* playground), taken up to once per scrolled frame, for pins whose VALUE
|
|
5150
|
+
* never moved. The signature makes an identity-only change invisible here.
|
|
4816
5151
|
*/
|
|
4817
5152
|
const fixedColumnsKey = computed(() =>
|
|
4818
|
-
|
|
4819
|
-
.map((
|
|
4820
|
-
typeof
|
|
4821
|
-
? `${column}:left`
|
|
4822
|
-
: `${column.index}:${column.position}`
|
|
5153
|
+
resolvedPins.value
|
|
5154
|
+
.map((pin) =>
|
|
5155
|
+
typeof pin === 'number' ? `${pin}:left` : `${pin.index}:${pin.position}`
|
|
4823
5156
|
)
|
|
4824
5157
|
.join(',')
|
|
4825
5158
|
);
|
|
@@ -4842,18 +5175,19 @@ watch(
|
|
|
4842
5175
|
() => [bodyRows.value, fixedColumnsKey.value, trackKeys.value] as const,
|
|
4843
5176
|
([, pins, tracks], previous) => {
|
|
4844
5177
|
const previousPins = previous?.[1];
|
|
4845
|
-
const fixedColumns =
|
|
5178
|
+
const fixedColumns = resolvedPins.value;
|
|
4846
5179
|
if (tableRef.value && (pins.length || previousPins?.length)) {
|
|
4847
5180
|
// A `virtual` window move mounts rows but moves no track: reuse the
|
|
4848
5181
|
// last publish instead of forcing a style read.
|
|
4849
5182
|
//
|
|
4850
|
-
// The reuse test used to also require
|
|
4851
|
-
// which asks a question about the CONSUMER's render
|
|
4852
|
-
// this table's geometry. `:
|
|
4853
|
-
//
|
|
4854
|
-
//
|
|
4855
|
-
// the test failed on essentially every window move
|
|
4856
|
-
// back to `getComputedStyle(root).gridTemplateColumns`.
|
|
5183
|
+
// The reuse test used to also require the pin config to be the SAME
|
|
5184
|
+
// array as last time, which asks a question about the CONSUMER's render
|
|
5185
|
+
// rather than about this table's geometry. An inline `:columns` array
|
|
5186
|
+
// — the documented form, and what the bench playground writes —
|
|
5187
|
+
// allocates a fresh array every time the parent renders (and so does
|
|
5188
|
+
// `resolvedPins`), so the test failed on essentially every window move
|
|
5189
|
+
// and the pass fell back to `getComputedStyle(root).gridTemplateColumns`.
|
|
5190
|
+
// That read
|
|
4857
5191
|
// forces style and layout across the whole subgrid: on the 5,000-row
|
|
4858
5192
|
// playground it measured ELEVEN MILLISECONDS each, once per window
|
|
4859
5193
|
// move, in the middle of the scroll — the largest single item on a
|
|
@@ -4894,7 +5228,7 @@ watch(
|
|
|
4894
5228
|
// just moved onto — ~500 cell touches per window move down to ~50.
|
|
4895
5229
|
applyFixedTableColumns(
|
|
4896
5230
|
tableRef.value,
|
|
4897
|
-
fixedColumns
|
|
5231
|
+
fixedColumns,
|
|
4898
5232
|
reuse ? lastResolvedTracks : null,
|
|
4899
5233
|
reuse
|
|
4900
5234
|
);
|
|
@@ -5048,6 +5382,33 @@ export type BbTableColumn<Item = any> = BaseColumn<Item> & {
|
|
|
5048
5382
|
*/
|
|
5049
5383
|
thClass?: Classes;
|
|
5050
5384
|
|
|
5385
|
+
/**
|
|
5386
|
+
* Hides the column: it renders no header and no cells and contributes no
|
|
5387
|
+
* grid track, but stays DECLARED — its key keeps its slot in `order`, the
|
|
5388
|
+
* header reorder writes the model back complete around it, and it comes
|
|
5389
|
+
* back exactly where it was when the flag clears. This is how a column
|
|
5390
|
+
* panel or a narrow-screen breakpoint hides a column; filtering `columns`
|
|
5391
|
+
* is for a column that does not exist at all (a field this user does not
|
|
5392
|
+
* have). Like `width`, the definition is the single declared truth: a
|
|
5393
|
+
* header's `hideColumn()` sets a per-mount override that `hide:column` reports
|
|
5394
|
+
* and that clears the moment this flag changes to anything but `true`
|
|
5395
|
+
* (write the reported key back as `hidden: true` to persist it).
|
|
5396
|
+
*/
|
|
5397
|
+
hidden?: boolean;
|
|
5398
|
+
|
|
5399
|
+
/**
|
|
5400
|
+
* Pins the column: its cells become `position: sticky` on the given side,
|
|
5401
|
+
* in place — the column keeps its render slot and sticks there as the rest
|
|
5402
|
+
* scrolls under it (spreadsheet freeze panes, not a pinned region). The
|
|
5403
|
+
* offset accumulates the pinned columns before it on the same side, so
|
|
5404
|
+
* pin a contiguous run from the edge: a lone `left` column in the middle
|
|
5405
|
+
* sticks at `left: 0` and slides over everything before it, by design.
|
|
5406
|
+
* The pin travels with the column through `order` and `reorderable`, and
|
|
5407
|
+
* a `hidden` pinned column pins nothing. The structural columns pin
|
|
5408
|
+
* through `fixed-select` / `fixed-actions` on the table.
|
|
5409
|
+
*/
|
|
5410
|
+
fixed?: 'left' | 'right';
|
|
5411
|
+
|
|
5051
5412
|
/**
|
|
5052
5413
|
* Freezes the column to a fixed width (numbers are treated as px). When
|
|
5053
5414
|
* omitted the column syncs its width to the parent table's matching track.
|
|
@@ -5055,26 +5416,36 @@ export type BbTableColumn<Item = any> = BaseColumn<Item> & {
|
|
|
5055
5416
|
width?: number | string;
|
|
5056
5417
|
|
|
5057
5418
|
/**
|
|
5058
|
-
* For a nested table,
|
|
5059
|
-
* parent's
|
|
5060
|
-
*
|
|
5419
|
+
* For a nested table, which parent column this column snaps onto — by the
|
|
5420
|
+
* parent's column KEY, as written (`'client'`, not a position), plus the
|
|
5421
|
+
* reserved names `'select'` and `'actions'` for the parent's structural
|
|
5422
|
+
* columns.
|
|
5423
|
+
*
|
|
5424
|
+
* - A single key takes that parent track: `snap: 'client'`.
|
|
5425
|
+
* - A `[start, end]` pair spans from the start of `start` through the END
|
|
5426
|
+
* of `end`, inclusive: `['client', 'amount']`, or `['name', 'actions']`
|
|
5427
|
+
* to run across the parent's actions column too.
|
|
5428
|
+
* - A trailing `.digits` on a key is a fraction of that track — `'amount.5'`
|
|
5429
|
+
* as an end means "through half of `amount`", as a start "from halfway
|
|
5430
|
+
* into it". Pairs only; a key that itself ends in `.digits` is written
|
|
5431
|
+
* in its slot-name spelling (`line_1`).
|
|
5061
5432
|
*
|
|
5062
|
-
*
|
|
5063
|
-
*
|
|
5064
|
-
*
|
|
5065
|
-
*
|
|
5066
|
-
*
|
|
5067
|
-
*
|
|
5068
|
-
*
|
|
5433
|
+
* When omitted the column pairs with the parent column of the SAME key when
|
|
5434
|
+
* there is one, and otherwise flows into the next parent track no other
|
|
5435
|
+
* column claimed. A column whose target the parent does not render (a
|
|
5436
|
+
* `hidden` parent column, a key it lacks) flows the same way; with no free
|
|
5437
|
+
* track left it content-sizes and overflows. The last column runs to the end
|
|
5438
|
+
* of the parent's data region unless it declares a `width` or an explicit
|
|
5439
|
+
* pair, so a table with fewer columns than its parent widens its last
|
|
5440
|
+
* content cell across the parent's trailing tracks rather than its actions
|
|
5441
|
+
* cell.
|
|
5069
5442
|
*
|
|
5070
|
-
*
|
|
5071
|
-
*
|
|
5072
|
-
*
|
|
5073
|
-
*
|
|
5074
|
-
* columns than its parent widens its last content cell across the parent's
|
|
5075
|
-
* trailing tracks rather than its actions cell.
|
|
5443
|
+
* Snaps resolve against the parent's RENDERED columns, so they are ignored
|
|
5444
|
+
* (dev builds warn) under a parent that reorders — `reorderable`, or a
|
|
5445
|
+
* non-empty `order` model — where the child falls back to positional
|
|
5446
|
+
* inheritance. Never reorder the snapped child itself.
|
|
5076
5447
|
*/
|
|
5077
|
-
snap?:
|
|
5448
|
+
snap?: string | [start: string, end: string];
|
|
5078
5449
|
};
|
|
5079
5450
|
|
|
5080
5451
|
export type BbTableProps<Item = any> = {
|
|
@@ -5085,10 +5456,6 @@ export type BbTableProps<Item = any> = {
|
|
|
5085
5456
|
* label reads like the row) — look a cell up by `key`, never by position.
|
|
5086
5457
|
*/
|
|
5087
5458
|
accessibleLabel?: (columns: MappedCell[], item: any) => string;
|
|
5088
|
-
/**
|
|
5089
|
-
* Displays the actions column.
|
|
5090
|
-
*/
|
|
5091
|
-
actions?: boolean;
|
|
5092
5459
|
/**
|
|
5093
5460
|
* Label used in the header of the actions column.
|
|
5094
5461
|
*/
|
|
@@ -5175,22 +5542,27 @@ export type BbTableProps<Item = any> = {
|
|
|
5175
5542
|
*/
|
|
5176
5543
|
fixed?: boolean;
|
|
5177
5544
|
/**
|
|
5178
|
-
*
|
|
5179
|
-
*
|
|
5180
|
-
*
|
|
5545
|
+
* Pins the actions column (the one the `#actions` slot creates) to the
|
|
5546
|
+
* right edge, `position: sticky`, in place. Data columns pin through
|
|
5547
|
+
* `column.fixed`; this is the same feature for the structural column that
|
|
5548
|
+
* has no definition to carry it. No effect without an `#actions` slot.
|
|
5181
5549
|
*
|
|
5182
|
-
*
|
|
5183
|
-
* index 0), like a spreadsheet's freeze panes — not per-column pins. With
|
|
5184
|
-
* `order` the pinned slot holds and whichever column lands in it is
|
|
5185
|
-
* pinned; dragging a column out of a pinned slot unpins it.
|
|
5186
|
-
*
|
|
5187
|
-
* @defaultValue `[]`
|
|
5550
|
+
* @defaultValue `false`
|
|
5188
5551
|
*/
|
|
5189
|
-
|
|
5552
|
+
fixedActions?: boolean;
|
|
5190
5553
|
/**
|
|
5191
5554
|
* Boolean that sets the headers as sticky to the top of the table.
|
|
5192
5555
|
*/
|
|
5193
5556
|
fixedHeaders?: boolean;
|
|
5557
|
+
/**
|
|
5558
|
+
* Pins the selection column to the left edge, `position: sticky`, in
|
|
5559
|
+
* place. Data columns pin through `column.fixed`; this is the same feature
|
|
5560
|
+
* for the structural column that has no definition to carry it. No effect
|
|
5561
|
+
* unless the table is `selectable`.
|
|
5562
|
+
*
|
|
5563
|
+
* @defaultValue `false`
|
|
5564
|
+
*/
|
|
5565
|
+
fixedSelect?: boolean;
|
|
5194
5566
|
/**
|
|
5195
5567
|
* Defines the classes to be passed to the header row.
|
|
5196
5568
|
*/
|
|
@@ -5317,16 +5689,15 @@ export type BbTableProps<Item = any> = {
|
|
|
5317
5689
|
* Used by `v-model:order`. Ordered array of column keys — the render
|
|
5318
5690
|
* order of the data columns. Keys listed here render first, in this order;
|
|
5319
5691
|
* declared columns it omits follow, in declaration order (so a column added
|
|
5320
|
-
* to `columns` later renders last);
|
|
5321
|
-
*
|
|
5322
|
-
*
|
|
5323
|
-
*
|
|
5324
|
-
*
|
|
5325
|
-
*
|
|
5326
|
-
* key
|
|
5327
|
-
*
|
|
5328
|
-
*
|
|
5329
|
-
* part of it.
|
|
5692
|
+
* to `columns` later renders last); a `hidden` column keeps its slot
|
|
5693
|
+
* without rendering, and keys that match no declared column render
|
|
5694
|
+
* nothing but are PRESERVED in place on every write. The header drag /
|
|
5695
|
+
* keyboard reorder of `reorderable` writes it back complete (every
|
|
5696
|
+
* declared key present, hidden ones included, duplicates dropped — first
|
|
5697
|
+
* occurrence wins) with exactly the moved column relocated: every other
|
|
5698
|
+
* key keeps its relative order, and a drop that changes nothing emits
|
|
5699
|
+
* nothing. Works uncontrolled when unbound, exactly like `sort`. The
|
|
5700
|
+
* `select` and `actions` columns are structural and never part of it.
|
|
5330
5701
|
*
|
|
5331
5702
|
* @defaultValue `[]`
|
|
5332
5703
|
*/
|
|
@@ -5564,6 +5935,14 @@ export type BbTableEvents = {
|
|
|
5564
5935
|
* its declared width.
|
|
5565
5936
|
*/
|
|
5566
5937
|
(e: 'resize:column', key: string, width: number | null): void;
|
|
5938
|
+
/**
|
|
5939
|
+
* A column was hidden from its header (a `#header:<key>` /
|
|
5940
|
+
* `#header:<key>:append` control calling the slot's `hideColumn()`). Fired once;
|
|
5941
|
+
* the table keeps the column hidden for the mount, or until the column's
|
|
5942
|
+
* `hidden` flag changes — write `hidden: true` into the definition to
|
|
5943
|
+
* persist it, `hidden: false` (a column panel) to show it again.
|
|
5944
|
+
*/
|
|
5945
|
+
(e: 'hide:column', key: string): void;
|
|
5567
5946
|
(e: 'update:expandedItems', value: any[]): void;
|
|
5568
5947
|
(e: 'update:unselectedItems', value: any[]): void;
|
|
5569
5948
|
(e: 'item:selected', value: any): void;
|
|
@@ -5593,6 +5972,12 @@ export type BbTableHeaderAffixSlotProps<Item = any> = {
|
|
|
5593
5972
|
sortOrder?: 'asc' | 'desc' | null;
|
|
5594
5973
|
/** Cycles this column's sort: unsorted → asc → desc → unsorted. */
|
|
5595
5974
|
toggleSort?: () => void;
|
|
5975
|
+
/**
|
|
5976
|
+
* Hides this column (fires `hide:column`). One-way by nature — a hidden
|
|
5977
|
+
* column has no header to call it from; showing it again is the
|
|
5978
|
+
* definition's job (`hidden: false`).
|
|
5979
|
+
*/
|
|
5980
|
+
hideColumn?: () => void;
|
|
5596
5981
|
};
|
|
5597
5982
|
|
|
5598
5983
|
export type BbTableSlots<Item = any> = {
|
|
@@ -5668,7 +6053,11 @@ export type BbTableSlots<Item = any> = {
|
|
|
5668
6053
|
*/
|
|
5669
6054
|
'no-data'?: (props: object) => any;
|
|
5670
6055
|
/**
|
|
5671
|
-
* Content rendered in the actions cell for each row.
|
|
6056
|
+
* Content rendered in the actions cell for each row. Providing this slot
|
|
6057
|
+
* is what CREATES the actions column (there is no prop): the header cell
|
|
6058
|
+
* (labelled by `actions-text`, replaceable through `#header:actions`) and
|
|
6059
|
+
* one cell per row appear with it, and go away with it. Pin it with
|
|
6060
|
+
* `fixed-actions`.
|
|
5672
6061
|
* @param expanded - Whether this row is currently expanded.
|
|
5673
6062
|
* @param expandProps - Bind these onto an expand-toggle button to wire up aria attributes and keyboard handling.
|
|
5674
6063
|
* @param toggleExpanded - Toggles the expanded state for this row.
|
|
@@ -5752,6 +6141,8 @@ export type BbTableSlots<Item = any> = {
|
|
|
5752
6141
|
sortable?: boolean;
|
|
5753
6142
|
sortOrder?: 'asc' | 'desc' | null;
|
|
5754
6143
|
toggleSort?: () => void;
|
|
6144
|
+
/** Header slots only: hides this column (fires `hide:column`). */
|
|
6145
|
+
hideColumn?: () => void;
|
|
5755
6146
|
/**
|
|
5756
6147
|
* Optional because this catch-all also types the `head:*` /
|
|
5757
6148
|
* `header:*` column slots, which have no row and never receive one.
|
|
@@ -5770,7 +6161,7 @@ export type BbTableSlots<Item = any> = {
|
|
|
5770
6161
|
.bb-table {
|
|
5771
6162
|
/* The table's surface colour. Cells are transparent at rest so the row can
|
|
5772
6163
|
tint them, but anything that sticks over scrolled content — the stuck
|
|
5773
|
-
head band and `fixed
|
|
6164
|
+
head band and pinned (`column.fixed`) cells — must be opaque, and paints this.
|
|
5774
6165
|
Set it on the table (`.my-table { --bg: … }`) when the table sits on a
|
|
5775
6166
|
surface that is not `--bb-panel`; a nested table re-declares its own. */
|
|
5776
6167
|
--bg: var(--bb-panel);
|
|
@@ -6010,7 +6401,7 @@ export type BbTableSlots<Item = any> = {
|
|
|
6010
6401
|
is bounded by its own grid container — the one-row-tall row). Descendant
|
|
6011
6402
|
form on purpose, so a nested table's head sticks along, as it always did.
|
|
6012
6403
|
The stuck band paints `--bg` so scrolled rows never show through it.
|
|
6013
|
-
Sticky-column header cells (inline sticky from `fixed
|
|
6404
|
+
Sticky-column header cells (inline sticky from `column.fixed`) sit above
|
|
6014
6405
|
their scrolled siblings within the band. */
|
|
6015
6406
|
/* Own head only (`> .bb-table__table >`): a descendant selector would also
|
|
6016
6407
|
stick every NESTED table's head — a nested head then painted over the
|
|
@@ -6048,7 +6439,7 @@ export type BbTableSlots<Item = any> = {
|
|
|
6048
6439
|
}
|
|
6049
6440
|
}
|
|
6050
6441
|
|
|
6051
|
-
/* A
|
|
6442
|
+
/* A pinned cell (`column.fixed`, inline `position: sticky` from the component)
|
|
6052
6443
|
slides over its scrolled siblings, so it cannot stay transparent. It
|
|
6053
6444
|
paints the row's current tint when the row has one (`--row-bg`, set by
|
|
6054
6445
|
the hover / highlighted states below), else the table surface — so a
|
|
@@ -6090,7 +6481,7 @@ export type BbTableSlots<Item = any> = {
|
|
|
6090
6481
|
A child that inherits its parent's tracks is sized to the parent's
|
|
6091
6482
|
CONTENT width, so a scrollport here would have its right edge off in
|
|
6092
6483
|
the overflow where nobody can see it, and `position: sticky` from
|
|
6093
|
-
`fixed
|
|
6484
|
+
`column.fixed` would affix to that instead of to the visible edge:
|
|
6094
6485
|
the parent's actions pin correctly while the child's sit hundreds of
|
|
6095
6486
|
pixels off-screen until you scroll all the way right.
|
|
6096
6487
|
|
|
@@ -6280,7 +6671,7 @@ export type BbTableSlots<Item = any> = {
|
|
|
6280
6671
|
user-select: none;
|
|
6281
6672
|
width: 9px;
|
|
6282
6673
|
/* Above the neighbour cell it overhangs (positioned, z auto) and below a
|
|
6283
|
-
pinned cell (z 2), so a handle scrolled under a
|
|
6674
|
+
pinned cell (z 2), so a handle scrolled under a pinned header
|
|
6284
6675
|
never slides over it. A pinned header's own handle lives inside that
|
|
6285
6676
|
cell's stacking context and keeps its overhang. */
|
|
6286
6677
|
z-index: 1;
|