bitboss-ui 2.1.126 → 2.1.128
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/BbButton.md +0 -10
- package/dist/ai/BbTable.md +635 -2
- package/dist/components/BaseButton/BaseButton.vue.d.ts +1 -1
- package/dist/components/BaseCheckbox/BaseCheckbox.vue.d.ts +6 -6
- package/dist/components/BaseCheckboxGroup/BaseCheckboxGroup.vue.d.ts +3 -3
- package/dist/components/BaseColorInput/BaseColorInput.vue.d.ts +6 -6
- package/dist/components/BaseDatePickerInput/BaseDatePickerInput.vue.d.ts +4 -4
- package/dist/components/BaseDialog/BaseDialog.vue.d.ts +6 -6
- package/dist/components/BaseInputContainer/BaseInputContainer.vue.d.ts +1 -1
- package/dist/components/BaseNumberInput/BaseNumberInput.vue.d.ts +8 -8
- package/dist/components/BaseRadio/BaseRadio.vue.d.ts +6 -6
- package/dist/components/BaseRadioGroup/BaseRadioGroup.vue.d.ts +3 -3
- package/dist/components/BaseRating/BaseRating.vue.d.ts +6 -6
- package/dist/components/BaseSelect/BaseSelect.vue.d.ts +2 -2
- package/dist/components/BaseSlider/BaseSlider.vue.d.ts +7 -7
- package/dist/components/BaseSwitch/BaseSwitch.vue.d.ts +6 -6
- package/dist/components/BaseSwitchGroup/BaseSwitchGroup.vue.d.ts +3 -3
- package/dist/components/BaseTag/BaseTag.vue.d.ts +6 -6
- package/dist/components/BaseTextInput/BaseTextInput.vue.d.ts +6 -6
- package/dist/components/BaseTextarea/BaseTextarea.vue.d.ts +6 -6
- package/dist/components/BbButton/BbButton.vue.d.ts +1 -1
- package/dist/components/BbCheckbox/BbCheckbox.vue.d.ts +7 -7
- package/dist/components/BbCheckboxGroup/BbCheckboxGroup.vue.d.ts +3 -3
- package/dist/components/BbColorInput/BbColorInput.vue.d.ts +6 -6
- package/dist/components/BbColorPalette/BbColorPalette.vue.d.ts +2 -2
- package/dist/components/BbDatePickerInput/BbDatePickerInput.vue.d.ts +4 -4
- package/dist/components/BbDialog/BbDialog.vue.d.ts +3 -3
- package/dist/components/BbDropdown/BbDropdown.vue.d.ts +3 -3
- package/dist/components/BbDropzone/BbDropzone.vue.d.ts +4 -4
- package/dist/components/BbNumberInput/BbNumberInput.vue.d.ts +8 -8
- package/dist/components/BbOffCanvas/BbOffCanvas.vue.d.ts +7 -7
- package/dist/components/BbPopover/BbPopover.vue.d.ts +4 -4
- package/dist/components/BbRadio/BbRadio.vue.d.ts +7 -7
- package/dist/components/BbRadioGroup/BbRadioGroup.vue.d.ts +3 -3
- package/dist/components/BbRating/BbRating.vue.d.ts +6 -6
- package/dist/components/BbSelect/BbSelect.vue.d.ts +2 -2
- package/dist/components/BbSelectPopover/BbSelectPopover.vue.d.ts +2 -2
- package/dist/components/BbSlider/BbSlider.vue.d.ts +6 -6
- package/dist/components/BbSwitch/BbSwitch.vue.d.ts +7 -7
- package/dist/components/BbSwitchGroup/BbSwitchGroup.vue.d.ts +3 -3
- package/dist/components/BbTable/types.d.ts +32 -0
- package/dist/components/BbTag/BbTag.vue.d.ts +6 -6
- package/dist/components/BbTextInput/BbTextInput.vue.d.ts +6 -6
- package/dist/components/BbTextarea/BbTextarea.vue.d.ts +6 -6
- package/dist/components/BbTooltip/BbTooltip.vue.d.ts +3 -3
- package/dist/components/CommonPopover/CommonPopover.vue.d.ts +1 -1
- package/dist/components/Transitions/Slide.vue.d.ts +1 -1
- package/dist/composables/useTableWidthContext.d.ts +35 -0
- package/dist/directives/bbDropdown.d.ts +8 -2
- package/dist/directives/bbTooltip.d.ts +7 -2
- package/dist/index.css +1 -1
- package/dist/index.js +9 -9
- package/dist/index240.js +58 -61
- package/dist/index270.js +1 -1
- package/dist/index325.js +34 -41
- package/dist/index326.js +40 -447
- package/dist/index327.js +632 -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 +1 -1
- 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">
|
|
@@ -373,7 +383,16 @@
|
|
|
373
383
|
lang="ts"
|
|
374
384
|
generic="Item = any, T extends ItemKey<Item> = string & {}"
|
|
375
385
|
>
|
|
376
|
-
import {
|
|
386
|
+
import {
|
|
387
|
+
computed,
|
|
388
|
+
nextTick,
|
|
389
|
+
onBeforeUnmount,
|
|
390
|
+
onMounted,
|
|
391
|
+
reactive,
|
|
392
|
+
ref,
|
|
393
|
+
toRef,
|
|
394
|
+
watch,
|
|
395
|
+
} from 'vue';
|
|
377
396
|
import { clamp } from '@/utilities/functions/clamp';
|
|
378
397
|
import { hash } from '@/utilities/functions/hash';
|
|
379
398
|
import { isNil } from '@/utilities/functions/isNil';
|
|
@@ -385,6 +404,7 @@ import { usePrefill } from '@/composables/usePrefill';
|
|
|
385
404
|
import { useIndexById } from '@/composables/useIndexById';
|
|
386
405
|
import { useBaseOptions } from '@/composables/useBaseOptions';
|
|
387
406
|
import { useHashedWatcher } from '@/composables/useHashedWatcher';
|
|
407
|
+
import { useTableWidthContext } from '@/composables/useTableWidthContext';
|
|
388
408
|
import { useLocale } from '@/composables/useLocale';
|
|
389
409
|
import BaseCheckbox from '../BaseCheckbox/BaseCheckbox.vue';
|
|
390
410
|
import BaseRadio from '../BaseRadio/BaseRadio.vue';
|
|
@@ -437,6 +457,7 @@ const props = withDefaults(defineProps<InternalProps<T>>(), {
|
|
|
437
457
|
max: Infinity,
|
|
438
458
|
multiple: true,
|
|
439
459
|
modelValue: () => [],
|
|
460
|
+
inheritColumnWidths: true,
|
|
440
461
|
unselectedItems: () => [],
|
|
441
462
|
});
|
|
442
463
|
|
|
@@ -476,6 +497,524 @@ if (props.multiple && !Array.isArray(props.modelValue)) {
|
|
|
476
497
|
const { t } = useLocale();
|
|
477
498
|
|
|
478
499
|
const tableRef = ref<HTMLTableElement>();
|
|
500
|
+
const containerRef = ref<HTMLElement>();
|
|
501
|
+
|
|
502
|
+
const {
|
|
503
|
+
id: tableId,
|
|
504
|
+
parentId,
|
|
505
|
+
parentNode,
|
|
506
|
+
cssVars,
|
|
507
|
+
parentVar,
|
|
508
|
+
setTracks,
|
|
509
|
+
} = useTableWidthContext({
|
|
510
|
+
id: () => props.id,
|
|
511
|
+
container: () => containerRef.value,
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
const inheritColumnWidths = computed(() => props.inheritColumnWidths !== false);
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Assigned column widths (frozen or inherited) are only authoritative under
|
|
518
|
+
* `table-layout: fixed`; in auto layout they behave like a minimum. So a table
|
|
519
|
+
* that drives widths — explicitly `fixed`, inheriting from a parent, or with a
|
|
520
|
+
* frozen column — uses fixed layout even when `fixed` was not set.
|
|
521
|
+
*/
|
|
522
|
+
const fixedLayout = computed(
|
|
523
|
+
() =>
|
|
524
|
+
props.fixed ||
|
|
525
|
+
props.columns.some((column) => column.width != null) ||
|
|
526
|
+
(inheritColumnWidths.value && !!parentId.value)
|
|
527
|
+
);
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* A nested table inheriting its parent's widths must size itself to the sum of
|
|
531
|
+
* those columns rather than stretching to fill its container — otherwise fixed
|
|
532
|
+
* layout would distribute the slack and the columns would no longer match the
|
|
533
|
+
* parent. Tables with the same column count already sum to the full width, so
|
|
534
|
+
* this is a no-op for them and only narrows tables with fewer columns.
|
|
535
|
+
*/
|
|
536
|
+
const sizeToColumns = computed(
|
|
537
|
+
() => inheritColumnWidths.value && !!parentId.value
|
|
538
|
+
);
|
|
539
|
+
|
|
540
|
+
/** Numbers and bare numeric strings (e.g. `400`, `'400'`) default to px; values
|
|
541
|
+
* that already carry a unit/keyword (`'50%'`, `'10rem'`, `'auto'`) pass through. */
|
|
542
|
+
const toLength = (width: number | string) => {
|
|
543
|
+
if (typeof width === 'number') return `${width}px`;
|
|
544
|
+
const trimmed = width.trim();
|
|
545
|
+
return /^-?\d*\.?\d+$/.test(trimmed) ? `${trimmed}px` : trimmed;
|
|
546
|
+
};
|
|
547
|
+
|
|
548
|
+
/** Number of data columns the parent renders (numeric track keys). */
|
|
549
|
+
const parentDataColumns = computed(() => {
|
|
550
|
+
const tracks = parentNode.value?.tracks;
|
|
551
|
+
return tracks
|
|
552
|
+
? Object.keys(tracks).filter((key) => /^\d+$/.test(key)).length
|
|
553
|
+
: 0;
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* The parent's rendered columns in order — `select` (if any), data columns
|
|
558
|
+
* `0…N-1`, then `actions` (if any) — form a gridline coordinate, one cell per
|
|
559
|
+
* column. Cell `c` is the range `[c, c + 1]`. So a no-select parent starts its
|
|
560
|
+
* data at cell 0, while a selectable parent starts it at cell 1.
|
|
561
|
+
*/
|
|
562
|
+
const parentColumnKeys = computed(() => {
|
|
563
|
+
const tracks = parentNode.value?.tracks ?? {};
|
|
564
|
+
const keys: string[] = [];
|
|
565
|
+
if ('select' in tracks) keys.push('select');
|
|
566
|
+
for (let k = 0; k < parentDataColumns.value; k++) keys.push(String(k));
|
|
567
|
+
if ('actions' in tracks) keys.push('actions');
|
|
568
|
+
return keys;
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
/** Gridline cells where the parent's data region begins/ends and the table ends. */
|
|
572
|
+
const firstDataCell = computed(() =>
|
|
573
|
+
'select' in (parentNode.value?.tracks ?? {}) ? 1 : 0
|
|
574
|
+
);
|
|
575
|
+
const dataEndCell = computed(
|
|
576
|
+
() => firstDataCell.value + parentDataColumns.value
|
|
577
|
+
);
|
|
578
|
+
const tableEndCell = computed(() => parentColumnKeys.value.length);
|
|
579
|
+
|
|
580
|
+
/** Parent track custom property for the integer cell `[cell, cell + 1]`. */
|
|
581
|
+
const cellTrackVar = (cell: number) => {
|
|
582
|
+
const key = parentColumnKeys.value[cell];
|
|
583
|
+
return key ? parentVar(key, '0px') : '0px';
|
|
584
|
+
};
|
|
585
|
+
|
|
586
|
+
/** CSS length summing the parent tracks across the gridline range `[a, b]`. */
|
|
587
|
+
const gridRangeWidth = (a: number, b: number): string | null => {
|
|
588
|
+
if (!parentId.value || b - a <= 1e-6) return null;
|
|
589
|
+
const parts: string[] = [];
|
|
590
|
+
for (let cell = Math.floor(a); cell < Math.ceil(b - 1e-6); cell++) {
|
|
591
|
+
const fraction = Math.min(b, cell + 1) - Math.max(a, cell);
|
|
592
|
+
if (fraction <= 1e-6) continue;
|
|
593
|
+
const track = cellTrackVar(cell);
|
|
594
|
+
parts.push(
|
|
595
|
+
fraction >= 1 - 1e-6 ? track : `${Number(fraction.toFixed(4))} * ${track}`
|
|
596
|
+
);
|
|
597
|
+
}
|
|
598
|
+
if (!parts.length) return null;
|
|
599
|
+
// A bare `var()` can stand alone, but a single fractional term still needs a
|
|
600
|
+
// `calc()` wrapper for the multiplication to be valid CSS.
|
|
601
|
+
if (parts.length === 1 && !parts[0].includes('*')) return parts[0];
|
|
602
|
+
return `calc(${parts.join(' + ')})`;
|
|
603
|
+
};
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* Resolved parent snap interval for each data column.
|
|
607
|
+
* - A number snaps to that start point; the column then runs up to the next
|
|
608
|
+
* column's snap point (or one whole track if the next is unset). So a single
|
|
609
|
+
* snap that skips ahead widens this column to cover the gap — `snap: 4` on the
|
|
610
|
+
* second column makes the first effectively `[start, 4]`.
|
|
611
|
+
* - A `[start, end]` pair is taken verbatim (`end` of `-1` runs to the end of
|
|
612
|
+
* the data region).
|
|
613
|
+
* - Omitted, the column chains from the previous column's end.
|
|
614
|
+
*/
|
|
615
|
+
const columnIntervals = computed(() => {
|
|
616
|
+
const cols = props.columns;
|
|
617
|
+
// Explicit start points; a later column's snap sets the previous column's end.
|
|
618
|
+
const starts = cols.map((column) =>
|
|
619
|
+
typeof column.snap === 'number'
|
|
620
|
+
? column.snap
|
|
621
|
+
: Array.isArray(column.snap)
|
|
622
|
+
? column.snap[0]
|
|
623
|
+
: null
|
|
624
|
+
);
|
|
625
|
+
let cursor = firstDataCell.value;
|
|
626
|
+
return cols.map((column, i) => {
|
|
627
|
+
const start = starts[i] ?? cursor;
|
|
628
|
+
let end: number;
|
|
629
|
+
if (Array.isArray(column.snap)) {
|
|
630
|
+
end = column.snap[1] === -1 ? dataEndCell.value : column.snap[1];
|
|
631
|
+
} else {
|
|
632
|
+
const next = starts[i + 1];
|
|
633
|
+
end = next != null && next > start ? next : Math.floor(start) + 1;
|
|
634
|
+
}
|
|
635
|
+
cursor = end;
|
|
636
|
+
return { start, end };
|
|
637
|
+
});
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
// Parent columns left of the first column (e.g. a select this table doesn't
|
|
641
|
+
// render, or columns skipped by the first snap) and those right of the last are
|
|
642
|
+
// folded into the edge columns as the structural offset, keeping the table
|
|
643
|
+
// aligned to the parent. `offsetStartValue` becomes the first column's padding.
|
|
644
|
+
const offsetStartValue = computed(() => {
|
|
645
|
+
const first = columnIntervals.value[0];
|
|
646
|
+
if (!inheritColumnWidths.value || !first) return null;
|
|
647
|
+
// Skip the parent's select cell only when this table renders its own.
|
|
648
|
+
const lead = props.selectable && firstDataCell.value === 1 ? 1 : 0;
|
|
649
|
+
return gridRangeWidth(lead, first.start);
|
|
650
|
+
});
|
|
651
|
+
const offsetEndValue = computed(() => {
|
|
652
|
+
const intervals = columnIntervals.value;
|
|
653
|
+
const last = intervals[intervals.length - 1];
|
|
654
|
+
if (!inheritColumnWidths.value || !last) return null;
|
|
655
|
+
const hasActions = 'actions' in (parentNode.value?.tracks ?? {});
|
|
656
|
+
const end =
|
|
657
|
+
props.actions && hasActions ? tableEndCell.value - 1 : tableEndCell.value;
|
|
658
|
+
return gridRangeWidth(last.end, end);
|
|
659
|
+
});
|
|
660
|
+
const offsetStartActive = computed(() => offsetStartValue.value !== null);
|
|
661
|
+
const offsetEndActive = computed(() => offsetEndValue.value !== null);
|
|
662
|
+
|
|
663
|
+
/**
|
|
664
|
+
* Measured intrinsic content widths of the structural columns. In a fixed table
|
|
665
|
+
* these pin `select`/`actions` to their content so the remaining space is shared
|
|
666
|
+
* among the data columns; `null` falls back to the regular auto behavior.
|
|
667
|
+
*/
|
|
668
|
+
const structuralFit = reactive<{
|
|
669
|
+
select: string | null;
|
|
670
|
+
actions: string | null;
|
|
671
|
+
}>({ select: null, actions: null });
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* Rendered columns in DOM order, each with the track key it publishes/inherits
|
|
675
|
+
* and the width to apply via `<colgroup>`. A frozen column width wins; otherwise
|
|
676
|
+
* an unfrozen column syncs to the parent's matching track (or `auto` at root).
|
|
677
|
+
* The first/last column additionally absorbs the structural edge offset.
|
|
678
|
+
*/
|
|
679
|
+
const columnTracks = computed(() => {
|
|
680
|
+
const sync = (key: string) =>
|
|
681
|
+
inheritColumnWidths.value ? parentVar(key) : 'auto';
|
|
682
|
+
// In fixed layout `auto` columns share width equally, so pin the structural
|
|
683
|
+
// columns to their measured content width and let the data columns share.
|
|
684
|
+
const structural = (key: 'select' | 'actions') =>
|
|
685
|
+
fixedLayout.value && structuralFit[key]
|
|
686
|
+
? (structuralFit[key] as string)
|
|
687
|
+
: sync(key);
|
|
688
|
+
const tracks: { key: string; width: string }[] = [];
|
|
689
|
+
|
|
690
|
+
if (props.selectable) {
|
|
691
|
+
tracks.push({ key: 'select', width: structural('select') });
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
const lastIndex = props.columns.length - 1;
|
|
695
|
+
props.columns.forEach((column, index) => {
|
|
696
|
+
const key = String(index);
|
|
697
|
+
const frozen = column.width != null ? toLength(column.width) : null;
|
|
698
|
+
// For auto columns in fixed layout, use the proportional width derived
|
|
699
|
+
// from the offscreen probe so each column is pinned to a precise value
|
|
700
|
+
// rather than relying on equal-share distribution (which ignores natural
|
|
701
|
+
// content widths and can under-allocate to wider auto columns).
|
|
702
|
+
const probeWidth =
|
|
703
|
+
frozen == null && fixedLayout.value
|
|
704
|
+
? (probeColWidths.value[index] ?? null)
|
|
705
|
+
: null;
|
|
706
|
+
// The column's own width is the gridline range it spans of the parent.
|
|
707
|
+
const interval = columnIntervals.value[index];
|
|
708
|
+
const span =
|
|
709
|
+
frozen ?? probeWidth ?? gridRangeWidth(interval.start, interval.end);
|
|
710
|
+
|
|
711
|
+
const offsets: string[] = [];
|
|
712
|
+
if (index === 0 && offsetStartActive.value) {
|
|
713
|
+
offsets.push('var(--bb-table-offset-internal-start)');
|
|
714
|
+
}
|
|
715
|
+
if (index === lastIndex && offsetEndActive.value) {
|
|
716
|
+
offsets.push('var(--bb-table-offset-internal-end)');
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// When folding an offset, use a calc-safe length base (no `auto`) so the
|
|
720
|
+
// column width = offset(s) + the column's own span.
|
|
721
|
+
const width = offsets.length
|
|
722
|
+
? `calc(${[...offsets, span ?? '0px'].join(' + ')})`
|
|
723
|
+
: (span ?? 'auto');
|
|
724
|
+
|
|
725
|
+
tracks.push({ key, width });
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
if (props.actions) {
|
|
729
|
+
tracks.push({ key: 'actions', width: structural('actions') });
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
return tracks;
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
/**
|
|
736
|
+
* Standalone edge-offset custom properties consumed by the first/last cell
|
|
737
|
+
* padding and the first/last column width. Set only when an offset is active;
|
|
738
|
+
* otherwise the CSS default of `0px` makes both calcs collapse to their base.
|
|
739
|
+
*/
|
|
740
|
+
const structuralOffset = computed<Record<string, string>>(() => {
|
|
741
|
+
const style: Record<string, string> = {};
|
|
742
|
+
if (offsetStartValue.value) {
|
|
743
|
+
style['--bb-table-offset-start'] = offsetStartValue.value;
|
|
744
|
+
}
|
|
745
|
+
if (offsetEndValue.value) {
|
|
746
|
+
style['--bb-table-offset-end'] = offsetEndValue.value;
|
|
747
|
+
}
|
|
748
|
+
return style;
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
const containerStyle = computed(() => {
|
|
752
|
+
const vars: Record<string, string> = {
|
|
753
|
+
...cssVars.value,
|
|
754
|
+
...structuralOffset.value,
|
|
755
|
+
};
|
|
756
|
+
|
|
757
|
+
if (naturalTotalWidth.value) {
|
|
758
|
+
vars['--bb-table-natural-width'] = naturalTotalWidth.value;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
return vars;
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
/** Reads the header cells in render order: [select?, ...data, actions?]. */
|
|
765
|
+
const headerCells = () =>
|
|
766
|
+
Array.from(
|
|
767
|
+
tableRef.value?.querySelectorAll<HTMLElement>(
|
|
768
|
+
':scope > thead > tr.bb-table-header-row > th'
|
|
769
|
+
) ?? []
|
|
770
|
+
);
|
|
771
|
+
|
|
772
|
+
/** Measures the rendered columns and publishes them as this table's tracks. */
|
|
773
|
+
const measureTracks = () => {
|
|
774
|
+
const keys = columnTracks.value.map((track) => track.key);
|
|
775
|
+
const tracks: Record<string, string> = {};
|
|
776
|
+
headerCells().forEach((cell, index) => {
|
|
777
|
+
const key = keys[index];
|
|
778
|
+
if (key != null) tracks[key] = `${cell.getBoundingClientRect().width}px`;
|
|
779
|
+
});
|
|
780
|
+
setTracks(tracks);
|
|
781
|
+
};
|
|
782
|
+
|
|
783
|
+
/**
|
|
784
|
+
* Measures the intrinsic content width of the structural columns across header
|
|
785
|
+
* and body. Fixed layout sizes columns from the header row only, so the body is
|
|
786
|
+
* required here: the actions header is `sr-only`, while the real control lives
|
|
787
|
+
* in the body. The measured child width is independent of the column width, so
|
|
788
|
+
* pinning the column to it converges instead of oscillating.
|
|
789
|
+
*/
|
|
790
|
+
const measureStructuralFit = () => {
|
|
791
|
+
const table = tableRef.value;
|
|
792
|
+
if (!table) return;
|
|
793
|
+
|
|
794
|
+
const fit = (selector: string) => {
|
|
795
|
+
let max = 0;
|
|
796
|
+
table.querySelectorAll<HTMLElement>(selector).forEach((cell) => {
|
|
797
|
+
let content = 0;
|
|
798
|
+
cell.querySelectorAll<HTMLElement>(':scope > *').forEach((child) => {
|
|
799
|
+
content = Math.max(content, child.getBoundingClientRect().width);
|
|
800
|
+
});
|
|
801
|
+
const style = getComputedStyle(cell);
|
|
802
|
+
const padding =
|
|
803
|
+
parseFloat(style.paddingLeft) + parseFloat(style.paddingRight);
|
|
804
|
+
max = Math.max(max, content + padding);
|
|
805
|
+
});
|
|
806
|
+
return max > 0 ? `${Math.ceil(max)}px` : null;
|
|
807
|
+
};
|
|
808
|
+
|
|
809
|
+
structuralFit.select = props.selectable
|
|
810
|
+
? fit('.bb-table-header--select, .bb-table-data__cell--select')
|
|
811
|
+
: null;
|
|
812
|
+
structuralFit.actions = props.actions
|
|
813
|
+
? fit('.bb-table-header--actions, .bb-table__cell--actions')
|
|
814
|
+
: null;
|
|
815
|
+
};
|
|
816
|
+
|
|
817
|
+
const measure = () => {
|
|
818
|
+
measureStructuralFit();
|
|
819
|
+
measureTracks();
|
|
820
|
+
};
|
|
821
|
+
|
|
822
|
+
/**
|
|
823
|
+
* Proportional `<col>` widths derived from an auto-layout probe, one entry per
|
|
824
|
+
* data column (`null` for frozen columns — they keep their declared width).
|
|
825
|
+
*
|
|
826
|
+
* For each auto column the formula is:
|
|
827
|
+
* width = max(natural, available × (natural / autoTotal))
|
|
828
|
+
*
|
|
829
|
+
* where `available` = containerWidth − frozenColumnWidths − structuralColumnWidths
|
|
830
|
+
* and `autoTotal` = sum of auto-column natural widths.
|
|
831
|
+
*
|
|
832
|
+
* This gives proportional growth when the container is wide (auto columns scale
|
|
833
|
+
* relative to each other) and a hard natural-width floor when it is narrow (the
|
|
834
|
+
* table-level `min-width` then triggers horizontal overflow instead of squishing).
|
|
835
|
+
*
|
|
836
|
+
* Re-measured after any DOM mutation (debounced 200 ms) and after any container
|
|
837
|
+
* resize (via a dedicated ResizeObserver on the container element).
|
|
838
|
+
*/
|
|
839
|
+
const probeColWidths = ref<(string | null)[]>([]);
|
|
840
|
+
|
|
841
|
+
/**
|
|
842
|
+
* Total natural minimum width of the table (all columns, including structural).
|
|
843
|
+
* Published as `--bb-table-natural-width` and used as `min-width` on the
|
|
844
|
+
* `<table>` element so fixed-layout compression never squeezes content below its
|
|
845
|
+
* natural size.
|
|
846
|
+
*/
|
|
847
|
+
const naturalTotalWidth = ref<string | null>(null);
|
|
848
|
+
|
|
849
|
+
const runNaturalWidthProbe = () => {
|
|
850
|
+
const container = containerRef.value;
|
|
851
|
+
if (!container || !fixedLayout.value) return;
|
|
852
|
+
if (!props.columns.some((col) => col.width != null)) return;
|
|
853
|
+
|
|
854
|
+
const containerWidth = container.offsetWidth;
|
|
855
|
+
const dataOffset = props.selectable ? 1 : 0;
|
|
856
|
+
|
|
857
|
+
// ── Real-table measurements ────────────────────────────────────────────
|
|
858
|
+
// Structural column widths come from measureStructuralFit (already run).
|
|
859
|
+
const selectWidth = structuralFit.select
|
|
860
|
+
? parseFloat(structuralFit.select)
|
|
861
|
+
: 0;
|
|
862
|
+
const actionsWidth = structuralFit.actions
|
|
863
|
+
? parseFloat(structuralFit.actions)
|
|
864
|
+
: 0;
|
|
865
|
+
|
|
866
|
+
// Frozen column widths from the live fixed-layout table — getBoundingClientRect
|
|
867
|
+
// returns the exact px value the browser computed from the declared CSS length,
|
|
868
|
+
// so no unit parsing is needed and viewport-relative units work correctly.
|
|
869
|
+
const realThs = headerCells();
|
|
870
|
+
let frozenTotal = 0;
|
|
871
|
+
props.columns.forEach((col, i) => {
|
|
872
|
+
if (col.width != null) {
|
|
873
|
+
frozenTotal +=
|
|
874
|
+
realThs[dataOffset + i]?.getBoundingClientRect().width ?? 0;
|
|
875
|
+
}
|
|
876
|
+
});
|
|
877
|
+
|
|
878
|
+
// ── Offscreen probe for natural (max-content) auto-column widths ───────
|
|
879
|
+
// The clone's inner table is forced to `width: max-content` and all <col>
|
|
880
|
+
// hints are stripped so each <th> reports its true content-driven width,
|
|
881
|
+
// independent of the container size or previous probe cycles.
|
|
882
|
+
const probe = document.createElement('div');
|
|
883
|
+
probe.style.cssText = [
|
|
884
|
+
'position:absolute',
|
|
885
|
+
'top:-9999px',
|
|
886
|
+
'left:-9999px',
|
|
887
|
+
'visibility:hidden',
|
|
888
|
+
'pointer-events:none',
|
|
889
|
+
`width:${containerWidth}px`,
|
|
890
|
+
`height:${container.offsetHeight}px`,
|
|
891
|
+
'overflow:visible',
|
|
892
|
+
].join(';');
|
|
893
|
+
|
|
894
|
+
const clone = container.cloneNode(true) as HTMLElement;
|
|
895
|
+
clone.classList.remove('bb-table--fixed');
|
|
896
|
+
|
|
897
|
+
// Frozen <col> hints are kept intact so auto columns are probed in the
|
|
898
|
+
// correct spatial context (a 400 px frozen column still occupies 400 px,
|
|
899
|
+
// giving neighbouring auto columns the right available space to size against).
|
|
900
|
+
// Only auto column hints are cleared so previous probe cycles cannot inflate
|
|
901
|
+
// their measured natural widths.
|
|
902
|
+
const probeColEls = Array.from(clone.querySelectorAll<HTMLElement>('col'));
|
|
903
|
+
props.columns.forEach((col, i) => {
|
|
904
|
+
if (col.width == null) {
|
|
905
|
+
const probeCol = probeColEls[dataOffset + i];
|
|
906
|
+
if (probeCol) probeCol.style.width = '';
|
|
907
|
+
}
|
|
908
|
+
});
|
|
909
|
+
|
|
910
|
+
const innerTable = clone.querySelector<HTMLElement>('table');
|
|
911
|
+
if (innerTable) {
|
|
912
|
+
innerTable.style.width = 'max-content';
|
|
913
|
+
innerTable.style.minWidth = '0';
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
probe.appendChild(clone);
|
|
917
|
+
document.body.appendChild(probe);
|
|
918
|
+
|
|
919
|
+
const headerRow = probe.querySelector<HTMLElement>(
|
|
920
|
+
'table thead tr.bb-table-header-row'
|
|
921
|
+
);
|
|
922
|
+
const ths = headerRow
|
|
923
|
+
? Array.from(headerRow.querySelectorAll<HTMLElement>(':scope > th'))
|
|
924
|
+
: [];
|
|
925
|
+
|
|
926
|
+
// Only auto columns — frozen columns use their declared widths from above.
|
|
927
|
+
const autoNaturals = props.columns.map((col, i) => {
|
|
928
|
+
if (col.width != null) return 0;
|
|
929
|
+
const th = ths[dataOffset + i];
|
|
930
|
+
return th ? th.getBoundingClientRect().width : 0;
|
|
931
|
+
});
|
|
932
|
+
|
|
933
|
+
document.body.removeChild(probe);
|
|
934
|
+
|
|
935
|
+
// ── Proportional <col> widths ──────────────────────────────────────────
|
|
936
|
+
const autoNaturalTotal = autoNaturals.reduce((s, w) => s + w, 0);
|
|
937
|
+
const available = containerWidth - frozenTotal - selectWidth - actionsWidth;
|
|
938
|
+
|
|
939
|
+
// Scale each auto column proportionally to available space, but never
|
|
940
|
+
// below its natural content width so content is never squished.
|
|
941
|
+
probeColWidths.value = props.columns.map((col, i) => {
|
|
942
|
+
if (col.width != null) return null;
|
|
943
|
+
const natural = autoNaturals[i];
|
|
944
|
+
if (autoNaturalTotal <= 0) return `${Math.ceil(natural)}px`;
|
|
945
|
+
const share = available > 0 ? available * (natural / autoNaturalTotal) : 0;
|
|
946
|
+
return `${Math.ceil(Math.max(natural, share))}px`;
|
|
947
|
+
});
|
|
948
|
+
|
|
949
|
+
// Table min-width = real frozen widths + natural auto widths + structural.
|
|
950
|
+
// Using real frozen widths (not probe) ensures declared px/rem/% lengths
|
|
951
|
+
// are respected rather than the probe's stripped-hint approximation.
|
|
952
|
+
const naturalTotal =
|
|
953
|
+
frozenTotal + autoNaturalTotal + selectWidth + actionsWidth;
|
|
954
|
+
naturalTotalWidth.value =
|
|
955
|
+
naturalTotal > 0 ? `${Math.ceil(naturalTotal)}px` : null;
|
|
956
|
+
};
|
|
957
|
+
|
|
958
|
+
let naturalWidthDebounceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
959
|
+
let mutationObserver: MutationObserver | undefined;
|
|
960
|
+
|
|
961
|
+
/** Invalidates the cached natural width and schedules a fresh probe. */
|
|
962
|
+
const scheduleNaturalWidthProbe = () => {
|
|
963
|
+
if (naturalWidthDebounceTimer !== null)
|
|
964
|
+
clearTimeout(naturalWidthDebounceTimer);
|
|
965
|
+
naturalWidthDebounceTimer = setTimeout(() => {
|
|
966
|
+
naturalWidthDebounceTimer = null;
|
|
967
|
+
runNaturalWidthProbe();
|
|
968
|
+
}, 200);
|
|
969
|
+
};
|
|
970
|
+
|
|
971
|
+
let resizeObserver: ResizeObserver | undefined;
|
|
972
|
+
/** Observes the container element specifically for size changes so the
|
|
973
|
+
* proportional <col> widths are recomputed whenever the container grows or
|
|
974
|
+
* shrinks — separately from the header-cell observer that drives measureTracks. */
|
|
975
|
+
let containerResizeObserver: ResizeObserver | undefined;
|
|
976
|
+
|
|
977
|
+
const observeColumns = () => {
|
|
978
|
+
if (!resizeObserver) return;
|
|
979
|
+
resizeObserver.disconnect();
|
|
980
|
+
headerCells().forEach((cell) => resizeObserver?.observe(cell));
|
|
981
|
+
measure();
|
|
982
|
+
};
|
|
983
|
+
|
|
984
|
+
onMounted(() => {
|
|
985
|
+
resizeObserver = new ResizeObserver(() => measure());
|
|
986
|
+
observeColumns();
|
|
987
|
+
|
|
988
|
+
// Initial probe runs after the first render so all cells have content.
|
|
989
|
+
nextTick(runNaturalWidthProbe);
|
|
990
|
+
|
|
991
|
+
// Invalidate the cache whenever rows or cell content change.
|
|
992
|
+
if (tableRef.value) {
|
|
993
|
+
mutationObserver = new MutationObserver(scheduleNaturalWidthProbe);
|
|
994
|
+
mutationObserver.observe(tableRef.value, {
|
|
995
|
+
childList: true,
|
|
996
|
+
subtree: true,
|
|
997
|
+
characterData: true,
|
|
998
|
+
});
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
// Re-probe whenever the container itself is resized so that proportional
|
|
1002
|
+
// <col> widths stay accurate as the available space changes.
|
|
1003
|
+
if (containerRef.value) {
|
|
1004
|
+
containerResizeObserver = new ResizeObserver(scheduleNaturalWidthProbe);
|
|
1005
|
+
containerResizeObserver.observe(containerRef.value);
|
|
1006
|
+
}
|
|
1007
|
+
});
|
|
1008
|
+
|
|
1009
|
+
onBeforeUnmount(() => {
|
|
1010
|
+
resizeObserver?.disconnect();
|
|
1011
|
+
containerResizeObserver?.disconnect();
|
|
1012
|
+
mutationObserver?.disconnect();
|
|
1013
|
+
if (naturalWidthDebounceTimer !== null)
|
|
1014
|
+
clearTimeout(naturalWidthDebounceTimer);
|
|
1015
|
+
});
|
|
1016
|
+
|
|
1017
|
+
watch(columnTracks, () => nextTick(observeColumns), { flush: 'post' });
|
|
479
1018
|
|
|
480
1019
|
/** Builds accessible label for selection controls from row content. */
|
|
481
1020
|
const getAccessibleLabel = (columns: MappedCell[]) =>
|
|
@@ -493,7 +1032,8 @@ const containerClass = computed(() => ({
|
|
|
493
1032
|
'bb-table': true,
|
|
494
1033
|
[`bb-table--align-${props.align}`]: true,
|
|
495
1034
|
'bb-table--compact': props.compact,
|
|
496
|
-
'bb-table--fixed':
|
|
1035
|
+
'bb-table--fixed': fixedLayout.value,
|
|
1036
|
+
'bb-table--inherit-widths': sizeToColumns.value,
|
|
497
1037
|
'bb-table--fixed-header': props.fixedHeaders,
|
|
498
1038
|
'bb-table--loading': loading.value,
|
|
499
1039
|
'bb-table--empty': !options.value.length,
|
|
@@ -611,6 +1151,7 @@ const mappedHeaders = computed(() =>
|
|
|
611
1151
|
? 'ascending'
|
|
612
1152
|
: 'descending'
|
|
613
1153
|
: undefined;
|
|
1154
|
+
|
|
614
1155
|
return {
|
|
615
1156
|
align,
|
|
616
1157
|
key: column.key,
|
|
@@ -919,6 +1460,30 @@ export type BbTableColumn<Item = any> = BaseColumn<Item> & {
|
|
|
919
1460
|
* Defines the classes to be passed to the `<th>`.
|
|
920
1461
|
*/
|
|
921
1462
|
thClass?: Classes;
|
|
1463
|
+
|
|
1464
|
+
/**
|
|
1465
|
+
* Freezes the column to a fixed width (numbers are treated as px). When
|
|
1466
|
+
* omitted the column syncs its width to the parent table's matching track.
|
|
1467
|
+
*/
|
|
1468
|
+
width?: number | string;
|
|
1469
|
+
|
|
1470
|
+
/**
|
|
1471
|
+
* For a nested table, where this column snaps onto the parent's grid. The
|
|
1472
|
+
* parent's columns form snap points where `select` occupies `[0, 1]`, data
|
|
1473
|
+
* column `k` occupies `[k + 1, k + 2]` and `actions` occupies `[N + 1, N + 2]`.
|
|
1474
|
+
*
|
|
1475
|
+
* - A single number is a start snap point that takes one parent track, e.g.
|
|
1476
|
+
* `2` → `[2, 3]`. It may skip ahead (`5` jumps straight to the sixth track),
|
|
1477
|
+
* and the next column resumes from where it ends.
|
|
1478
|
+
* - A `[start, end]` pair snaps between two points; the width is the sum of
|
|
1479
|
+
* the fractional parent tracks it covers, so `[3, 4.5]` is the full fourth
|
|
1480
|
+
* track plus half of the fifth. `end` of `-1` runs to the end of the data
|
|
1481
|
+
* region.
|
|
1482
|
+
*
|
|
1483
|
+
* When omitted the column chains from the previous column's end to the next
|
|
1484
|
+
* whole snap point (a one-to-one inheritance by index).
|
|
1485
|
+
*/
|
|
1486
|
+
snap?: number | [start: number, end: number];
|
|
922
1487
|
};
|
|
923
1488
|
|
|
924
1489
|
export type BbTableProps<Item = any> = {
|
|
@@ -943,6 +1508,18 @@ export type BbTableProps<Item = any> = {
|
|
|
943
1508
|
*/
|
|
944
1509
|
align?: 'left' | 'center' | 'right';
|
|
945
1510
|
|
|
1511
|
+
/**
|
|
1512
|
+
* Stable id for this table's width context. When omitted a unique id is
|
|
1513
|
+
* generated. Nested tables use the nearest ancestor id to inherit widths.
|
|
1514
|
+
*/
|
|
1515
|
+
id?: string;
|
|
1516
|
+
|
|
1517
|
+
/**
|
|
1518
|
+
* When `true` (the default) unfrozen columns inherit their width from the
|
|
1519
|
+
* parent table's matching track, accounting for `select`/`actions` columns.
|
|
1520
|
+
*/
|
|
1521
|
+
inheritColumnWidths?: boolean;
|
|
1522
|
+
|
|
946
1523
|
/**
|
|
947
1524
|
* Boolean that defines whether to display a "Select all" checkbox.
|
|
948
1525
|
*/
|
|
@@ -1291,10 +1868,24 @@ export type BbTableSlots<Item = any> = {
|
|
|
1291
1868
|
--padding-x: 16px;
|
|
1292
1869
|
--padding-y: 8px;
|
|
1293
1870
|
--actions-spacing: 8px;
|
|
1871
|
+
/* Raw structural edge offset measured by the component (the parent's
|
|
1872
|
+
select/actions track width). Default 0 keeps every calc at its base. */
|
|
1873
|
+
--bb-table-offset-start: 0px;
|
|
1874
|
+
--bb-table-offset-end: 0px;
|
|
1875
|
+
/* Routing of that offset. By default it is "internal": added to the
|
|
1876
|
+
first/last cell padding and folded into the first/last column width.
|
|
1877
|
+
Swap internal↔external (set internal to 0 and external to the raw offset)
|
|
1878
|
+
to pad the whole table instead of the edge cells. */
|
|
1879
|
+
--bb-table-offset-internal-start: var(--bb-table-offset-start);
|
|
1880
|
+
--bb-table-offset-internal-end: var(--bb-table-offset-end);
|
|
1881
|
+
--bb-table-offset-external-start: 0px;
|
|
1882
|
+
--bb-table-offset-external-end: 0px;
|
|
1294
1883
|
border-collapse: separate;
|
|
1295
1884
|
border-spacing: 0;
|
|
1296
1885
|
display: grid;
|
|
1297
1886
|
overflow-x: auto;
|
|
1887
|
+
padding-inline-end: var(--bb-table-offset-external-end);
|
|
1888
|
+
padding-inline-start: var(--bb-table-offset-external-start);
|
|
1298
1889
|
|
|
1299
1890
|
position: relative;
|
|
1300
1891
|
|
|
@@ -1367,6 +1958,28 @@ export type BbTableSlots<Item = any> = {
|
|
|
1367
1958
|
min-width: 100%;
|
|
1368
1959
|
}
|
|
1369
1960
|
|
|
1961
|
+
/* A nested table inheriting its parent's widths sizes to the sum of its
|
|
1962
|
+
columns instead of filling its container, so fixed layout cannot stretch
|
|
1963
|
+
them out of alignment. Declared after `--fixed` and the base table rule so
|
|
1964
|
+
it overrides their width/min-width at equal specificity. */
|
|
1965
|
+
&--inherit-widths {
|
|
1966
|
+
table {
|
|
1967
|
+
min-width: 0;
|
|
1968
|
+
width: max-content;
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1972
|
+
/* Declared after the base `table { min-width: 100% }` rule so it wins the
|
|
1973
|
+
cascade at equal specificity. The probe publishes the natural content width
|
|
1974
|
+
of all columns; the table fills its container freely but never compresses
|
|
1975
|
+
below that floor — the container's `overflow-x: auto` then shows a
|
|
1976
|
+
scrollbar instead of squishing content. */
|
|
1977
|
+
&--fixed {
|
|
1978
|
+
table {
|
|
1979
|
+
min-width: var(--bb-table-natural-width, 0px);
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1370
1983
|
.bb-table-caption {
|
|
1371
1984
|
}
|
|
1372
1985
|
|
|
@@ -1421,6 +2034,26 @@ export type BbTableSlots<Item = any> = {
|
|
|
1421
2034
|
}
|
|
1422
2035
|
}
|
|
1423
2036
|
|
|
2037
|
+
/* Apply the structural edge offsets to the first/last cell of every data
|
|
2038
|
+
row. The full-width expand and loading cells are excluded. */
|
|
2039
|
+
thead tr > th:first-child,
|
|
2040
|
+
tbody
|
|
2041
|
+
tr
|
|
2042
|
+
> td:first-child:not(.bb-table-expand__cell):not(.bb-table-loading__cell) {
|
|
2043
|
+
padding-inline-start: calc(
|
|
2044
|
+
var(--padding-x) + var(--bb-table-offset-internal-start)
|
|
2045
|
+
);
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
thead tr > th:last-child,
|
|
2049
|
+
tbody
|
|
2050
|
+
tr
|
|
2051
|
+
> td:last-child:not(.bb-table-expand__cell):not(.bb-table-loading__cell) {
|
|
2052
|
+
padding-inline-end: calc(
|
|
2053
|
+
var(--padding-x) + var(--bb-table-offset-internal-end)
|
|
2054
|
+
);
|
|
2055
|
+
}
|
|
2056
|
+
|
|
1424
2057
|
&.bb-table--loading {
|
|
1425
2058
|
.bb-table-loading__row {
|
|
1426
2059
|
.bb-table-loading__cell {
|