bitboss-ui 2.1.127 → 2.1.129
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 +875 -6
- package/dist/components/BbTable/types.d.ts +38 -0
- package/dist/composables/useTableWidthContext.d.ts +42 -0
- package/dist/index.css +1 -1
- package/dist/index.js +9 -9
- package/dist/index240.js +1 -1
- package/dist/index270.js +1 -1
- package/dist/index325.js +38 -41
- package/dist/index326.js +40 -447
- package/dist/index327.js +664 -0
- package/dist/index329.js +5 -154
- package/dist/index330.js +155 -0
- package/dist/index332.js +5 -141
- package/dist/index333.js +142 -0
- package/dist/index335.js +5 -154
- package/dist/index336.js +155 -0
- package/dist/index338.js +5 -57
- package/dist/index339.js +57 -4
- package/dist/index340.js +4 -19
- package/dist/index341.js +20 -0
- package/dist/index343.js +5 -141
- package/dist/index344.js +142 -0
- package/dist/index346.js +5 -3
- package/dist/index347.js +3 -108
- package/dist/index348.js +109 -0
- package/dist/index350.js +5 -54
- package/dist/index351.js +54 -23
- package/dist/index352.js +12 -11
- package/dist/index353.js +22 -838
- package/dist/index354.js +811 -446
- package/dist/index355.js +472 -50
- package/dist/index356.js +51 -6
- package/dist/index357.js +7 -0
- package/package.json +2 -2
- package/dist/index328.js +0 -6
- package/dist/index331.js +0 -6
- package/dist/index334.js +0 -6
- package/dist/index337.js +0 -6
- package/dist/index342.js +0 -6
- package/dist/index345.js +0 -6
- package/dist/index349.js +0 -6
package/dist/ai/BbTable.md
CHANGED
|
@@ -6,10 +6,13 @@
|
|
|
6
6
|
<template>
|
|
7
7
|
<component
|
|
8
8
|
:is="containerTag"
|
|
9
|
+
ref="containerRef"
|
|
9
10
|
:aria-busy="loading"
|
|
10
11
|
:aria-rowcount="hasProvidedAccessibilityData ? props.totalItems : undefined"
|
|
11
12
|
:class="containerClass"
|
|
13
|
+
:data-bb-table-id="tableId"
|
|
12
14
|
:inert="disabled"
|
|
15
|
+
:style="containerStyle"
|
|
13
16
|
>
|
|
14
17
|
<legend v-if="selectable" class="sr-only">{{ legend }}</legend>
|
|
15
18
|
<table ref="tableRef">
|
|
@@ -24,6 +27,13 @@
|
|
|
24
27
|
caption
|
|
25
28
|
}}
|
|
26
29
|
</caption>
|
|
30
|
+
<colgroup>
|
|
31
|
+
<col
|
|
32
|
+
v-for="track in columnTracks"
|
|
33
|
+
:key="track.key"
|
|
34
|
+
:style="{ width: track.width }"
|
|
35
|
+
/>
|
|
36
|
+
</colgroup>
|
|
27
37
|
<thead>
|
|
28
38
|
<!-- @vue-ignore -->
|
|
29
39
|
<slot name="thead">
|
|
@@ -74,11 +84,18 @@
|
|
|
74
84
|
</slot>
|
|
75
85
|
</th>
|
|
76
86
|
<th
|
|
77
|
-
v-for="header in mappedHeaders"
|
|
87
|
+
v-for="(header, index) in mappedHeaders"
|
|
78
88
|
:key="header.key"
|
|
79
89
|
:aria-sort="header.sortDirection"
|
|
80
90
|
class="bb-table-header"
|
|
81
|
-
:class="
|
|
91
|
+
:class="[
|
|
92
|
+
header.classes,
|
|
93
|
+
{
|
|
94
|
+
'bb-table-header--edge-start': index === 0,
|
|
95
|
+
'bb-table-header--edge-end':
|
|
96
|
+
index === mappedHeaders.length - 1,
|
|
97
|
+
},
|
|
98
|
+
]"
|
|
82
99
|
scope="col"
|
|
83
100
|
>
|
|
84
101
|
<!-- @vue-ignore -->
|
|
@@ -297,10 +314,17 @@
|
|
|
297
314
|
</slot>
|
|
298
315
|
</td>
|
|
299
316
|
<td
|
|
300
|
-
v-for="col in item.cols"
|
|
317
|
+
v-for="(col, colIndex) in item.cols"
|
|
301
318
|
:key="col.key"
|
|
302
319
|
class="bb-table-data__cell"
|
|
303
|
-
:class="
|
|
320
|
+
:class="[
|
|
321
|
+
col.classes,
|
|
322
|
+
{
|
|
323
|
+
'bb-table-data__cell--edge-start': colIndex === 0,
|
|
324
|
+
'bb-table-data__cell--edge-end':
|
|
325
|
+
colIndex === item.cols.length - 1,
|
|
326
|
+
},
|
|
327
|
+
]"
|
|
304
328
|
>
|
|
305
329
|
<!-- @vue-ignore -->
|
|
306
330
|
<slot
|
|
@@ -373,7 +397,16 @@
|
|
|
373
397
|
lang="ts"
|
|
374
398
|
generic="Item = any, T extends ItemKey<Item> = string & {}"
|
|
375
399
|
>
|
|
376
|
-
import {
|
|
400
|
+
import {
|
|
401
|
+
computed,
|
|
402
|
+
nextTick,
|
|
403
|
+
onBeforeUnmount,
|
|
404
|
+
onMounted,
|
|
405
|
+
reactive,
|
|
406
|
+
ref,
|
|
407
|
+
toRef,
|
|
408
|
+
watch,
|
|
409
|
+
} from 'vue';
|
|
377
410
|
import { clamp } from '@/utilities/functions/clamp';
|
|
378
411
|
import { hash } from '@/utilities/functions/hash';
|
|
379
412
|
import { isNil } from '@/utilities/functions/isNil';
|
|
@@ -385,6 +418,7 @@ import { usePrefill } from '@/composables/usePrefill';
|
|
|
385
418
|
import { useIndexById } from '@/composables/useIndexById';
|
|
386
419
|
import { useBaseOptions } from '@/composables/useBaseOptions';
|
|
387
420
|
import { useHashedWatcher } from '@/composables/useHashedWatcher';
|
|
421
|
+
import { useTableWidthContext } from '@/composables/useTableWidthContext';
|
|
388
422
|
import { useLocale } from '@/composables/useLocale';
|
|
389
423
|
import BaseCheckbox from '../BaseCheckbox/BaseCheckbox.vue';
|
|
390
424
|
import BaseRadio from '../BaseRadio/BaseRadio.vue';
|
|
@@ -437,6 +471,7 @@ const props = withDefaults(defineProps<InternalProps<T>>(), {
|
|
|
437
471
|
max: Infinity,
|
|
438
472
|
multiple: true,
|
|
439
473
|
modelValue: () => [],
|
|
474
|
+
inheritColumnWidths: true,
|
|
440
475
|
unselectedItems: () => [],
|
|
441
476
|
});
|
|
442
477
|
|
|
@@ -476,6 +511,709 @@ if (props.multiple && !Array.isArray(props.modelValue)) {
|
|
|
476
511
|
const { t } = useLocale();
|
|
477
512
|
|
|
478
513
|
const tableRef = ref<HTMLTableElement>();
|
|
514
|
+
const containerRef = ref<HTMLElement>();
|
|
515
|
+
|
|
516
|
+
const {
|
|
517
|
+
id: tableId,
|
|
518
|
+
parentId,
|
|
519
|
+
parentNode,
|
|
520
|
+
cssVars,
|
|
521
|
+
parentVar,
|
|
522
|
+
setTracks,
|
|
523
|
+
} = useTableWidthContext({
|
|
524
|
+
id: () => props.id,
|
|
525
|
+
container: () => containerRef.value,
|
|
526
|
+
inheritFrom: () =>
|
|
527
|
+
typeof props.inheritColumnWidths === 'string'
|
|
528
|
+
? props.inheritColumnWidths
|
|
529
|
+
: undefined,
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
const inheritColumnWidths = computed(() => props.inheritColumnWidths !== false);
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Assigned column widths (frozen or inherited) are only authoritative under
|
|
536
|
+
* `table-layout: fixed`; in auto layout they behave like a minimum. So a table
|
|
537
|
+
* that drives widths — explicitly `fixed`, inheriting from a parent, or with a
|
|
538
|
+
* frozen column — uses fixed layout even when `fixed` was not set.
|
|
539
|
+
*/
|
|
540
|
+
const fixedLayout = computed(
|
|
541
|
+
() =>
|
|
542
|
+
props.fixed ||
|
|
543
|
+
props.columns.some((column) => column.width != null) ||
|
|
544
|
+
(inheritColumnWidths.value && !!parentId.value)
|
|
545
|
+
);
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* A nested table inheriting its parent's widths must size itself to the sum of
|
|
549
|
+
* those columns rather than stretching to fill its container — otherwise fixed
|
|
550
|
+
* layout would distribute the slack and the columns would no longer match the
|
|
551
|
+
* parent. Tables with the same column count already sum to the full width, so
|
|
552
|
+
* this is a no-op for them and only narrows tables with fewer columns.
|
|
553
|
+
*/
|
|
554
|
+
/** True for a nested table that inherits its column widths from a parent. */
|
|
555
|
+
const isInheriting = computed(
|
|
556
|
+
() => inheritColumnWidths.value && !!parentId.value
|
|
557
|
+
);
|
|
558
|
+
|
|
559
|
+
const sizeToColumns = isInheriting;
|
|
560
|
+
|
|
561
|
+
/** Numbers and bare numeric strings (e.g. `400`, `'400'`) default to px; values
|
|
562
|
+
* that already carry a unit/keyword (`'50%'`, `'10rem'`, `'auto'`) pass through. */
|
|
563
|
+
const toLength = (width: number | string) => {
|
|
564
|
+
if (typeof width === 'number') return `${width}px`;
|
|
565
|
+
const trimmed = width.trim();
|
|
566
|
+
return /^-?\d*\.?\d+$/.test(trimmed) ? `${trimmed}px` : trimmed;
|
|
567
|
+
};
|
|
568
|
+
|
|
569
|
+
/** Number of data columns the parent renders (numeric track keys). */
|
|
570
|
+
const parentDataColumns = computed(() => {
|
|
571
|
+
const tracks = parentNode.value?.tracks;
|
|
572
|
+
return tracks
|
|
573
|
+
? Object.keys(tracks).filter((key) => /^\d+$/.test(key)).length
|
|
574
|
+
: 0;
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* The parent's rendered columns in order — `select` (if any), data columns
|
|
579
|
+
* `0…N-1`, then `actions` (if any) — form a gridline coordinate, one cell per
|
|
580
|
+
* column. Cell `c` is the range `[c, c + 1]`. So a no-select parent starts its
|
|
581
|
+
* data at cell 0, while a selectable parent starts it at cell 1.
|
|
582
|
+
*/
|
|
583
|
+
const parentColumnKeys = computed(() => {
|
|
584
|
+
const tracks = parentNode.value?.tracks ?? {};
|
|
585
|
+
const keys: string[] = [];
|
|
586
|
+
if ('select' in tracks) keys.push('select');
|
|
587
|
+
for (let k = 0; k < parentDataColumns.value; k++) keys.push(String(k));
|
|
588
|
+
if ('actions' in tracks) keys.push('actions');
|
|
589
|
+
return keys;
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
/** Gridline cells where the parent's data region begins/ends and the table ends. */
|
|
593
|
+
const firstDataCell = computed(() =>
|
|
594
|
+
'select' in (parentNode.value?.tracks ?? {}) ? 1 : 0
|
|
595
|
+
);
|
|
596
|
+
const dataEndCell = computed(
|
|
597
|
+
() => firstDataCell.value + parentDataColumns.value
|
|
598
|
+
);
|
|
599
|
+
const tableEndCell = computed(() => parentColumnKeys.value.length);
|
|
600
|
+
|
|
601
|
+
/** Parent track custom property for the integer cell `[cell, cell + 1]`. */
|
|
602
|
+
const cellTrackVar = (cell: number) => {
|
|
603
|
+
const key = parentColumnKeys.value[cell];
|
|
604
|
+
return key ? parentVar(key, '0px') : '0px';
|
|
605
|
+
};
|
|
606
|
+
|
|
607
|
+
/** CSS length summing the parent tracks across the gridline range `[a, b]`. */
|
|
608
|
+
const gridRangeWidth = (a: number, b: number): string | null => {
|
|
609
|
+
if (!parentId.value || b - a <= 1e-6) return null;
|
|
610
|
+
const parts: string[] = [];
|
|
611
|
+
for (let cell = Math.floor(a); cell < Math.ceil(b - 1e-6); cell++) {
|
|
612
|
+
const fraction = Math.min(b, cell + 1) - Math.max(a, cell);
|
|
613
|
+
if (fraction <= 1e-6) continue;
|
|
614
|
+
const track = cellTrackVar(cell);
|
|
615
|
+
parts.push(
|
|
616
|
+
fraction >= 1 - 1e-6 ? track : `${Number(fraction.toFixed(4))} * ${track}`
|
|
617
|
+
);
|
|
618
|
+
}
|
|
619
|
+
if (!parts.length) return null;
|
|
620
|
+
// A bare `var()` can stand alone, but a single fractional term still needs a
|
|
621
|
+
// `calc()` wrapper for the multiplication to be valid CSS.
|
|
622
|
+
if (parts.length === 1 && !parts[0].includes('*')) return parts[0];
|
|
623
|
+
return `calc(${parts.join(' + ')})`;
|
|
624
|
+
};
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* Resolved parent snap interval for each data column.
|
|
628
|
+
* - A number snaps to that start point; the column then runs up to the next
|
|
629
|
+
* column's snap point (or one whole track if the next is unset). So a single
|
|
630
|
+
* snap that skips ahead widens this column to cover the gap — `snap: 4` on the
|
|
631
|
+
* second column makes the first effectively `[start, 4]`.
|
|
632
|
+
* - A `[start, end]` pair is taken verbatim (`end` of `-1` runs to the end of
|
|
633
|
+
* the data region).
|
|
634
|
+
* - Omitted, the column chains from the previous column's end.
|
|
635
|
+
*/
|
|
636
|
+
const columnIntervals = computed(() => {
|
|
637
|
+
const cols = props.columns;
|
|
638
|
+
// Explicit start points; a later column's snap sets the previous column's end.
|
|
639
|
+
const starts = cols.map((column) =>
|
|
640
|
+
typeof column.snap === 'number'
|
|
641
|
+
? column.snap
|
|
642
|
+
: Array.isArray(column.snap)
|
|
643
|
+
? column.snap[0]
|
|
644
|
+
: null
|
|
645
|
+
);
|
|
646
|
+
let cursor = firstDataCell.value;
|
|
647
|
+
return cols.map((column, i) => {
|
|
648
|
+
const start = starts[i] ?? cursor;
|
|
649
|
+
let end: number;
|
|
650
|
+
if (Array.isArray(column.snap)) {
|
|
651
|
+
end = column.snap[1] === -1 ? dataEndCell.value : column.snap[1];
|
|
652
|
+
} else {
|
|
653
|
+
const next = starts[i + 1];
|
|
654
|
+
end = next != null && next > start ? next : Math.floor(start) + 1;
|
|
655
|
+
}
|
|
656
|
+
cursor = end;
|
|
657
|
+
return { start, end };
|
|
658
|
+
});
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* A column whose interval begins at or beyond the parent's last data cell has no
|
|
663
|
+
* matching parent column to inherit from — the table has more columns than its
|
|
664
|
+
* parent. Such columns are sized to their own measured content and scroll within
|
|
665
|
+
* the expand row instead of inheriting a track.
|
|
666
|
+
*/
|
|
667
|
+
const isOverflowColumn = (interval: { start: number }) =>
|
|
668
|
+
isInheriting.value && interval.start >= dataEndCell.value;
|
|
669
|
+
|
|
670
|
+
/** True when any column overflows the parent (more columns than the parent). */
|
|
671
|
+
const overflowsParent = computed(() =>
|
|
672
|
+
columnIntervals.value.some(isOverflowColumn)
|
|
673
|
+
);
|
|
674
|
+
|
|
675
|
+
/**
|
|
676
|
+
* A "flex" column shares a root table's leftover space (min-content floor)
|
|
677
|
+
* proportionally via the probe. Only root tables flex this way; an inheriting
|
|
678
|
+
* table instead snaps its in-range columns to the parent, content-sizes its
|
|
679
|
+
* overflow columns, and lets a single fill column take up any slack.
|
|
680
|
+
*/
|
|
681
|
+
const isFlexColumn = (index: number) =>
|
|
682
|
+
props.columns[index]?.width == null && !isInheriting.value;
|
|
683
|
+
|
|
684
|
+
/**
|
|
685
|
+
* The last column of an inheriting table whose width is left undefined — neither
|
|
686
|
+
* a `width` nor a `snap` (a `snap` *is* a width definition: the track range it
|
|
687
|
+
* spans). That column absorbs the leftover space so the table fills its parent
|
|
688
|
+
* when a frozen/snapped column narrower than the parent's would otherwise leave a
|
|
689
|
+
* gap. `-1` for a root table, or when every column defines its own width.
|
|
690
|
+
*/
|
|
691
|
+
const fillColumnIndex = computed(() => {
|
|
692
|
+
if (!isInheriting.value) return -1;
|
|
693
|
+
for (let i = props.columns.length - 1; i >= 0; i--) {
|
|
694
|
+
const column = props.columns[i];
|
|
695
|
+
if (column.width == null && column.snap == null) return i;
|
|
696
|
+
}
|
|
697
|
+
return -1;
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
// Parent columns left of the first column (e.g. a select this table doesn't
|
|
701
|
+
// render, or columns skipped by the first snap) and those right of the last are
|
|
702
|
+
// folded into the edge columns as the structural offset, keeping the table
|
|
703
|
+
// aligned to the parent. `offsetStartValue` becomes the first column's padding.
|
|
704
|
+
const offsetStartValue = computed(() => {
|
|
705
|
+
const first = columnIntervals.value[0];
|
|
706
|
+
// No parent to align to (a root table) means no offset at all — without this
|
|
707
|
+
// the structural reclaim below would still fold a negative `-ownSelect` in.
|
|
708
|
+
if (!inheritColumnWidths.value || !parentId.value || !first) return null;
|
|
709
|
+
// The fill runs from the parent's left edge to this table's first data
|
|
710
|
+
// column — the parent's select cell, if any, plus any skipped data columns.
|
|
711
|
+
const head = gridRangeWidth(0, first.start);
|
|
712
|
+
// This table's own select column sits flush at the left edge and takes its
|
|
713
|
+
// measured width, so reclaim it from the fill (mirror of the actions side):
|
|
714
|
+
// the first data column then lines the *next* column up flush with the
|
|
715
|
+
// parent. When the parent has a select cell it is part of `head`, so only the
|
|
716
|
+
// surplus over it is reclaimed; when the parent has none the whole select
|
|
717
|
+
// width comes out of the first column, which shrinks to make room for it.
|
|
718
|
+
const ownSelect = props.selectable ? structuralFit.select : null;
|
|
719
|
+
if (!ownSelect) return head;
|
|
720
|
+
return `calc(${head ?? '0px'} - ${ownSelect})`;
|
|
721
|
+
});
|
|
722
|
+
const offsetEndValue = computed(() => {
|
|
723
|
+
const intervals = columnIntervals.value;
|
|
724
|
+
const last = intervals[intervals.length - 1];
|
|
725
|
+
// No parent to align to (a root table) means no offset at all — without this
|
|
726
|
+
// the structural reclaim below would still fold a negative `-ownActions` in.
|
|
727
|
+
if (!inheritColumnWidths.value || !parentId.value || !last) return null;
|
|
728
|
+
// With more columns than the parent, the table runs past the parent's right
|
|
729
|
+
// edge: there is no trailing parent region to fold, and the last column is a
|
|
730
|
+
// content-sized overflow column, so there is no end fill.
|
|
731
|
+
if (overflowsParent.value) return null;
|
|
732
|
+
// When the last column is the (auto) fill column it already absorbs the whole
|
|
733
|
+
// trailing region via fixed layout, so folding the offset in too would double
|
|
734
|
+
// it — and skew its right-aligned content inward.
|
|
735
|
+
if (fillColumnIndex.value === props.columns.length - 1) return null;
|
|
736
|
+
// The fill runs from the last data column's end all the way to the parent's
|
|
737
|
+
// right edge — any unmapped data columns plus the parent's actions cell, if
|
|
738
|
+
// it has one.
|
|
739
|
+
const tail = gridRangeWidth(last.end, tableEndCell.value);
|
|
740
|
+
// This table's own actions column sits flush against that right edge and
|
|
741
|
+
// takes its measured content width, so reclaim it from the fill: the table
|
|
742
|
+
// then ends exactly at the parent's edge instead of overflowing by the
|
|
743
|
+
// actions width. When the parent has an actions cell it is part of `tail`, so
|
|
744
|
+
// only the surplus over it is effectively reclaimed; when the parent has none
|
|
745
|
+
// the whole actions width comes out of the data fill. The result may be
|
|
746
|
+
// negative — the last data column then shrinks to make room for the actions.
|
|
747
|
+
const ownActions = props.actions ? structuralFit.actions : null;
|
|
748
|
+
if (!ownActions) return tail;
|
|
749
|
+
return `calc(${tail ?? '0px'} - ${ownActions})`;
|
|
750
|
+
});
|
|
751
|
+
const offsetStartActive = computed(() => offsetStartValue.value !== null);
|
|
752
|
+
const offsetEndActive = computed(() => offsetEndValue.value !== null);
|
|
753
|
+
|
|
754
|
+
/**
|
|
755
|
+
* Measured intrinsic content widths of the structural columns. In a fixed table
|
|
756
|
+
* these pin `select`/`actions` to their content so the remaining space is shared
|
|
757
|
+
* among the data columns; `null` falls back to the regular auto behavior.
|
|
758
|
+
*/
|
|
759
|
+
const structuralFit = reactive<{
|
|
760
|
+
select: string | null;
|
|
761
|
+
actions: string | null;
|
|
762
|
+
}>({ select: null, actions: null });
|
|
763
|
+
|
|
764
|
+
/**
|
|
765
|
+
* Rendered columns in DOM order, each with the track key it publishes/inherits
|
|
766
|
+
* and the width to apply via `<colgroup>`. A frozen column width wins; otherwise
|
|
767
|
+
* an unfrozen column syncs to the parent's matching track (or `auto` at root).
|
|
768
|
+
* The first/last column additionally absorbs the structural edge offset.
|
|
769
|
+
*/
|
|
770
|
+
const columnTracks = computed(() => {
|
|
771
|
+
const sync = (key: string) =>
|
|
772
|
+
inheritColumnWidths.value ? parentVar(key) : 'auto';
|
|
773
|
+
// In fixed layout `auto` columns share width equally, so pin the structural
|
|
774
|
+
// columns to their measured content width and let the data columns share.
|
|
775
|
+
const structural = (key: 'select' | 'actions') =>
|
|
776
|
+
fixedLayout.value && structuralFit[key]
|
|
777
|
+
? (structuralFit[key] as string)
|
|
778
|
+
: sync(key);
|
|
779
|
+
const tracks: { key: string; width: string }[] = [];
|
|
780
|
+
|
|
781
|
+
if (props.selectable) {
|
|
782
|
+
tracks.push({ key: 'select', width: structural('select') });
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
const lastIndex = props.columns.length - 1;
|
|
786
|
+
props.columns.forEach((column, index) => {
|
|
787
|
+
const key = String(index);
|
|
788
|
+
const frozen = column.width != null ? toLength(column.width) : null;
|
|
789
|
+
const interval = columnIntervals.value[index];
|
|
790
|
+
|
|
791
|
+
// The fill column is left `auto` so fixed layout (the inheriting table has
|
|
792
|
+
// a definite `width: 100%`) hands it the leftover space — its own inherited
|
|
793
|
+
// track plus the folded edge region plus any deficit. Skipped when the
|
|
794
|
+
// table overflows: there is no leftover then, and it must content-size.
|
|
795
|
+
if (index === fillColumnIndex.value && !overflowsParent.value) {
|
|
796
|
+
tracks.push({ key, width: 'auto' });
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
let span: string | null;
|
|
801
|
+
if (frozen != null) {
|
|
802
|
+
span = frozen;
|
|
803
|
+
} else if (isFlexColumn(index)) {
|
|
804
|
+
// Root table auto column: a precise share of the leftover space from the
|
|
805
|
+
// proportional probe rather than fixed layout's equal split. `auto` is
|
|
806
|
+
// the fallback before the probe runs.
|
|
807
|
+
span = (fixedLayout.value && probeColWidths.value[index]) || null;
|
|
808
|
+
} else if (isOverflowColumn(interval)) {
|
|
809
|
+
// Inheriting table, past the parent's last column: no track to snap to,
|
|
810
|
+
// so size to its own content and let the table overflow (and scroll).
|
|
811
|
+
span = columnMinWidths.value[index] ?? null;
|
|
812
|
+
} else {
|
|
813
|
+
// In range: snap to the parent's matching column.
|
|
814
|
+
span = gridRangeWidth(interval.start, interval.end);
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
const offsets: string[] = [];
|
|
818
|
+
if (index === 0 && offsetStartActive.value) {
|
|
819
|
+
offsets.push('var(--bb-table-offset-internal-start)');
|
|
820
|
+
}
|
|
821
|
+
if (index === lastIndex && offsetEndActive.value) {
|
|
822
|
+
offsets.push('var(--bb-table-offset-internal-end)');
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
// When folding an offset, use a calc-safe length base (no `auto`) so the
|
|
826
|
+
// column width = offset(s) + the column's own span.
|
|
827
|
+
const width = offsets.length
|
|
828
|
+
? `calc(${[...offsets, span ?? '0px'].join(' + ')})`
|
|
829
|
+
: (span ?? 'auto');
|
|
830
|
+
|
|
831
|
+
tracks.push({ key, width });
|
|
832
|
+
});
|
|
833
|
+
|
|
834
|
+
if (props.actions) {
|
|
835
|
+
tracks.push({ key: 'actions', width: structural('actions') });
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
return tracks;
|
|
839
|
+
});
|
|
840
|
+
|
|
841
|
+
/**
|
|
842
|
+
* Standalone edge-offset custom properties consumed by the first/last cell
|
|
843
|
+
* padding and the first/last column width. Set only when an offset is active;
|
|
844
|
+
* otherwise the CSS default of `0px` makes both calcs collapse to their base.
|
|
845
|
+
*/
|
|
846
|
+
const structuralOffset = computed<Record<string, string>>(() => {
|
|
847
|
+
const style: Record<string, string> = {};
|
|
848
|
+
if (offsetStartValue.value) {
|
|
849
|
+
style['--bb-table-offset-start'] = offsetStartValue.value;
|
|
850
|
+
}
|
|
851
|
+
if (offsetEndValue.value) {
|
|
852
|
+
style['--bb-table-offset-end'] = offsetEndValue.value;
|
|
853
|
+
}
|
|
854
|
+
return style;
|
|
855
|
+
});
|
|
856
|
+
|
|
857
|
+
const containerStyle = computed(() => {
|
|
858
|
+
const vars: Record<string, string> = {
|
|
859
|
+
...cssVars.value,
|
|
860
|
+
...structuralOffset.value,
|
|
861
|
+
};
|
|
862
|
+
|
|
863
|
+
if (naturalTotalWidth.value) {
|
|
864
|
+
vars['--bb-table-natural-width'] = naturalTotalWidth.value;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
if (fillValue.value > 0) {
|
|
868
|
+
vars['--bb-table-fill'] = `${fillValue.value}px`;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
return vars;
|
|
872
|
+
});
|
|
873
|
+
|
|
874
|
+
/** Reads the header cells in render order: [select?, ...data, actions?]. */
|
|
875
|
+
const headerCells = () =>
|
|
876
|
+
Array.from(
|
|
877
|
+
tableRef.value?.querySelectorAll<HTMLElement>(
|
|
878
|
+
':scope > thead > tr.bb-table-header-row > th'
|
|
879
|
+
) ?? []
|
|
880
|
+
);
|
|
881
|
+
|
|
882
|
+
/** Measures the rendered columns and publishes them as this table's tracks. */
|
|
883
|
+
const measureTracks = () => {
|
|
884
|
+
const keys = columnTracks.value.map((track) => track.key);
|
|
885
|
+
const tracks: Record<string, string> = {};
|
|
886
|
+
headerCells().forEach((cell, index) => {
|
|
887
|
+
const key = keys[index];
|
|
888
|
+
if (key != null) tracks[key] = `${cell.getBoundingClientRect().width}px`;
|
|
889
|
+
});
|
|
890
|
+
setTracks(tracks);
|
|
891
|
+
};
|
|
892
|
+
|
|
893
|
+
/**
|
|
894
|
+
* Measures the intrinsic content width of the structural columns across header
|
|
895
|
+
* and body. Fixed layout sizes columns from the header row only, so the body is
|
|
896
|
+
* required here: the actions header is `sr-only`, while the real control lives
|
|
897
|
+
* in the body. The measured child width is independent of the column width, so
|
|
898
|
+
* pinning the column to it converges instead of oscillating.
|
|
899
|
+
*/
|
|
900
|
+
const measureStructuralFit = () => {
|
|
901
|
+
const table = tableRef.value;
|
|
902
|
+
if (!table) return;
|
|
903
|
+
|
|
904
|
+
const fit = (selector: string) => {
|
|
905
|
+
let max = 0;
|
|
906
|
+
table.querySelectorAll<HTMLElement>(selector).forEach((cell) => {
|
|
907
|
+
// Combined horizontal extent of the children, not the widest one: an
|
|
908
|
+
// actions cell holds several side-by-side controls whose total width is
|
|
909
|
+
// what the column must fit. Measured from edges so it stays correct even
|
|
910
|
+
// when the cell is narrower than its content (controls overflow on
|
|
911
|
+
// `nowrap`), which keeps the pin-to-content convergence stable.
|
|
912
|
+
let left = Infinity;
|
|
913
|
+
let right = -Infinity;
|
|
914
|
+
cell.querySelectorAll<HTMLElement>(':scope > *').forEach((child) => {
|
|
915
|
+
const rect = child.getBoundingClientRect();
|
|
916
|
+
left = Math.min(left, rect.left);
|
|
917
|
+
right = Math.max(right, rect.right);
|
|
918
|
+
});
|
|
919
|
+
const content = right > left ? right - left : 0;
|
|
920
|
+
const style = getComputedStyle(cell);
|
|
921
|
+
const padding =
|
|
922
|
+
parseFloat(style.paddingLeft) + parseFloat(style.paddingRight);
|
|
923
|
+
max = Math.max(max, content + padding);
|
|
924
|
+
});
|
|
925
|
+
return max > 0 ? `${Math.ceil(max)}px` : null;
|
|
926
|
+
};
|
|
927
|
+
|
|
928
|
+
// Scoped to this table's own header/body rows: an unscoped descendant query
|
|
929
|
+
// would also match the structural cells of a nested table living in an expand
|
|
930
|
+
// row, letting a child's actions/select width hijack this table's column.
|
|
931
|
+
structuralFit.select = props.selectable
|
|
932
|
+
? fit(
|
|
933
|
+
':scope > thead > tr > th.bb-table-header--select, :scope > tbody > tr > td.bb-table-data__cell--select'
|
|
934
|
+
)
|
|
935
|
+
: null;
|
|
936
|
+
structuralFit.actions = props.actions
|
|
937
|
+
? fit(
|
|
938
|
+
':scope > thead > tr > th.bb-table-header--actions, :scope > tbody > tr > td.bb-table__cell--actions'
|
|
939
|
+
)
|
|
940
|
+
: null;
|
|
941
|
+
};
|
|
942
|
+
|
|
943
|
+
const measure = () => {
|
|
944
|
+
measureStructuralFit();
|
|
945
|
+
measureTracks();
|
|
946
|
+
};
|
|
947
|
+
|
|
948
|
+
/**
|
|
949
|
+
* Proportional `<col>` widths derived from an auto-layout probe, one entry per
|
|
950
|
+
* data column (`null` for frozen columns — they keep their declared width).
|
|
951
|
+
*
|
|
952
|
+
* For each auto column the formula is:
|
|
953
|
+
* width = max(minimum, available × (minimum / minimumTotal))
|
|
954
|
+
*
|
|
955
|
+
* where `minimum` is the column's `min-content` width (what it shrinks to in a
|
|
956
|
+
* very narrow container, i.e. the floor below which content would be clipped),
|
|
957
|
+
* `available` = containerWidth − frozenColumnWidths − structuralColumnWidths
|
|
958
|
+
* and `minimumTotal` = sum of the auto columns' minimums.
|
|
959
|
+
*
|
|
960
|
+
* So when the container has room (`available ≥ minimumTotal`) the slack is shared
|
|
961
|
+
* out proportionally to each column's minimum, scaling them up together; when it
|
|
962
|
+
* does not, every column drops to its minimum and the table-level `min-width`
|
|
963
|
+
* lets it overflow (the container scrolls) rather than squashing content further.
|
|
964
|
+
*
|
|
965
|
+
* Re-measured after any DOM mutation (debounced 200 ms) and after any container
|
|
966
|
+
* resize (via a dedicated ResizeObserver on the container element).
|
|
967
|
+
*/
|
|
968
|
+
const probeColWidths = ref<(string | null)[]>([]);
|
|
969
|
+
|
|
970
|
+
/**
|
|
971
|
+
* Each unfrozen column's measured `min-content` width (`null` for frozen). Used
|
|
972
|
+
* to content-size the overflow columns of an inheriting table — those past the
|
|
973
|
+
* parent's last column, with no track to snap to.
|
|
974
|
+
*/
|
|
975
|
+
const columnMinWidths = ref<(string | null)[]>([]);
|
|
976
|
+
|
|
977
|
+
/**
|
|
978
|
+
* Leftover px the inheriting fill column adds so the table reaches its parent's
|
|
979
|
+
* width. Published as `--bb-table-fill`; `0` when the table already fills or
|
|
980
|
+
* overflows. Derived self-correctingly from the live total (see the probe).
|
|
981
|
+
*/
|
|
982
|
+
const fillValue = ref(0);
|
|
983
|
+
|
|
984
|
+
/**
|
|
985
|
+
* Total minimum width of the table (all columns at their `min-content`, plus the
|
|
986
|
+
* structural columns). Published as `--bb-table-natural-width` and used as
|
|
987
|
+
* `min-width` on the `<table>` so the table scrolls horizontally only once the
|
|
988
|
+
* container is narrower than this true minimum.
|
|
989
|
+
*/
|
|
990
|
+
const naturalTotalWidth = ref<string | null>(null);
|
|
991
|
+
|
|
992
|
+
const runNaturalWidthProbe = () => {
|
|
993
|
+
const container = containerRef.value;
|
|
994
|
+
if (!container || !fixedLayout.value) return;
|
|
995
|
+
// Runs for a frozen-column table (to share space), or any inheriting table
|
|
996
|
+
// (to content-size overflow columns and drive the fill column's slack).
|
|
997
|
+
if (
|
|
998
|
+
!props.columns.some((col) => col.width != null) &&
|
|
999
|
+
!overflowsParent.value &&
|
|
1000
|
+
fillColumnIndex.value < 0
|
|
1001
|
+
)
|
|
1002
|
+
return;
|
|
1003
|
+
|
|
1004
|
+
const containerWidth = container.offsetWidth;
|
|
1005
|
+
const dataOffset = props.selectable ? 1 : 0;
|
|
1006
|
+
|
|
1007
|
+
// ── Real-table measurements ────────────────────────────────────────────
|
|
1008
|
+
// Structural column widths come from measureStructuralFit (already run).
|
|
1009
|
+
const selectWidth = structuralFit.select
|
|
1010
|
+
? parseFloat(structuralFit.select)
|
|
1011
|
+
: 0;
|
|
1012
|
+
const actionsWidth = structuralFit.actions
|
|
1013
|
+
? parseFloat(structuralFit.actions)
|
|
1014
|
+
: 0;
|
|
1015
|
+
|
|
1016
|
+
// Live rendered widths from the fixed-layout table. `frozenTotal` (declared
|
|
1017
|
+
// widths) feeds the stable min-width floor; `nonFlexTotal` (frozen + inherited)
|
|
1018
|
+
// is what the flex columns share the remainder of. The inherited part is only
|
|
1019
|
+
// used for that `available` figure — never fed back into min-width, since those
|
|
1020
|
+
// widths stretch with the table and would otherwise lock min-width above
|
|
1021
|
+
// max-content and force a spurious uniform stretch on every column.
|
|
1022
|
+
const realThs = headerCells();
|
|
1023
|
+
let frozenTotal = 0;
|
|
1024
|
+
let nonFlexTotal = 0;
|
|
1025
|
+
props.columns.forEach((col, i) => {
|
|
1026
|
+
const width = realThs[dataOffset + i]?.getBoundingClientRect().width ?? 0;
|
|
1027
|
+
if (col.width != null) frozenTotal += width;
|
|
1028
|
+
if (!isFlexColumn(i)) nonFlexTotal += width;
|
|
1029
|
+
});
|
|
1030
|
+
|
|
1031
|
+
// ── Offscreen probe for the auto columns' minimum (min-content) widths ──
|
|
1032
|
+
// The clone's inner table is forced to `width: min-content` and the auto <col>
|
|
1033
|
+
// hints are stripped so each <th> reports the narrowest width it can take
|
|
1034
|
+
// before content would be clipped, independent of the container size or
|
|
1035
|
+
// previous probe cycles.
|
|
1036
|
+
const probe = document.createElement('div');
|
|
1037
|
+
probe.style.cssText = [
|
|
1038
|
+
'position:absolute',
|
|
1039
|
+
'top:-9999px',
|
|
1040
|
+
'left:-9999px',
|
|
1041
|
+
'visibility:hidden',
|
|
1042
|
+
'pointer-events:none',
|
|
1043
|
+
`width:${containerWidth}px`,
|
|
1044
|
+
`height:${container.offsetHeight}px`,
|
|
1045
|
+
'overflow:visible',
|
|
1046
|
+
].join(';');
|
|
1047
|
+
|
|
1048
|
+
const clone = container.cloneNode(true) as HTMLElement;
|
|
1049
|
+
clone.classList.remove('bb-table--fixed');
|
|
1050
|
+
|
|
1051
|
+
// Expand rows are full-width (colspan) and hold nested tables; under the
|
|
1052
|
+
// auto-layout probe their content would back-pressure this table's own
|
|
1053
|
+
// columns, so drop them and measure this table's columns in isolation.
|
|
1054
|
+
clone
|
|
1055
|
+
.querySelectorAll('.bb-table-expand__row')
|
|
1056
|
+
.forEach((row) => row.remove());
|
|
1057
|
+
|
|
1058
|
+
// Auto <col> hints are cleared so a previous cycle's computed width cannot
|
|
1059
|
+
// act as a minimum and inflate the measurement; frozen hints are left intact
|
|
1060
|
+
// since their declared width is a genuine floor for that column.
|
|
1061
|
+
const probeColEls = Array.from(clone.querySelectorAll<HTMLElement>('col'));
|
|
1062
|
+
props.columns.forEach((col, i) => {
|
|
1063
|
+
if (col.width == null) {
|
|
1064
|
+
const probeCol = probeColEls[dataOffset + i];
|
|
1065
|
+
if (probeCol) probeCol.style.width = '';
|
|
1066
|
+
}
|
|
1067
|
+
});
|
|
1068
|
+
|
|
1069
|
+
const innerTable = clone.querySelector<HTMLElement>('table');
|
|
1070
|
+
if (innerTable) {
|
|
1071
|
+
innerTable.style.width = 'min-content';
|
|
1072
|
+
innerTable.style.minWidth = '0';
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
probe.appendChild(clone);
|
|
1076
|
+
document.body.appendChild(probe);
|
|
1077
|
+
|
|
1078
|
+
const headerRow = probe.querySelector<HTMLElement>(
|
|
1079
|
+
'table thead tr.bb-table-header-row'
|
|
1080
|
+
);
|
|
1081
|
+
const ths = headerRow
|
|
1082
|
+
? Array.from(headerRow.querySelectorAll<HTMLElement>(':scope > th'))
|
|
1083
|
+
: [];
|
|
1084
|
+
|
|
1085
|
+
// Min-content of every unfrozen column (0 for frozen). Independent of the live
|
|
1086
|
+
// table's width, so it is a stable basis for both the flex share and the
|
|
1087
|
+
// min-width floor — unlike the rendered inherited widths.
|
|
1088
|
+
const minContents = props.columns.map((col, i) => {
|
|
1089
|
+
if (col.width != null) return 0;
|
|
1090
|
+
const th = ths[dataOffset + i];
|
|
1091
|
+
return th ? th.getBoundingClientRect().width : 0;
|
|
1092
|
+
});
|
|
1093
|
+
|
|
1094
|
+
document.body.removeChild(probe);
|
|
1095
|
+
|
|
1096
|
+
// ── Proportional <col> widths ──────────────────────────────────────────
|
|
1097
|
+
// Only the flex columns share `available`; the inherited columns are already
|
|
1098
|
+
// laid out and excluded via `nonFlexTotal`.
|
|
1099
|
+
const flexMinTotal = props.columns.reduce(
|
|
1100
|
+
(s, _, i) => s + (isFlexColumn(i) ? minContents[i] : 0),
|
|
1101
|
+
0
|
|
1102
|
+
);
|
|
1103
|
+
const available = containerWidth - nonFlexTotal - selectWidth - actionsWidth;
|
|
1104
|
+
|
|
1105
|
+
// Share the available space proportionally to each flex column's minimum, but
|
|
1106
|
+
// never drop below that minimum — when space runs out the table overflows
|
|
1107
|
+
// (and the container scrolls) instead of clipping content.
|
|
1108
|
+
//
|
|
1109
|
+
// Widths are kept sub-pixel (not rounded up): the raw shares sum to exactly
|
|
1110
|
+
// `available`, whereas ceiling each column would over-allocate by up to a
|
|
1111
|
+
// pixel apiece and, under `table-layout: fixed`, push the table a few px past
|
|
1112
|
+
// the container and trip the horizontal scrollbar. `toFixed` only trims the
|
|
1113
|
+
// float tail, well below the 1px a scrollbar needs.
|
|
1114
|
+
const px = (n: number) => `${Number(n.toFixed(3))}px`;
|
|
1115
|
+
probeColWidths.value = props.columns.map((_, i) => {
|
|
1116
|
+
if (!isFlexColumn(i)) return null;
|
|
1117
|
+
const minimum = minContents[i];
|
|
1118
|
+
if (flexMinTotal <= 0) return px(minimum);
|
|
1119
|
+
const share = available > 0 ? available * (minimum / flexMinTotal) : 0;
|
|
1120
|
+
return px(Math.max(minimum, share));
|
|
1121
|
+
});
|
|
1122
|
+
|
|
1123
|
+
// Min-content per unfrozen column, for content-sizing an inheriting table's
|
|
1124
|
+
// overflow columns (no parent track to snap to).
|
|
1125
|
+
columnMinWidths.value = minContents.map((w) => (w > 0 ? px(w) : null));
|
|
1126
|
+
|
|
1127
|
+
// Leftover the inheriting fill column must absorb to reach the parent's width.
|
|
1128
|
+
// `parentTotal` is the parent table's full width (the expand cell this table
|
|
1129
|
+
// sits in); the container itself shrink-wraps to the table, so it can't be the
|
|
1130
|
+
// reference. Self-correcting: the live total already includes last cycle's
|
|
1131
|
+
// fill, so adding it back recovers the intrinsic total and the result is the
|
|
1132
|
+
// same once the table fills (0 when it already fills or overflows).
|
|
1133
|
+
if (fillColumnIndex.value >= 0) {
|
|
1134
|
+
const parentTotal = Object.values(parentNode.value?.tracks ?? {}).reduce(
|
|
1135
|
+
(s, w) => s + (parseFloat(w) || 0),
|
|
1136
|
+
0
|
|
1137
|
+
);
|
|
1138
|
+
const liveTotal = realThs.reduce(
|
|
1139
|
+
(s, th) => s + th.getBoundingClientRect().width,
|
|
1140
|
+
0
|
|
1141
|
+
);
|
|
1142
|
+
fillValue.value = Math.max(0, parentTotal - liveTotal + fillValue.value);
|
|
1143
|
+
} else {
|
|
1144
|
+
fillValue.value = 0;
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
// Table min-width = frozen (declared) widths + every unfrozen column's
|
|
1148
|
+
// min-content + structural. Built only from values that do not depend on the
|
|
1149
|
+
// table's own width, so it can never exceed max-content and force a stretch.
|
|
1150
|
+
const minContentTotal = minContents.reduce((s, w) => s + w, 0);
|
|
1151
|
+
const naturalTotal =
|
|
1152
|
+
frozenTotal + minContentTotal + selectWidth + actionsWidth;
|
|
1153
|
+
naturalTotalWidth.value =
|
|
1154
|
+
naturalTotal > 0 ? `${Math.ceil(naturalTotal)}px` : null;
|
|
1155
|
+
};
|
|
1156
|
+
|
|
1157
|
+
let naturalWidthDebounceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
1158
|
+
let mutationObserver: MutationObserver | undefined;
|
|
1159
|
+
|
|
1160
|
+
/** Invalidates the cached natural width and schedules a fresh probe. */
|
|
1161
|
+
const scheduleNaturalWidthProbe = () => {
|
|
1162
|
+
if (naturalWidthDebounceTimer !== null)
|
|
1163
|
+
clearTimeout(naturalWidthDebounceTimer);
|
|
1164
|
+
naturalWidthDebounceTimer = setTimeout(() => {
|
|
1165
|
+
naturalWidthDebounceTimer = null;
|
|
1166
|
+
runNaturalWidthProbe();
|
|
1167
|
+
}, 200);
|
|
1168
|
+
};
|
|
1169
|
+
|
|
1170
|
+
let resizeObserver: ResizeObserver | undefined;
|
|
1171
|
+
/** Observes the container element specifically for size changes so the
|
|
1172
|
+
* proportional <col> widths are recomputed whenever the container grows or
|
|
1173
|
+
* shrinks — separately from the header-cell observer that drives measureTracks. */
|
|
1174
|
+
let containerResizeObserver: ResizeObserver | undefined;
|
|
1175
|
+
|
|
1176
|
+
const observeColumns = () => {
|
|
1177
|
+
if (!resizeObserver) return;
|
|
1178
|
+
resizeObserver.disconnect();
|
|
1179
|
+
headerCells().forEach((cell) => resizeObserver?.observe(cell));
|
|
1180
|
+
measure();
|
|
1181
|
+
};
|
|
1182
|
+
|
|
1183
|
+
onMounted(() => {
|
|
1184
|
+
resizeObserver = new ResizeObserver(() => measure());
|
|
1185
|
+
observeColumns();
|
|
1186
|
+
|
|
1187
|
+
// Initial probe runs after the first render so all cells have content.
|
|
1188
|
+
nextTick(runNaturalWidthProbe);
|
|
1189
|
+
|
|
1190
|
+
// Invalidate the cache whenever rows or cell content change.
|
|
1191
|
+
if (tableRef.value) {
|
|
1192
|
+
mutationObserver = new MutationObserver(scheduleNaturalWidthProbe);
|
|
1193
|
+
mutationObserver.observe(tableRef.value, {
|
|
1194
|
+
childList: true,
|
|
1195
|
+
subtree: true,
|
|
1196
|
+
characterData: true,
|
|
1197
|
+
});
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
// Re-probe whenever the container itself is resized so that proportional
|
|
1201
|
+
// <col> widths stay accurate as the available space changes.
|
|
1202
|
+
if (containerRef.value) {
|
|
1203
|
+
containerResizeObserver = new ResizeObserver(scheduleNaturalWidthProbe);
|
|
1204
|
+
containerResizeObserver.observe(containerRef.value);
|
|
1205
|
+
}
|
|
1206
|
+
});
|
|
1207
|
+
|
|
1208
|
+
onBeforeUnmount(() => {
|
|
1209
|
+
resizeObserver?.disconnect();
|
|
1210
|
+
containerResizeObserver?.disconnect();
|
|
1211
|
+
mutationObserver?.disconnect();
|
|
1212
|
+
if (naturalWidthDebounceTimer !== null)
|
|
1213
|
+
clearTimeout(naturalWidthDebounceTimer);
|
|
1214
|
+
});
|
|
1215
|
+
|
|
1216
|
+
watch(columnTracks, () => nextTick(observeColumns), { flush: 'post' });
|
|
479
1217
|
|
|
480
1218
|
/** Builds accessible label for selection controls from row content. */
|
|
481
1219
|
const getAccessibleLabel = (columns: MappedCell[]) =>
|
|
@@ -493,11 +1231,13 @@ const containerClass = computed(() => ({
|
|
|
493
1231
|
'bb-table': true,
|
|
494
1232
|
[`bb-table--align-${props.align}`]: true,
|
|
495
1233
|
'bb-table--compact': props.compact,
|
|
496
|
-
'bb-table--fixed':
|
|
1234
|
+
'bb-table--fixed': fixedLayout.value,
|
|
1235
|
+
'bb-table--inherit-widths': sizeToColumns.value,
|
|
497
1236
|
'bb-table--fixed-header': props.fixedHeaders,
|
|
498
1237
|
'bb-table--loading': loading.value,
|
|
499
1238
|
'bb-table--empty': !options.value.length,
|
|
500
1239
|
'bb-table--selectable': !!props.selectable,
|
|
1240
|
+
'scrollbar-border': true,
|
|
501
1241
|
}));
|
|
502
1242
|
|
|
503
1243
|
const replacementContentSpan = computed(
|
|
@@ -611,6 +1351,7 @@ const mappedHeaders = computed(() =>
|
|
|
611
1351
|
? 'ascending'
|
|
612
1352
|
: 'descending'
|
|
613
1353
|
: undefined;
|
|
1354
|
+
|
|
614
1355
|
return {
|
|
615
1356
|
align,
|
|
616
1357
|
key: column.key,
|
|
@@ -919,6 +1660,30 @@ export type BbTableColumn<Item = any> = BaseColumn<Item> & {
|
|
|
919
1660
|
* Defines the classes to be passed to the `<th>`.
|
|
920
1661
|
*/
|
|
921
1662
|
thClass?: Classes;
|
|
1663
|
+
|
|
1664
|
+
/**
|
|
1665
|
+
* Freezes the column to a fixed width (numbers are treated as px). When
|
|
1666
|
+
* omitted the column syncs its width to the parent table's matching track.
|
|
1667
|
+
*/
|
|
1668
|
+
width?: number | string;
|
|
1669
|
+
|
|
1670
|
+
/**
|
|
1671
|
+
* For a nested table, where this column snaps onto the parent's grid. The
|
|
1672
|
+
* parent's columns form snap points where `select` occupies `[0, 1]`, data
|
|
1673
|
+
* column `k` occupies `[k + 1, k + 2]` and `actions` occupies `[N + 1, N + 2]`.
|
|
1674
|
+
*
|
|
1675
|
+
* - A single number is a start snap point that takes one parent track, e.g.
|
|
1676
|
+
* `2` → `[2, 3]`. It may skip ahead (`5` jumps straight to the sixth track),
|
|
1677
|
+
* and the next column resumes from where it ends.
|
|
1678
|
+
* - A `[start, end]` pair snaps between two points; the width is the sum of
|
|
1679
|
+
* the fractional parent tracks it covers, so `[3, 4.5]` is the full fourth
|
|
1680
|
+
* track plus half of the fifth. `end` of `-1` runs to the end of the data
|
|
1681
|
+
* region.
|
|
1682
|
+
*
|
|
1683
|
+
* When omitted the column chains from the previous column's end to the next
|
|
1684
|
+
* whole snap point (a one-to-one inheritance by index).
|
|
1685
|
+
*/
|
|
1686
|
+
snap?: number | [start: number, end: number];
|
|
922
1687
|
};
|
|
923
1688
|
|
|
924
1689
|
export type BbTableProps<Item = any> = {
|
|
@@ -943,6 +1708,24 @@ export type BbTableProps<Item = any> = {
|
|
|
943
1708
|
*/
|
|
944
1709
|
align?: 'left' | 'center' | 'right';
|
|
945
1710
|
|
|
1711
|
+
/**
|
|
1712
|
+
* Stable id for this table's width context. When omitted a unique id is
|
|
1713
|
+
* generated. Nested tables use the nearest ancestor id to inherit widths.
|
|
1714
|
+
*/
|
|
1715
|
+
id?: string;
|
|
1716
|
+
|
|
1717
|
+
/**
|
|
1718
|
+
* Controls whether unfrozen columns inherit their width from an ancestor
|
|
1719
|
+
* table's matching track, accounting for `select`/`actions` columns.
|
|
1720
|
+
* - `true` (the default): inherit from the nearest ancestor table.
|
|
1721
|
+
* - `false`: do not inherit.
|
|
1722
|
+
* - a table id (string): inherit from the specific ancestor in the chain with
|
|
1723
|
+
* that id rather than the immediate parent. Use this when an intermediate
|
|
1724
|
+
* table has a different column count, so the extra columns would otherwise
|
|
1725
|
+
* misalign against it.
|
|
1726
|
+
*/
|
|
1727
|
+
inheritColumnWidths?: boolean | string;
|
|
1728
|
+
|
|
946
1729
|
/**
|
|
947
1730
|
* Boolean that defines whether to display a "Select all" checkbox.
|
|
948
1731
|
*/
|
|
@@ -1291,10 +2074,32 @@ export type BbTableSlots<Item = any> = {
|
|
|
1291
2074
|
--padding-x: 16px;
|
|
1292
2075
|
--padding-y: 8px;
|
|
1293
2076
|
--actions-spacing: 8px;
|
|
2077
|
+
/* Raw structural edge offset measured by the component (the parent's
|
|
2078
|
+
select/actions track width). Default 0 keeps every calc at its base. */
|
|
2079
|
+
--bb-table-offset-start: 0px;
|
|
2080
|
+
--bb-table-offset-end: 0px;
|
|
2081
|
+
/* Routing of that offset. By default it is "internal": added to the
|
|
2082
|
+
first/last cell padding and folded into the first/last column width.
|
|
2083
|
+
Swap internal↔external (set internal to 0 and external to the raw offset)
|
|
2084
|
+
to pad the whole table instead of the edge cells. */
|
|
2085
|
+
--bb-table-offset-internal-start: var(--bb-table-offset-start);
|
|
2086
|
+
--bb-table-offset-internal-end: var(--bb-table-offset-end);
|
|
2087
|
+
--bb-table-offset-external-start: 0px;
|
|
2088
|
+
--bb-table-offset-external-end: 0px;
|
|
2089
|
+
/* Slack the fill column adds to reach the parent's width. Reset per table so a
|
|
2090
|
+
nested table never inherits an ancestor's value through the cascade. */
|
|
2091
|
+
--bb-table-fill: 0px;
|
|
1294
2092
|
border-collapse: separate;
|
|
1295
2093
|
border-spacing: 0;
|
|
1296
2094
|
display: grid;
|
|
2095
|
+
/* Override the UA `min-inline-size: min-content` that a <fieldset> container
|
|
2096
|
+
(rendered for selectable tables) carries — without this the container can't
|
|
2097
|
+
shrink below the table's content width, so it overflows its parent and
|
|
2098
|
+
pushes the page instead of scrolling inside its own `overflow-x: auto`. */
|
|
2099
|
+
min-width: 0;
|
|
1297
2100
|
overflow-x: auto;
|
|
2101
|
+
padding-inline-end: var(--bb-table-offset-external-end);
|
|
2102
|
+
padding-inline-start: var(--bb-table-offset-external-start);
|
|
1298
2103
|
|
|
1299
2104
|
position: relative;
|
|
1300
2105
|
|
|
@@ -1367,6 +2172,40 @@ export type BbTableSlots<Item = any> = {
|
|
|
1367
2172
|
min-width: 100%;
|
|
1368
2173
|
}
|
|
1369
2174
|
|
|
2175
|
+
/* A nested table inheriting its parent's widths sizes to the sum of its
|
|
2176
|
+
columns instead of filling its container, so fixed layout cannot stretch
|
|
2177
|
+
them out of alignment. Declared after `--fixed` and the base table rule so
|
|
2178
|
+
it overrides their width/min-width at equal specificity. */
|
|
2179
|
+
&--inherit-widths {
|
|
2180
|
+
/* Never grow past the parent's expand cell: a child with more columns
|
|
2181
|
+
than the parent overflows the cell, so cap the container here and let its
|
|
2182
|
+
own `overflow-x: auto` scroll the surplus columns inside the expand row
|
|
2183
|
+
rather than stretching the ancestor. */
|
|
2184
|
+
max-width: 100%;
|
|
2185
|
+
min-width: 0;
|
|
2186
|
+
|
|
2187
|
+
table {
|
|
2188
|
+
min-width: 0;
|
|
2189
|
+
/* A *definite* width (not `max-content`) so `table-layout: fixed` stays
|
|
2190
|
+
content-independent: inherited columns keep their colgroup widths and
|
|
2191
|
+
align with the parent instead of growing to fit content. The fill
|
|
2192
|
+
column is `auto`, so fixed layout hands it the leftover; when columns
|
|
2193
|
+
overflow, the table grows past 100% and scrolls. */
|
|
2194
|
+
width: 100%;
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
2197
|
+
|
|
2198
|
+
/* Declared after the base `table { min-width: 100% }` rule so it wins the
|
|
2199
|
+
cascade at equal specificity. The probe publishes the natural content width
|
|
2200
|
+
of all columns; the table fills its container freely but never compresses
|
|
2201
|
+
below that floor — the container's `overflow-x: auto` then shows a
|
|
2202
|
+
scrollbar instead of squishing content. */
|
|
2203
|
+
&--fixed {
|
|
2204
|
+
table {
|
|
2205
|
+
min-width: var(--bb-table-natural-width, 0px);
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2208
|
+
|
|
1370
2209
|
.bb-table-caption {
|
|
1371
2210
|
}
|
|
1372
2211
|
|
|
@@ -1416,11 +2255,41 @@ export type BbTableSlots<Item = any> = {
|
|
|
1416
2255
|
}
|
|
1417
2256
|
|
|
1418
2257
|
td.bb-table-expand__cell {
|
|
2258
|
+
/* Collapse to the content's height instead of inheriting the data-row
|
|
2259
|
+
cell height, so an expand row with no slot content takes no space. */
|
|
2260
|
+
height: auto;
|
|
1419
2261
|
padding: 0;
|
|
2262
|
+
|
|
2263
|
+
&:empty {
|
|
2264
|
+
display: none;
|
|
2265
|
+
}
|
|
1420
2266
|
}
|
|
1421
2267
|
}
|
|
1422
2268
|
}
|
|
1423
2269
|
|
|
2270
|
+
/* Apply the structural edge offsets to the first/last *data* column. They
|
|
2271
|
+
must land on the data cells, not the absolute first/last child: a select
|
|
2272
|
+
or actions column would otherwise absorb the fill and leave the data
|
|
2273
|
+
columns misaligned with the parent (and bloat the actions cell).
|
|
2274
|
+
|
|
2275
|
+
The offset is clamped at 0 here: a positive fill widens the cell and is
|
|
2276
|
+
reserved as extra edge padding, but a *negative* offset (a select/actions
|
|
2277
|
+
column reclaimed from this column's width) must not eat into the normal
|
|
2278
|
+
`--padding-x` — that only shrinks the column width, not its inner padding. */
|
|
2279
|
+
thead tr > th.bb-table-header--edge-start,
|
|
2280
|
+
tbody tr > td.bb-table-data__cell--edge-start {
|
|
2281
|
+
padding-inline-start: calc(
|
|
2282
|
+
var(--padding-x) + max(0px, var(--bb-table-offset-internal-start))
|
|
2283
|
+
);
|
|
2284
|
+
}
|
|
2285
|
+
|
|
2286
|
+
thead tr > th.bb-table-header--edge-end,
|
|
2287
|
+
tbody tr > td.bb-table-data__cell--edge-end {
|
|
2288
|
+
padding-inline-end: calc(
|
|
2289
|
+
var(--padding-x) + max(0px, var(--bb-table-offset-internal-end))
|
|
2290
|
+
);
|
|
2291
|
+
}
|
|
2292
|
+
|
|
1424
2293
|
&.bb-table--loading {
|
|
1425
2294
|
.bb-table-loading__row {
|
|
1426
2295
|
.bb-table-loading__cell {
|