vueless 1.4.12-beta.2 → 1.4.12-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/README.md +1 -1
- package/composables/useUI.ts +19 -1
- package/index.d.ts +128 -0
- package/index.ts +128 -0
- package/package.json +10 -9
- package/ui.button/config.ts +1 -1
- package/ui.button-link/config.ts +1 -1
- package/ui.button-toggle/UToggle.vue +6 -4
- package/ui.button-toggle/storybook/stories.ts +4 -1
- package/ui.button-toggle/tests/UToggle.test-d.ts +48 -0
- package/ui.button-toggle/tests/UToggleTypes.vue +55 -0
- package/ui.button-toggle/types.ts +18 -6
- package/ui.container-accordion/types.ts +5 -2
- package/ui.container-collapsible/config.ts +1 -1
- package/ui.container-drawer/tests/UDrawer.test.ts +10 -17
- package/ui.container-modal/tests/UModal.test.ts +10 -17
- package/ui.data-list/UDataList.vue +48 -17
- package/ui.data-list/config.ts +2 -2
- package/ui.data-list/storybook/stories.ts +5 -2
- package/ui.data-list/tests/UDataList.test-d.ts +76 -0
- package/ui.data-list/tests/UDataList.test.ts +52 -3
- package/ui.data-list/tests/UDataListTypes.vue +110 -0
- package/ui.data-list/types.ts +37 -5
- package/ui.data-table/UTable.vue +202 -24
- package/ui.data-table/UTableRow.vue +4 -3
- package/ui.data-table/config.ts +11 -2
- package/ui.data-table/storybook/stories.ts +34 -1
- package/ui.data-table/tests/UTable.test-d.ts +179 -0
- package/ui.data-table/tests/UTable.test.ts +85 -7
- package/ui.data-table/tests/UTableEmptyColumnsTypes.vue +27 -0
- package/ui.data-table/tests/UTableTypes.vue +193 -0
- package/ui.data-table/types.ts +106 -9
- package/ui.data-table/utilTable.ts +1 -1
- package/ui.dropdown/UDropdown.vue +19 -8
- package/ui.dropdown/storybook/stories.ts +4 -1
- package/ui.dropdown/tests/UDropdown.test-d.ts +61 -0
- package/ui.dropdown/tests/UDropdown.test.ts +8 -1
- package/ui.dropdown/tests/UDropdownTypes.vue +111 -0
- package/ui.dropdown/types.ts +27 -3
- package/ui.dropdown-badge/UDropdownBadge.vue +9 -4
- package/ui.dropdown-badge/storybook/stories.ts +6 -1
- package/ui.dropdown-badge/tests/UDropdownBadge.test-d.ts +55 -0
- package/ui.dropdown-badge/tests/UDropdownBadge.test.ts +8 -1
- package/ui.dropdown-badge/tests/UDropdownBadgeTypes.vue +107 -0
- package/ui.dropdown-badge/types.ts +17 -3
- package/ui.dropdown-button/UDropdownButton.vue +9 -4
- package/ui.dropdown-button/storybook/stories.ts +6 -1
- package/ui.dropdown-button/tests/UDropdownButton.test-d.ts +57 -0
- package/ui.dropdown-button/tests/UDropdownButton.test.ts +8 -1
- package/ui.dropdown-button/tests/UDropdownButtonTypes.vue +107 -0
- package/ui.dropdown-button/types.ts +17 -3
- package/ui.dropdown-link/UDropdownLink.vue +9 -4
- package/ui.dropdown-link/config.ts +1 -1
- package/ui.dropdown-link/storybook/stories.ts +4 -1
- package/ui.dropdown-link/tests/UDropdownLink.test-d.ts +55 -0
- package/ui.dropdown-link/tests/UDropdownLink.test.ts +8 -1
- package/ui.dropdown-link/tests/UDropdownLinkTypes.vue +107 -0
- package/ui.dropdown-link/types.ts +17 -3
- package/ui.form-calendar/config.ts +1 -1
- package/ui.form-checkbox/config.ts +1 -1
- package/ui.form-checkbox/tests/UCheckbox.test.ts +4 -4
- package/ui.form-checkbox/types.ts +4 -2
- package/ui.form-checkbox-group/UCheckboxGroup.vue +8 -2
- package/ui.form-date-picker/UDatePicker.vue +11 -1
- package/ui.form-date-picker/tests/UDatePicker.test.ts +47 -0
- package/ui.form-input/UInput.vue +1 -0
- package/ui.form-input/config.ts +1 -1
- package/ui.form-input/tests/UInput.test.ts +16 -0
- package/ui.form-input-file/config.ts +1 -1
- package/ui.form-listbox/UListbox.vue +29 -12
- package/ui.form-listbox/config.ts +1 -1
- package/ui.form-listbox/storybook/stories.ts +4 -1
- package/ui.form-listbox/tests/UListbox.test-d.ts +63 -0
- package/ui.form-listbox/tests/UListboxTypes.vue +107 -0
- package/ui.form-listbox/types.ts +34 -2
- package/ui.form-radio/config.ts +1 -1
- package/ui.form-radio-group/URadioGroup.vue +15 -3
- package/ui.form-radio-group/config.ts +8 -3
- package/ui.form-radio-group/types.ts +2 -3
- package/ui.form-select/USelect.vue +49 -17
- package/ui.form-select/config.ts +4 -4
- package/ui.form-select/storybook/stories.ts +4 -1
- package/ui.form-select/tests/USelect.test-d.ts +76 -0
- package/ui.form-select/tests/USelect.test.ts +65 -1
- package/ui.form-select/tests/USelectTypes.vue +113 -0
- package/ui.form-select/types.ts +55 -4
- package/ui.form-switch/USwitch.vue +13 -12
- package/ui.form-switch/config.ts +2 -1
- package/ui.form-switch/tests/USwitch.test.ts +151 -0
- package/ui.form-textarea/config.ts +1 -1
- package/ui.image-avatar/types.ts +5 -2
- package/ui.image-avatar-group/types.ts +5 -2
- package/ui.image-icon/UIcon.vue +3 -2
- package/ui.image-icon/types.ts +2 -2
- package/ui.navigation-breadcrumbs/types.ts +5 -2
- package/ui.navigation-tab/UTab.vue +14 -9
- package/ui.navigation-tab/tests/UTab.test.ts +14 -0
- package/ui.navigation-tabs/UTabs.vue +158 -6
- package/ui.navigation-tabs/config.ts +2 -1
- package/ui.navigation-tabs/constants.ts +2 -0
- package/ui.navigation-tabs/storybook/stories.ts +12 -1
- package/ui.navigation-tabs/tests/UTabs.test-d.ts +50 -0
- package/ui.navigation-tabs/tests/UTabs.test.ts +213 -1
- package/ui.navigation-tabs/tests/UTabsTypes.vue +65 -0
- package/ui.navigation-tabs/types.ts +26 -4
- package/ui.skeleton-text/constants.ts +1 -1
- package/ui.text-badge/config.ts +1 -1
- package/utils/node/loaderIcon.js +1 -1
- package/v.tooltip/vTooltip.ts +1 -1
package/ui.data-table/UTable.vue
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
1
|
+
<script setup lang="ts" generic="TRow extends TableRow, TCol extends string = string">
|
|
2
2
|
import {
|
|
3
3
|
shallowRef,
|
|
4
4
|
ref,
|
|
@@ -31,6 +31,7 @@ import UEmpty from "../ui.container-empty/UEmpty.vue";
|
|
|
31
31
|
import UCheckbox from "../ui.form-checkbox/UCheckbox.vue";
|
|
32
32
|
import ULoaderProgress from "../ui.loader-progress/ULoaderProgress.vue";
|
|
33
33
|
import UDivider from "../ui.container-divider/UDivider.vue";
|
|
34
|
+
import USkeleton from "../ui.skeleton/USkeleton.vue";
|
|
34
35
|
import UTableRow from "./UTableRow.vue";
|
|
35
36
|
|
|
36
37
|
import type { ComputedRef, VNode } from "vue";
|
|
@@ -46,18 +47,22 @@ import type {
|
|
|
46
47
|
FlatRow,
|
|
47
48
|
ColumnObject,
|
|
48
49
|
SearchMatch,
|
|
50
|
+
TableRow,
|
|
49
51
|
UTableRowProps,
|
|
52
|
+
UTableSlots,
|
|
53
|
+
SelectedSlotRow,
|
|
50
54
|
} from "./types";
|
|
51
55
|
|
|
52
56
|
defineOptions({ inheritAttrs: false });
|
|
53
57
|
|
|
54
|
-
const props = withDefaults(defineProps<Props
|
|
55
|
-
...getDefaults<Props, Config>(defaultConfig, COMPONENT_NAME),
|
|
58
|
+
const props = withDefaults(defineProps<Props<TRow, TCol>>(), {
|
|
59
|
+
...getDefaults<Props<TRow, TCol>, Config>(defaultConfig, COMPONENT_NAME),
|
|
56
60
|
columns: () => [],
|
|
57
61
|
rows: () => [],
|
|
58
62
|
dateDivider: () => [],
|
|
59
63
|
selectedRows: () => [],
|
|
60
64
|
expandedRows: () => [],
|
|
65
|
+
skeletonWidths: () => defaultConfig.defaults.skeletonWidths,
|
|
61
66
|
});
|
|
62
67
|
|
|
63
68
|
const emit = defineEmits([
|
|
@@ -110,6 +115,8 @@ const emit = defineEmits([
|
|
|
110
115
|
"search",
|
|
111
116
|
]);
|
|
112
117
|
|
|
118
|
+
defineSlots<UTableSlots<TRow, TCol>>();
|
|
119
|
+
|
|
113
120
|
const slots = useSlots();
|
|
114
121
|
|
|
115
122
|
const selectAll = ref(false);
|
|
@@ -143,7 +150,7 @@ const sortedRows: ComputedRef<FlatRow[]> = computed(() => {
|
|
|
143
150
|
typeof column === "object" ? column.key : column,
|
|
144
151
|
);
|
|
145
152
|
|
|
146
|
-
const keyOrder = new Map(headerKeys.map((key, i) => [key, i]));
|
|
153
|
+
const keyOrder = new Map<string, number>(headerKeys.map((key, i) => [key, i]));
|
|
147
154
|
|
|
148
155
|
return flatTableRows.value.map((row) => {
|
|
149
156
|
const entries = Object.entries(row);
|
|
@@ -169,6 +176,11 @@ const isFooterSticky = computed(() => {
|
|
|
169
176
|
|
|
170
177
|
const normalizedColumns = computed(() => normalizeColumns(props.columns));
|
|
171
178
|
|
|
179
|
+
/* Slot keys are narrowed to `TCol` for callers; internal dispatch is driven by runtime strings. */
|
|
180
|
+
function headerSlotName(key: string) {
|
|
181
|
+
return `header-${key}` as `header-${TCol}`;
|
|
182
|
+
}
|
|
183
|
+
|
|
172
184
|
const visibleColumns = computed(() => {
|
|
173
185
|
return normalizedColumns.value.filter((column) => column.isShown !== false);
|
|
174
186
|
});
|
|
@@ -206,9 +218,14 @@ const isCheckedMoreOneTableItems = computed(() => {
|
|
|
206
218
|
return Boolean(localSelectedRows.value.length);
|
|
207
219
|
});
|
|
208
220
|
|
|
209
|
-
|
|
221
|
+
/* Select-all includes nested children, which need not carry `TRow`'s members. */
|
|
222
|
+
const selectedRowsSlotValue = computed(() => localSelectedRows.value as SelectedSlotRow<TRow>[]);
|
|
223
|
+
|
|
224
|
+
const tableRowWidthStyle = computed(() => ({
|
|
225
|
+
width: `${tableWidth.value / PX_IN_REM}rem`,
|
|
226
|
+
}));
|
|
210
227
|
|
|
211
|
-
const flatTableRows = computed(() => getFlatRows(props.rows));
|
|
228
|
+
const flatTableRows = computed(() => getFlatRows(props.rows as Row[]));
|
|
212
229
|
|
|
213
230
|
const visibleFlatRows = computed(() => {
|
|
214
231
|
const expanded = expandedRowsSet.value;
|
|
@@ -216,6 +233,18 @@ const visibleFlatRows = computed(() => {
|
|
|
216
233
|
return flatTableRows.value.filter((row) => !row.parentRowId || expanded.has(row.parentRowId));
|
|
217
234
|
});
|
|
218
235
|
|
|
236
|
+
const showSkeletonLoading = computed(() => {
|
|
237
|
+
return props.skeletonLoading && !sortedRows.value.length;
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
const progressLoading = computed(() => {
|
|
241
|
+
return showSkeletonLoading.value ? false : props.loading;
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
const skeletonRowIndexes = computed(() => {
|
|
245
|
+
return Array.from({ length: props.skeletonRows }, (_, index) => index);
|
|
246
|
+
});
|
|
247
|
+
|
|
219
248
|
const virtualScroll = useVirtualScroll({
|
|
220
249
|
containerRef: tableWrapperRef,
|
|
221
250
|
totalCount: computed(() => (props.virtualScroll ? visibleFlatRows.value.length : 0)),
|
|
@@ -243,6 +272,10 @@ function isRowVisible(row: FlatRow): boolean {
|
|
|
243
272
|
return !row.parentRowId || expandedRowsSet.value.has(row.parentRowId);
|
|
244
273
|
}
|
|
245
274
|
|
|
275
|
+
function getSkeletonWidthClass(columnIndex: number, rowIndex: number): string {
|
|
276
|
+
return props.skeletonWidths[(columnIndex + rowIndex) % props.skeletonWidths.length];
|
|
277
|
+
}
|
|
278
|
+
|
|
246
279
|
const searchMatches = computed<SearchMatch[]>(() => {
|
|
247
280
|
const query = props.search?.toLowerCase();
|
|
248
281
|
|
|
@@ -512,7 +545,7 @@ onBeforeUnmount(() => {
|
|
|
512
545
|
|
|
513
546
|
function onChangeSelectedRows() {
|
|
514
547
|
if (!isEqual(props.selectedRows, localSelectedRows.value)) {
|
|
515
|
-
localSelectedRows.value = props.selectedRows;
|
|
548
|
+
localSelectedRows.value = props.selectedRows as Row[];
|
|
516
549
|
}
|
|
517
550
|
}
|
|
518
551
|
|
|
@@ -663,7 +696,9 @@ function getDateDividerData(rowDate: string | Date | undefined) {
|
|
|
663
696
|
function setFooterCellWidth(zero?: null) {
|
|
664
697
|
const ZERO_WIDTH = 0;
|
|
665
698
|
|
|
666
|
-
if (!props.stickyFooter || !footerRowRef.value || !stickyFooterRowRef.value)
|
|
699
|
+
if (!props.stickyFooter || !footerRowRef.value || !stickyFooterRowRef.value) {
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
667
702
|
|
|
668
703
|
const mainFooterItems = [...footerRowRef.value.children] as HTMLElement[];
|
|
669
704
|
const stickyFooterItems = [...stickyFooterRowRef.value.children] as HTMLElement[];
|
|
@@ -816,6 +851,50 @@ function onBodyDoubleClick(event: MouseEvent) {
|
|
|
816
851
|
onDoubleClickRow(rowData);
|
|
817
852
|
}
|
|
818
853
|
|
|
854
|
+
// Detect double-tap on touch devices and dispatch a native `dblclick`,
|
|
855
|
+
// which bubbles to the tbody and triggers onBodyDoubleClick.
|
|
856
|
+
const DOUBLE_TAP_DELAY = 300;
|
|
857
|
+
const DOUBLE_TAP_MAX_MOVE = 24;
|
|
858
|
+
|
|
859
|
+
let lastTapTime = 0;
|
|
860
|
+
let lastTapRow: Element | null = null;
|
|
861
|
+
let lastTapX = 0;
|
|
862
|
+
let lastTapY = 0;
|
|
863
|
+
|
|
864
|
+
function onBodyTouchEnd(event: TouchEvent) {
|
|
865
|
+
if (event.changedTouches.length !== 1) return;
|
|
866
|
+
|
|
867
|
+
const [touch] = event.changedTouches;
|
|
868
|
+
const target = touch.target as HTMLElement | null;
|
|
869
|
+
const row = target?.closest("tr[data-row-id]") ?? null;
|
|
870
|
+
|
|
871
|
+
if (!row) {
|
|
872
|
+
lastTapRow = null;
|
|
873
|
+
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
const now = event.timeStamp;
|
|
878
|
+
const isSameRow = row === lastTapRow;
|
|
879
|
+
const isQuick = now - lastTapTime < DOUBLE_TAP_DELAY;
|
|
880
|
+
const isNear =
|
|
881
|
+
Math.abs(touch.clientX - lastTapX) < DOUBLE_TAP_MAX_MOVE &&
|
|
882
|
+
Math.abs(touch.clientY - lastTapY) < DOUBLE_TAP_MAX_MOVE;
|
|
883
|
+
|
|
884
|
+
if (isSameRow && isQuick && isNear) {
|
|
885
|
+
lastTapTime = 0;
|
|
886
|
+
lastTapRow = null;
|
|
887
|
+
row.dispatchEvent(new MouseEvent("dblclick", { bubbles: true, cancelable: true }));
|
|
888
|
+
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
lastTapTime = now;
|
|
893
|
+
lastTapRow = row;
|
|
894
|
+
lastTapX = touch.clientX;
|
|
895
|
+
lastTapY = touch.clientY;
|
|
896
|
+
}
|
|
897
|
+
|
|
819
898
|
function onChangeSelectAll(selectAll: boolean) {
|
|
820
899
|
if (selectAll && canSelectAll.value) {
|
|
821
900
|
localSelectedRows.value = [...flatTableRows.value];
|
|
@@ -889,7 +968,7 @@ function getDateDividerConfig(row: Row, isSelected: boolean) {
|
|
|
889
968
|
: bodyDateDividerAttrs.value.config;
|
|
890
969
|
|
|
891
970
|
return getMergedConfig({
|
|
892
|
-
defaultConfig
|
|
971
|
+
defaultConfig,
|
|
893
972
|
globalConfig: getDateDividerData(row.rowDate).config,
|
|
894
973
|
}) as UDividerConfig;
|
|
895
974
|
}
|
|
@@ -917,7 +996,9 @@ function getRowSearchMatchColumns(row: FlatRow): Set<string> | undefined {
|
|
|
917
996
|
}
|
|
918
997
|
|
|
919
998
|
function getRowActiveSearchMatchColumn(row: FlatRow): string | undefined {
|
|
920
|
-
if (!activeMatch.value || activeMatch.value.rowId !== row.id)
|
|
999
|
+
if (!activeMatch.value || activeMatch.value.rowId !== row.id) {
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
921
1002
|
|
|
922
1003
|
return activeMatch.value.columnKey;
|
|
923
1004
|
}
|
|
@@ -1036,9 +1117,11 @@ const {
|
|
|
1036
1117
|
bodyCellSearchMatchTextAttrs,
|
|
1037
1118
|
bodyCellSearchMatchActiveAttrs,
|
|
1038
1119
|
bodyCellSearchMatchTextActiveAttrs,
|
|
1120
|
+
skeletonCellAttrs,
|
|
1121
|
+
skeletonCheckboxAttrs,
|
|
1039
1122
|
} = useUI<Config>(defaultConfig, mutatedProps);
|
|
1040
1123
|
|
|
1041
|
-
/* Plain object — inner refs are already reactive. */
|
|
1124
|
+
/* Plain object — inner refs are already reactive and identity-stable (see useUI). */
|
|
1042
1125
|
const tableRowAttrs = {
|
|
1043
1126
|
bodyCellContentAttrs,
|
|
1044
1127
|
bodyCellCheckboxAttrs,
|
|
@@ -1085,8 +1168,40 @@ function renderDateDividerRow(row: FlatRow, rowIndex: number): VNode | null {
|
|
|
1085
1168
|
]);
|
|
1086
1169
|
}
|
|
1087
1170
|
|
|
1171
|
+
/**
|
|
1172
|
+
* Per-row VNode memo cache. Toggling one checkbox invalidates the `selectedRowIds`
|
|
1173
|
+
* computed, which re-runs the body render function. Without memoization every row's
|
|
1174
|
+
* VNode would be rebuilt and diffed on each toggle (200+ rows → visible lag). We cache
|
|
1175
|
+
* each row's VNode keyed by row id and only rebuild it when an input that actually
|
|
1176
|
+
* affects that row changes — so an unrelated row keeps the same VNode reference and
|
|
1177
|
+
* Vue skips it entirely during patch.
|
|
1178
|
+
*/
|
|
1179
|
+
const rowVNodeCache = new Map<RowId, { signature: string; row: FlatRow; vnode: VNode }>();
|
|
1180
|
+
|
|
1181
|
+
function getRowSignature(row: FlatRow, rowIndex: number): string {
|
|
1182
|
+
return [
|
|
1183
|
+
rowIndex,
|
|
1184
|
+
Number(isRowSelected(row)),
|
|
1185
|
+
Number(expandedRowsSet.value.has(row.id)),
|
|
1186
|
+
Number(isRowVisible(row)),
|
|
1187
|
+
props.selectable ? 1 : 0,
|
|
1188
|
+
props.search || "",
|
|
1189
|
+
getRowActiveSearchMatchColumn(row) || "",
|
|
1190
|
+
[...(getRowSearchMatchColumns(row) || [])].join(","),
|
|
1191
|
+
].join("|");
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1088
1194
|
function renderTableRow(row: FlatRow, rowIndex: number): VNode {
|
|
1089
|
-
|
|
1195
|
+
const signature = getRowSignature(row, rowIndex);
|
|
1196
|
+
const cached = rowVNodeCache.get(row.id);
|
|
1197
|
+
|
|
1198
|
+
// `row` identity guards against stale data: `flatTableRows` yields fresh row
|
|
1199
|
+
// objects whenever `props.rows` changes, so a new reference means new cell data.
|
|
1200
|
+
if (cached && cached.row === row && cached.signature === signature) {
|
|
1201
|
+
return cached.vnode;
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
const vnode = h(
|
|
1090
1205
|
UTableRow,
|
|
1091
1206
|
{
|
|
1092
1207
|
key: row.id,
|
|
@@ -1112,13 +1227,48 @@ function renderTableRow(row: FlatRow, rowIndex: number): VNode {
|
|
|
1112
1227
|
} as unknown as UTableRowProps,
|
|
1113
1228
|
slots,
|
|
1114
1229
|
);
|
|
1230
|
+
|
|
1231
|
+
rowVNodeCache.set(row.id, { signature, row, vnode });
|
|
1232
|
+
|
|
1233
|
+
return vnode;
|
|
1115
1234
|
}
|
|
1116
1235
|
|
|
1236
|
+
/* Column layout / config changes affect every row — invalidate the whole cache. */
|
|
1237
|
+
watch(
|
|
1238
|
+
[
|
|
1239
|
+
normalizedColumns,
|
|
1240
|
+
config,
|
|
1241
|
+
columnPositions,
|
|
1242
|
+
() => props.textEllipsis,
|
|
1243
|
+
() => props.emptyCellLabel,
|
|
1244
|
+
],
|
|
1245
|
+
() => rowVNodeCache.clear(),
|
|
1246
|
+
);
|
|
1247
|
+
|
|
1248
|
+
/* Drop cache entries for rows that no longer exist (filters, pagination reset). */
|
|
1249
|
+
watch(flatTableRows, (rows) => {
|
|
1250
|
+
const liveIds = new Set(rows.map((row) => row.id));
|
|
1251
|
+
|
|
1252
|
+
for (const id of rowVNodeCache.keys()) {
|
|
1253
|
+
if (!liveIds.has(id)) rowVNodeCache.delete(id);
|
|
1254
|
+
}
|
|
1255
|
+
});
|
|
1256
|
+
|
|
1117
1257
|
function renderRowTemplate(row: FlatRow, rowIndex: number): VNode[] {
|
|
1118
1258
|
return [renderDateDividerRow(row, rowIndex), renderTableRow(row, rowIndex)].filter(
|
|
1119
1259
|
Boolean,
|
|
1120
1260
|
) as VNode[];
|
|
1121
1261
|
}
|
|
1262
|
+
|
|
1263
|
+
/**
|
|
1264
|
+
* Stable functional component for the body rows. Defined once so its type
|
|
1265
|
+
* identity never changes across parent re-renders — a previous inline `:is`
|
|
1266
|
+
* arrow created a new type on every render, forcing Vue to unmount and rebuild
|
|
1267
|
+
* the entire tbody (e.g. on a sticky-header toggle). Reading `renderedRows`
|
|
1268
|
+
* through the closure keeps it reactive while row keys drive reconciliation.
|
|
1269
|
+
*/
|
|
1270
|
+
const BodyRows = () =>
|
|
1271
|
+
renderedRows.value.map((row, rowIndex) => renderRowTemplate(row, rowIndex)).flat();
|
|
1122
1272
|
</script>
|
|
1123
1273
|
|
|
1124
1274
|
<template>
|
|
@@ -1163,7 +1313,7 @@ function renderRowTemplate(row: FlatRow, rowIndex: number): VNode[] {
|
|
|
1163
1313
|
@binding {object} column
|
|
1164
1314
|
@binding {number} index
|
|
1165
1315
|
-->
|
|
1166
|
-
<slot :name="
|
|
1316
|
+
<slot :name="headerSlotName(column.key)" :column="column" :index="index" />
|
|
1167
1317
|
</template>
|
|
1168
1318
|
|
|
1169
1319
|
<template v-else>
|
|
@@ -1171,7 +1321,7 @@ function renderRowTemplate(row: FlatRow, rowIndex: number): VNode[] {
|
|
|
1171
1321
|
</template>
|
|
1172
1322
|
</div>
|
|
1173
1323
|
|
|
1174
|
-
<ULoaderProgress :loading="
|
|
1324
|
+
<ULoaderProgress :loading="progressLoading" v-bind="stickyHeaderLoaderAttrs" />
|
|
1175
1325
|
</div>
|
|
1176
1326
|
|
|
1177
1327
|
<div
|
|
@@ -1201,9 +1351,9 @@ function renderRowTemplate(row: FlatRow, rowIndex: number): VNode[] {
|
|
|
1201
1351
|
@slot Use it to add action buttons within the actions header, which appear when rows are selected.
|
|
1202
1352
|
@binding {array} selected-rows
|
|
1203
1353
|
-->
|
|
1204
|
-
<slot name="header-actions" :selected-rows="
|
|
1354
|
+
<slot name="header-actions" :selected-rows="selectedRowsSlotValue" />
|
|
1205
1355
|
|
|
1206
|
-
<ULoaderProgress :loading="
|
|
1356
|
+
<ULoaderProgress :loading="progressLoading" v-bind="stickyHeaderLoaderAttrs" />
|
|
1207
1357
|
</div>
|
|
1208
1358
|
|
|
1209
1359
|
<div
|
|
@@ -1233,9 +1383,9 @@ function renderRowTemplate(row: FlatRow, rowIndex: number): VNode[] {
|
|
|
1233
1383
|
@slot Use it to add action buttons within the actions header, which appear when rows are selected.
|
|
1234
1384
|
@binding {array} selected-rows
|
|
1235
1385
|
-->
|
|
1236
|
-
<slot name="header-actions" :selected-rows="
|
|
1386
|
+
<slot name="header-actions" :selected-rows="selectedRowsSlotValue" />
|
|
1237
1387
|
|
|
1238
|
-
<ULoaderProgress :loading="
|
|
1388
|
+
<ULoaderProgress :loading="progressLoading" v-bind="stickyHeaderLoaderAttrs" />
|
|
1239
1389
|
</div>
|
|
1240
1390
|
|
|
1241
1391
|
<div ref="table-wrapper" v-bind="tableWrapperAttrs" @scroll="virtualScroll.onScroll">
|
|
@@ -1293,8 +1443,13 @@ function renderRowTemplate(row: FlatRow, rowIndex: number): VNode[] {
|
|
|
1293
1443
|
@binding {number} index
|
|
1294
1444
|
-->
|
|
1295
1445
|
<slot
|
|
1296
|
-
v-if="
|
|
1297
|
-
|
|
1446
|
+
v-if="
|
|
1447
|
+
hasSlotContent($slots[`header-${column.key}`], {
|
|
1448
|
+
column,
|
|
1449
|
+
index,
|
|
1450
|
+
})
|
|
1451
|
+
"
|
|
1452
|
+
:name="headerSlotName(column.key)"
|
|
1298
1453
|
:column="column"
|
|
1299
1454
|
:index="index"
|
|
1300
1455
|
/>
|
|
@@ -1305,7 +1460,7 @@ function renderRowTemplate(row: FlatRow, rowIndex: number): VNode[] {
|
|
|
1305
1460
|
</th>
|
|
1306
1461
|
</tr>
|
|
1307
1462
|
|
|
1308
|
-
<ULoaderProgress :loading="
|
|
1463
|
+
<ULoaderProgress :loading="progressLoading" v-bind="headerLoaderAttrs" />
|
|
1309
1464
|
</thead>
|
|
1310
1465
|
|
|
1311
1466
|
<tbody
|
|
@@ -1313,6 +1468,7 @@ function renderRowTemplate(row: FlatRow, rowIndex: number): VNode[] {
|
|
|
1313
1468
|
v-bind="bodyAttrs"
|
|
1314
1469
|
@click="onBodyClick"
|
|
1315
1470
|
@dblclick="onBodyDoubleClick"
|
|
1471
|
+
@touchend="onBodyTouchEnd"
|
|
1316
1472
|
>
|
|
1317
1473
|
<tr
|
|
1318
1474
|
v-if="hasBeforeFirstRowSlot"
|
|
@@ -1331,9 +1487,7 @@ function renderRowTemplate(row: FlatRow, rowIndex: number): VNode[] {
|
|
|
1331
1487
|
/>
|
|
1332
1488
|
</tr>
|
|
1333
1489
|
|
|
1334
|
-
<component
|
|
1335
|
-
:is="() => renderedRows.map((row, rowIndex) => renderRowTemplate(row, rowIndex)).flat()"
|
|
1336
|
-
/>
|
|
1490
|
+
<component :is="BodyRows" />
|
|
1337
1491
|
|
|
1338
1492
|
<tr v-if="props.virtualScroll && virtualScroll.bottomSpacerHeight.value > 0">
|
|
1339
1493
|
<td
|
|
@@ -1356,6 +1510,30 @@ function renderRowTemplate(row: FlatRow, rowIndex: number): VNode[] {
|
|
|
1356
1510
|
</tr>
|
|
1357
1511
|
</tbody>
|
|
1358
1512
|
|
|
1513
|
+
<tbody v-else-if="showSkeletonLoading" v-bind="bodyAttrs">
|
|
1514
|
+
<tr
|
|
1515
|
+
v-for="rowIndex in skeletonRowIndexes"
|
|
1516
|
+
:key="`skeleton-row-${rowIndex}`"
|
|
1517
|
+
v-bind="bodyRowAttrs"
|
|
1518
|
+
>
|
|
1519
|
+
<td v-if="selectable" v-bind="bodyCellCheckboxAttrs">
|
|
1520
|
+
<USkeleton v-bind="skeletonCheckboxAttrs" />
|
|
1521
|
+
</td>
|
|
1522
|
+
|
|
1523
|
+
<td
|
|
1524
|
+
v-for="(column, columnIndex) in visibleColumns"
|
|
1525
|
+
:key="`${column.key}-skeleton-${rowIndex}`"
|
|
1526
|
+
v-bind="bodyCellBaseAttrs"
|
|
1527
|
+
:style="getStickyColumnStyle(column)"
|
|
1528
|
+
>
|
|
1529
|
+
<USkeleton
|
|
1530
|
+
v-bind="skeletonCellAttrs"
|
|
1531
|
+
:class="getSkeletonWidthClass(columnIndex, rowIndex)"
|
|
1532
|
+
/>
|
|
1533
|
+
</td>
|
|
1534
|
+
</tr>
|
|
1535
|
+
</tbody>
|
|
1536
|
+
|
|
1359
1537
|
<tbody v-else>
|
|
1360
1538
|
<tr>
|
|
1361
1539
|
<td :colspan="colsCount" v-bind="bodyEmptyStateCellAttrs">
|
|
@@ -400,6 +400,7 @@ function renderNestedFirstCell(value: Cell, key: string, cellIndex: number): VNo
|
|
|
400
400
|
|
|
401
401
|
function renderTableCell(value: Cell, key: string, index: number): VNode {
|
|
402
402
|
const keyStr = String(key);
|
|
403
|
+
const column = props.columns.find((col) => col.key === key) ?? props.columns[index];
|
|
403
404
|
|
|
404
405
|
const nestedCellNode = isNestedFirstCell(index)
|
|
405
406
|
? renderNestedFirstCell(value, key, index)
|
|
@@ -414,12 +415,12 @@ function renderTableCell(value: Cell, key: string, index: number): VNode {
|
|
|
414
415
|
...props.attrs.bodyCellBaseAttrs.value,
|
|
415
416
|
class: cx([
|
|
416
417
|
props.attrs.bodyCellBaseAttrs.value.class,
|
|
417
|
-
|
|
418
|
+
column.tdClass,
|
|
418
419
|
getCellClasses(props.row, keyStr),
|
|
419
|
-
getStickyColumnClass(
|
|
420
|
+
getStickyColumnClass(column),
|
|
420
421
|
getSearchMatchCellClass(keyStr),
|
|
421
422
|
]),
|
|
422
|
-
style: getStickyColumnStyle(
|
|
423
|
+
style: getStickyColumnStyle(column),
|
|
423
424
|
"data-cell-key": key,
|
|
424
425
|
"data-test": getDataTest(`${key}-cell`),
|
|
425
426
|
},
|
package/ui.data-table/config.ts
CHANGED
|
@@ -13,7 +13,11 @@ export default /*tw*/ {
|
|
|
13
13
|
},
|
|
14
14
|
compoundVariants: [
|
|
15
15
|
{ stickedHeader: true, actionsHeader: true, class: "rounded-none" },
|
|
16
|
-
{
|
|
16
|
+
{
|
|
17
|
+
stickedHeader: true,
|
|
18
|
+
actionsHeader: false,
|
|
19
|
+
class: "border-muted bg-default",
|
|
20
|
+
},
|
|
17
21
|
],
|
|
18
22
|
},
|
|
19
23
|
stickyHeaderCell: "{>headerCellBase} flex-none whitespace-nowrap",
|
|
@@ -41,7 +45,7 @@ export default /*tw*/ {
|
|
|
41
45
|
base: "min-w-full border-none text-medium w-auto table-auto border-separate border-spacing-0",
|
|
42
46
|
variants: {
|
|
43
47
|
virtualScroll: {
|
|
44
|
-
true: "table-
|
|
48
|
+
true: "table-fixed w-full",
|
|
45
49
|
},
|
|
46
50
|
},
|
|
47
51
|
},
|
|
@@ -111,6 +115,8 @@ export default /*tw*/ {
|
|
|
111
115
|
},
|
|
112
116
|
bodyEmptyState: "{UEmpty} my-8",
|
|
113
117
|
bodyEmptyStateCell: "",
|
|
118
|
+
skeletonCell: "{USkeleton} h-4 rounded-small",
|
|
119
|
+
skeletonCheckbox: "{USkeleton} size-4 rounded-small",
|
|
114
120
|
footer: {
|
|
115
121
|
base: "group/footer border-t border-solid border-muted",
|
|
116
122
|
variants: {
|
|
@@ -147,6 +153,9 @@ export default /*tw*/ {
|
|
|
147
153
|
stickyHeader: false,
|
|
148
154
|
stickyFooter: false,
|
|
149
155
|
loading: false,
|
|
156
|
+
skeletonLoading: false,
|
|
157
|
+
skeletonRows: 5,
|
|
158
|
+
skeletonWidths: ["w-11/12", "w-4/5", "w-3/4", "w-2/3", "w-1/2"],
|
|
150
159
|
textEllipsis: false,
|
|
151
160
|
search: "",
|
|
152
161
|
searchMatch: -1,
|
|
@@ -12,7 +12,7 @@ import { getRandomId } from "../../utils/helper";
|
|
|
12
12
|
|
|
13
13
|
import defaultConfig from "../config";
|
|
14
14
|
|
|
15
|
-
import
|
|
15
|
+
import UTableComponent from "../UTable.vue";
|
|
16
16
|
import UButton from "../../ui.button/UButton.vue";
|
|
17
17
|
import ULink from "../../ui.button-link/ULink.vue";
|
|
18
18
|
import UNumber from "../../ui.text-number/UNumber.vue";
|
|
@@ -27,6 +27,9 @@ import tooltip from "../../v.tooltip/vTooltip";
|
|
|
27
27
|
import type { Row, Props, ColumnObject } from "../types";
|
|
28
28
|
import { StickySide } from "../types";
|
|
29
29
|
|
|
30
|
+
/* Generic SFCs are typed as a function, which drops the compiler-generated `__name`. */
|
|
31
|
+
const UTable = UTableComponent as typeof UTableComponent & { __name: string };
|
|
32
|
+
|
|
30
33
|
interface UTableArgs extends Props {
|
|
31
34
|
slotTemplate?: string;
|
|
32
35
|
enum: "size";
|
|
@@ -182,6 +185,36 @@ Loading.parameters = {
|
|
|
182
185
|
},
|
|
183
186
|
};
|
|
184
187
|
|
|
188
|
+
export const SkeletonLoading = DefaultTemplate.bind({});
|
|
189
|
+
SkeletonLoading.args = {
|
|
190
|
+
rows: [],
|
|
191
|
+
skeletonLoading: true,
|
|
192
|
+
};
|
|
193
|
+
SkeletonLoading.parameters = {
|
|
194
|
+
docs: {
|
|
195
|
+
description: {
|
|
196
|
+
story:
|
|
197
|
+
"Use `skeletonLoading` while the first request is pending. The table keeps its structure " +
|
|
198
|
+
"and renders cell skeletons instead of the empty state.",
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
export const SkeletonLoadingConfigured = DefaultTemplate.bind({});
|
|
204
|
+
SkeletonLoadingConfigured.args = {
|
|
205
|
+
rows: [],
|
|
206
|
+
skeletonLoading: true,
|
|
207
|
+
skeletonRows: 3,
|
|
208
|
+
skeletonWidths: ["w-11/12", "w-2/3", "w-1/2"],
|
|
209
|
+
};
|
|
210
|
+
SkeletonLoadingConfigured.parameters = {
|
|
211
|
+
docs: {
|
|
212
|
+
description: {
|
|
213
|
+
story: "Configure skeleton row count and cell widths for a single table with props.",
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
};
|
|
217
|
+
|
|
185
218
|
export const EmptyCellLabel = DefaultTemplate.bind({});
|
|
186
219
|
EmptyCellLabel.args = {
|
|
187
220
|
emptyCellLabel: "NO DATA",
|