vueless 1.3.9-beta.8 → 1.4.0
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/composables/useUI.ts +187 -155
- package/composables/useVirtualScroll.ts +100 -0
- package/package.json +2 -2
- package/types.ts +11 -0
- package/ui.button-link/ULink.vue +18 -2
- package/ui.data-table/UTable.vue +597 -198
- package/ui.data-table/UTableRow.vue +372 -168
- package/ui.data-table/config.ts +33 -5
- package/ui.data-table/storybook/stories.ts +163 -0
- package/ui.data-table/tests/UTable.test.ts +456 -20
- package/ui.data-table/tests/UTableRow.test.ts +35 -104
- package/ui.data-table/types.ts +57 -0
- package/ui.data-table/utilTable.ts +16 -10
- package/ui.form-checkbox/UCheckbox.vue +3 -7
- package/ui.text-notify/config.ts +1 -1
- package/ui.text-number/UNumber.vue +36 -2
- package/ui.text-number/config.ts +1 -0
- package/ui.text-number/storybook/stories.ts +11 -0
- package/ui.text-number/tests/UNumber.test.ts +90 -0
- package/ui.text-number/types.ts +5 -0
- package/utils/node/mergeConfigs.d.ts +2 -2
- package/utils/node/mergeConfigs.js +240 -121
- package/utils/node/vuelessConfig.d.ts +1 -1
package/ui.data-table/UTable.vue
CHANGED
|
@@ -1,35 +1,39 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import {
|
|
3
|
+
shallowRef,
|
|
3
4
|
ref,
|
|
4
5
|
computed,
|
|
5
6
|
watch,
|
|
7
|
+
watchEffect,
|
|
6
8
|
useSlots,
|
|
7
9
|
nextTick,
|
|
8
10
|
onMounted,
|
|
9
|
-
onUpdated,
|
|
10
11
|
onBeforeUnmount,
|
|
11
12
|
useTemplateRef,
|
|
13
|
+
h,
|
|
12
14
|
} from "vue";
|
|
13
15
|
import { isEqual } from "lodash-es";
|
|
14
16
|
|
|
15
|
-
import UEmpty from "../ui.container-empty/UEmpty.vue";
|
|
16
|
-
import UCheckbox from "../ui.form-checkbox/UCheckbox.vue";
|
|
17
|
-
import ULoaderProgress from "../ui.loader-progress/ULoaderProgress.vue";
|
|
18
|
-
import UTableRow from "./UTableRow.vue";
|
|
19
|
-
import UDivider from "../ui.container-divider/UDivider.vue";
|
|
20
|
-
|
|
21
17
|
import { useUI } from "../composables/useUI";
|
|
18
|
+
import { useVirtualScroll } from "../composables/useVirtualScroll";
|
|
19
|
+
import { useComponentLocaleMessages } from "../composables/useComponentLocaleMassages";
|
|
22
20
|
import { getDefaults, cx, getMergedConfig } from "../utils/ui";
|
|
23
21
|
import { hasSlotContent } from "../utils/helper";
|
|
24
|
-
import { useComponentLocaleMessages } from "../composables/useComponentLocaleMassages";
|
|
25
|
-
|
|
26
|
-
import defaultConfig from "./config";
|
|
27
|
-
import { normalizeColumns, mapRowColumns, getFlatRows, getRowChildrenIds } from "./utilTable";
|
|
28
22
|
|
|
29
23
|
import { PX_IN_REM } from "../constants";
|
|
30
24
|
import { COMPONENT_NAME } from "./constants";
|
|
31
25
|
|
|
32
|
-
import
|
|
26
|
+
import defaultConfig from "./config";
|
|
27
|
+
import { normalizeColumns, getFlatRows, getRowChildrenIds } from "./utilTable";
|
|
28
|
+
import { StickySide } from "./types";
|
|
29
|
+
|
|
30
|
+
import UEmpty from "../ui.container-empty/UEmpty.vue";
|
|
31
|
+
import UCheckbox from "../ui.form-checkbox/UCheckbox.vue";
|
|
32
|
+
import ULoaderProgress from "../ui.loader-progress/ULoaderProgress.vue";
|
|
33
|
+
import UDivider from "../ui.container-divider/UDivider.vue";
|
|
34
|
+
import UTableRow from "./UTableRow.vue";
|
|
35
|
+
|
|
36
|
+
import type { ComputedRef, VNode } from "vue";
|
|
33
37
|
import type { Config as UDividerConfig } from "../ui.container-divider/types";
|
|
34
38
|
import type {
|
|
35
39
|
Cell,
|
|
@@ -41,8 +45,9 @@ import type {
|
|
|
41
45
|
DateDivider,
|
|
42
46
|
FlatRow,
|
|
43
47
|
ColumnObject,
|
|
48
|
+
SearchMatch,
|
|
49
|
+
UTableRowProps,
|
|
44
50
|
} from "./types";
|
|
45
|
-
import { StickySide } from "./types";
|
|
46
51
|
|
|
47
52
|
defineOptions({ inheritAttrs: false });
|
|
48
53
|
|
|
@@ -75,13 +80,13 @@ const emit = defineEmits([
|
|
|
75
80
|
"clickCell",
|
|
76
81
|
|
|
77
82
|
/**
|
|
78
|
-
*
|
|
83
|
+
* Triggers when row expanded.
|
|
79
84
|
* @property {object} row
|
|
80
85
|
*/
|
|
81
86
|
"row-expand",
|
|
82
87
|
|
|
83
88
|
/**
|
|
84
|
-
*
|
|
89
|
+
* Triggers when row collapsed.
|
|
85
90
|
* @property {object} row
|
|
86
91
|
*/
|
|
87
92
|
"row-collapse",
|
|
@@ -97,6 +102,12 @@ const emit = defineEmits([
|
|
|
97
102
|
* @property {array} rowId
|
|
98
103
|
*/
|
|
99
104
|
"update:expandedRows",
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Triggers when search matches are found.
|
|
108
|
+
* @property {number} totalMatches
|
|
109
|
+
*/
|
|
110
|
+
"search",
|
|
100
111
|
]);
|
|
101
112
|
|
|
102
113
|
const slots = useSlots();
|
|
@@ -122,35 +133,29 @@ const { localeMessages } = useComponentLocaleMessages<typeof defaultConfig.i18n>
|
|
|
122
133
|
props?.config?.i18n,
|
|
123
134
|
);
|
|
124
135
|
|
|
125
|
-
const localSelectedRows =
|
|
126
|
-
const localExpandedRows =
|
|
136
|
+
const localSelectedRows = shallowRef<Row[]>([]);
|
|
137
|
+
const localExpandedRows = shallowRef<RowId[]>([]);
|
|
138
|
+
|
|
139
|
+
const expandedRowsSet = computed(() => new Set(localExpandedRows.value));
|
|
127
140
|
|
|
128
141
|
const sortedRows: ComputedRef<FlatRow[]> = computed(() => {
|
|
129
142
|
const headerKeys = props.columns.map((column) =>
|
|
130
143
|
typeof column === "object" ? column.key : column,
|
|
131
144
|
);
|
|
132
145
|
|
|
133
|
-
|
|
134
|
-
const rowEntries = Object.entries(row);
|
|
135
|
-
|
|
136
|
-
const sortedEntries: typeof rowEntries = new Array(rowEntries.length);
|
|
137
|
-
|
|
138
|
-
rowEntries.forEach((entry) => {
|
|
139
|
-
const [key] = entry;
|
|
140
|
-
const headerIndex = headerKeys.indexOf(key);
|
|
146
|
+
const keyOrder = new Map(headerKeys.map((key, i) => [key, i]));
|
|
141
147
|
|
|
142
|
-
|
|
143
|
-
|
|
148
|
+
return flatTableRows.value.map((row) => {
|
|
149
|
+
const entries = Object.entries(row);
|
|
144
150
|
|
|
145
|
-
|
|
146
|
-
|
|
151
|
+
entries.sort((a, b) => {
|
|
152
|
+
const aIdx = keyOrder.get(a[0]) ?? Infinity;
|
|
153
|
+
const bIdx = keyOrder.get(b[0]) ?? Infinity;
|
|
147
154
|
|
|
148
|
-
|
|
155
|
+
return aIdx - bIdx;
|
|
149
156
|
});
|
|
150
157
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
return sortedRow as FlatRow;
|
|
158
|
+
return Object.fromEntries(entries) as FlatRow;
|
|
154
159
|
});
|
|
155
160
|
});
|
|
156
161
|
|
|
@@ -168,7 +173,7 @@ const visibleColumns = computed(() => {
|
|
|
168
173
|
return normalizedColumns.value.filter((column) => column.isShown !== false);
|
|
169
174
|
});
|
|
170
175
|
|
|
171
|
-
const columnPositions =
|
|
176
|
+
const columnPositions = shallowRef<Map<string, number>>(new Map());
|
|
172
177
|
|
|
173
178
|
const colsCount = computed(() => {
|
|
174
179
|
return normalizedColumns.value.length + 1;
|
|
@@ -183,11 +188,10 @@ const isShownActionsHeader = computed(() => {
|
|
|
183
188
|
return hasSelectedRows && hasHeaderActions;
|
|
184
189
|
});
|
|
185
190
|
|
|
186
|
-
const
|
|
187
|
-
const positionForFixHeader =
|
|
188
|
-
Number(headerRowRef.value?.getBoundingClientRect()?.top) + Number(window?.scrollY) || 0;
|
|
191
|
+
const headerOffsetTop = ref(0);
|
|
189
192
|
|
|
190
|
-
|
|
193
|
+
const isHeaderSticky = computed(() => {
|
|
194
|
+
return headerOffsetTop.value <= pagePositionY.value && props.stickyHeader;
|
|
191
195
|
});
|
|
192
196
|
|
|
193
197
|
const isShownFooterPosition = computed(() => {
|
|
@@ -206,53 +210,304 @@ const tableRowWidthStyle = computed(() => ({ width: `${tableWidth.value / PX_IN_
|
|
|
206
210
|
|
|
207
211
|
const flatTableRows = computed(() => getFlatRows(props.rows));
|
|
208
212
|
|
|
213
|
+
const visibleFlatRows = computed(() => {
|
|
214
|
+
const expanded = expandedRowsSet.value;
|
|
215
|
+
|
|
216
|
+
return flatTableRows.value.filter((row) => !row.parentRowId || expanded.has(row.parentRowId));
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
const virtualScroll = useVirtualScroll({
|
|
220
|
+
containerRef: tableWrapperRef,
|
|
221
|
+
totalCount: computed(() => (props.virtualScroll ? visibleFlatRows.value.length : 0)),
|
|
222
|
+
rowHeight: props.rowHeight,
|
|
223
|
+
bufferSize: props.bufferSize,
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
const renderedRows = computed(() => {
|
|
227
|
+
if (props.virtualScroll) {
|
|
228
|
+
// For virtual scroll, we still need to slice based on visible rows
|
|
229
|
+
// but we need to map back to the actual rows from flatTableRows
|
|
230
|
+
const visibleSlice = visibleFlatRows.value.slice(
|
|
231
|
+
virtualScroll.startIndex.value,
|
|
232
|
+
virtualScroll.endIndex.value,
|
|
233
|
+
);
|
|
234
|
+
const visibleIds = new Set(visibleSlice.map((row) => row.id));
|
|
235
|
+
|
|
236
|
+
return flatTableRows.value.filter((row) => visibleIds.has(row.id));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return flatTableRows.value;
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
function isRowVisible(row: FlatRow): boolean {
|
|
243
|
+
return !row.parentRowId || expandedRowsSet.value.has(row.parentRowId);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const searchMatches = computed<SearchMatch[]>(() => {
|
|
247
|
+
const query = props.search?.toLowerCase();
|
|
248
|
+
|
|
249
|
+
if (!query) return [];
|
|
250
|
+
|
|
251
|
+
const matches: SearchMatch[] = [];
|
|
252
|
+
const columns = visibleColumns.value;
|
|
253
|
+
const rows = visibleFlatRows.value;
|
|
254
|
+
|
|
255
|
+
for (let i = 0; i < rows.length; i++) {
|
|
256
|
+
const row = rows[i];
|
|
257
|
+
|
|
258
|
+
for (let j = 0; j < columns.length; j++) {
|
|
259
|
+
const key = columns[j].key;
|
|
260
|
+
const cellValue = row[key];
|
|
261
|
+
const text = getCellTextValue(cellValue);
|
|
262
|
+
|
|
263
|
+
if (!text) continue;
|
|
264
|
+
|
|
265
|
+
const lowerText = text.toLowerCase();
|
|
266
|
+
const indices: number[] = [];
|
|
267
|
+
let pos = 0;
|
|
268
|
+
|
|
269
|
+
while ((pos = lowerText.indexOf(query, pos)) !== -1) {
|
|
270
|
+
indices.push(pos);
|
|
271
|
+
pos += query.length;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (indices.length) {
|
|
275
|
+
matches.push({ rowId: row.id, columnKey: key, indices });
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return matches;
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
const searchMatchColumnSets = computed(() => {
|
|
284
|
+
const map = new Map<RowId, Set<string>>();
|
|
285
|
+
|
|
286
|
+
for (const match of searchMatches.value) {
|
|
287
|
+
let set = map.get(match.rowId);
|
|
288
|
+
|
|
289
|
+
if (!set) {
|
|
290
|
+
set = new Set();
|
|
291
|
+
map.set(match.rowId, set);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
set.add(match.columnKey);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return map;
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
const activeMatch = computed(() => {
|
|
301
|
+
const idx = props.searchMatch;
|
|
302
|
+
|
|
303
|
+
if (idx === undefined || idx < 0 || !searchMatches.value.length) return null;
|
|
304
|
+
|
|
305
|
+
let globalIndex = 0;
|
|
306
|
+
|
|
307
|
+
for (const match of searchMatches.value) {
|
|
308
|
+
if (globalIndex + match.indices.length > idx) {
|
|
309
|
+
return {
|
|
310
|
+
rowId: match.rowId,
|
|
311
|
+
columnKey: match.columnKey,
|
|
312
|
+
charIndex: match.indices[idx - globalIndex],
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
globalIndex += match.indices.length;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return null;
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
const totalSearchMatches = computed(() => {
|
|
323
|
+
let count = 0;
|
|
324
|
+
|
|
325
|
+
for (const match of searchMatches.value) {
|
|
326
|
+
count += match.indices.length;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return count;
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
const selectedRowIds = computed(() => {
|
|
333
|
+
return new Set(localSelectedRows.value.map((row) => row.id));
|
|
334
|
+
});
|
|
335
|
+
|
|
209
336
|
const isSelectedAllRows = computed(() => {
|
|
210
337
|
return localSelectedRows.value.length === flatTableRows.value.length;
|
|
211
338
|
});
|
|
212
339
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
340
|
+
// Conditional watchers - only create listeners when their associated features are enabled
|
|
341
|
+
// Using watchEffect to dynamically create/destroy watchers based on feature state
|
|
342
|
+
|
|
343
|
+
// Search-related watchers
|
|
344
|
+
// Note: totalSearchMatches watcher always runs to emit events (even when search is cleared)
|
|
345
|
+
watch(totalSearchMatches, (count) => emit("search", count), { flush: "post" });
|
|
346
|
+
|
|
347
|
+
// activeMatch watcher - only created when search is active
|
|
348
|
+
let stopActiveMatchWatch: (() => void) | null = null;
|
|
349
|
+
|
|
350
|
+
watchEffect(() => {
|
|
351
|
+
if (props.search) {
|
|
352
|
+
// Create watcher if it doesn't exist
|
|
353
|
+
if (!stopActiveMatchWatch) {
|
|
354
|
+
stopActiveMatchWatch = watch(
|
|
355
|
+
activeMatch,
|
|
356
|
+
(match) => {
|
|
357
|
+
if (!match) return;
|
|
358
|
+
|
|
359
|
+
if (props.virtualScroll) {
|
|
360
|
+
const rowIndex = visibleFlatRows.value.findIndex((row) => row.id === match.rowId);
|
|
361
|
+
|
|
362
|
+
if (rowIndex === -1) return;
|
|
363
|
+
|
|
364
|
+
virtualScroll.scrollToIndex(rowIndex);
|
|
365
|
+
} else {
|
|
366
|
+
scrollToRow(match.rowId);
|
|
367
|
+
}
|
|
368
|
+
},
|
|
369
|
+
{ flush: "post" },
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
} else {
|
|
373
|
+
// Cleanup watcher if it exists
|
|
374
|
+
if (stopActiveMatchWatch) {
|
|
375
|
+
stopActiveMatchWatch();
|
|
376
|
+
stopActiveMatchWatch = null;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
});
|
|
227
380
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
381
|
+
// Selection-related watchers (only created when selectable is enabled)
|
|
382
|
+
let stopLocalSelectedRowsWatch: (() => void) | null = null;
|
|
383
|
+
let stopSelectedRowsWatch: (() => void) | null = null;
|
|
384
|
+
let stopSelectAllWatch: (() => void) | null = null;
|
|
385
|
+
|
|
386
|
+
watchEffect(() => {
|
|
387
|
+
if (props.selectable) {
|
|
388
|
+
// Create watchers if they don't exist
|
|
389
|
+
if (!stopLocalSelectedRowsWatch) {
|
|
390
|
+
stopLocalSelectedRowsWatch = watch(localSelectedRows, onChangeLocalSelectedRows);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (!stopSelectedRowsWatch) {
|
|
394
|
+
stopSelectedRowsWatch = watch(() => props.selectedRows, onChangeSelectedRows, {
|
|
395
|
+
immediate: true,
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (!stopSelectAllWatch) {
|
|
400
|
+
stopSelectAllWatch = watch(selectAll, onChangeSelectAll);
|
|
401
|
+
}
|
|
402
|
+
} else {
|
|
403
|
+
// Cleanup watchers if they exist
|
|
404
|
+
if (stopLocalSelectedRowsWatch) {
|
|
405
|
+
stopLocalSelectedRowsWatch();
|
|
406
|
+
stopLocalSelectedRowsWatch = null;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (stopSelectedRowsWatch) {
|
|
410
|
+
stopSelectedRowsWatch();
|
|
411
|
+
stopSelectedRowsWatch = null;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (stopSelectAllWatch) {
|
|
415
|
+
stopSelectAllWatch();
|
|
416
|
+
stopSelectAllWatch = null;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
// Expansion watcher (always register as it's a core feature)
|
|
422
|
+
watch(() => props.expandedRows, onChangeExpandedRows, { immediate: true });
|
|
423
|
+
|
|
424
|
+
// Sticky header watcher (only created when stickyHeader is enabled)
|
|
425
|
+
let stopHeaderStickyWatch: (() => void) | null = null;
|
|
426
|
+
|
|
427
|
+
watchEffect(() => {
|
|
428
|
+
if (props.stickyHeader) {
|
|
429
|
+
// Create watcher if it doesn't exist
|
|
430
|
+
if (!stopHeaderStickyWatch) {
|
|
431
|
+
stopHeaderStickyWatch = watch(isHeaderSticky, setHeaderCellWidth);
|
|
432
|
+
}
|
|
433
|
+
} else {
|
|
434
|
+
// Cleanup watcher if it exists
|
|
435
|
+
if (stopHeaderStickyWatch) {
|
|
436
|
+
stopHeaderStickyWatch();
|
|
437
|
+
stopHeaderStickyWatch = null;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
// Sticky footer watcher (only created when stickyFooter is enabled)
|
|
443
|
+
let stopFooterStickyWatch: (() => void) | null = null;
|
|
444
|
+
|
|
445
|
+
watchEffect(() => {
|
|
446
|
+
if (props.stickyFooter) {
|
|
447
|
+
// Create watcher if it doesn't exist
|
|
448
|
+
if (!stopFooterStickyWatch) {
|
|
449
|
+
stopFooterStickyWatch = watch(isFooterSticky, (newValue) =>
|
|
450
|
+
newValue ? nextTick(setFooterCellWidth) : setFooterCellWidth(null),
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
} else {
|
|
454
|
+
// Cleanup watcher if it exists
|
|
455
|
+
if (stopFooterStickyWatch) {
|
|
456
|
+
stopFooterStickyWatch();
|
|
457
|
+
stopFooterStickyWatch = null;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
let resizeObserver: ResizeObserver | null = null;
|
|
463
|
+
let scrollRafId: number | null = null;
|
|
464
|
+
let resizeDebounceId: ReturnType<typeof setTimeout> | null = null;
|
|
465
|
+
|
|
466
|
+
function scheduleWindowResize(tableRectWidth: number, tableRectHeight: number) {
|
|
467
|
+
if (resizeDebounceId) clearTimeout(resizeDebounceId);
|
|
468
|
+
|
|
469
|
+
resizeDebounceId = setTimeout(() => {
|
|
470
|
+
resizeDebounceId = null;
|
|
471
|
+
onWindowResize();
|
|
472
|
+
|
|
473
|
+
tableHeight.value = tableRectHeight;
|
|
474
|
+
tableWidth.value = tableRectWidth;
|
|
475
|
+
}, 300);
|
|
476
|
+
}
|
|
236
477
|
|
|
237
478
|
onMounted(async () => {
|
|
238
479
|
document.addEventListener("keyup", onKeyupEsc);
|
|
239
480
|
document.addEventListener("scroll", onScroll, { passive: true });
|
|
240
|
-
window.addEventListener("resize", onWindowResize);
|
|
241
481
|
|
|
242
482
|
await nextTick();
|
|
483
|
+
updateHeaderOffsetTop();
|
|
243
484
|
calculateStickyColumnPositions();
|
|
244
|
-
});
|
|
245
485
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
486
|
+
if (tableWrapperRef.value) {
|
|
487
|
+
resizeObserver = new ResizeObserver((entries) => {
|
|
488
|
+
const entry = entries[0];
|
|
489
|
+
|
|
490
|
+
if (!entry) return;
|
|
491
|
+
|
|
492
|
+
scheduleWindowResize(entry.contentRect.width, entry.contentRect.height);
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
resizeObserver.observe(tableWrapperRef.value);
|
|
496
|
+
}
|
|
250
497
|
});
|
|
251
498
|
|
|
252
499
|
onBeforeUnmount(() => {
|
|
253
500
|
document.removeEventListener("keyup", onKeyupEsc);
|
|
254
501
|
document.removeEventListener("scroll", onScroll);
|
|
255
|
-
|
|
502
|
+
resizeObserver?.disconnect();
|
|
503
|
+
|
|
504
|
+
if (scrollRafId !== null) {
|
|
505
|
+
cancelAnimationFrame(scrollRafId);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
if (resizeDebounceId !== null) {
|
|
509
|
+
clearTimeout(resizeDebounceId);
|
|
510
|
+
}
|
|
256
511
|
});
|
|
257
512
|
|
|
258
513
|
function onChangeSelectedRows() {
|
|
@@ -268,13 +523,18 @@ function onChangeExpandedRows() {
|
|
|
268
523
|
}
|
|
269
524
|
|
|
270
525
|
function onWindowResize() {
|
|
271
|
-
|
|
272
|
-
|
|
526
|
+
updateHeaderOffsetTop();
|
|
273
527
|
setHeaderCellWidth();
|
|
274
528
|
setFooterCellWidth();
|
|
275
529
|
calculateStickyColumnPositions();
|
|
276
530
|
}
|
|
277
531
|
|
|
532
|
+
function updateHeaderOffsetTop() {
|
|
533
|
+
if (headerRowRef.value) {
|
|
534
|
+
headerOffsetTop.value = headerRowRef.value.getBoundingClientRect().top + window.scrollY;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
278
538
|
function calculateStickyColumnPositions() {
|
|
279
539
|
if (!headerRowRef.value) return;
|
|
280
540
|
|
|
@@ -437,7 +697,12 @@ function setHeaderCellWidth() {
|
|
|
437
697
|
}
|
|
438
698
|
|
|
439
699
|
function onScroll() {
|
|
440
|
-
|
|
700
|
+
if (scrollRafId !== null) return;
|
|
701
|
+
|
|
702
|
+
scrollRafId = requestAnimationFrame(() => {
|
|
703
|
+
pagePositionY.value = Number(window?.scrollY);
|
|
704
|
+
scrollRafId = null;
|
|
705
|
+
});
|
|
441
706
|
}
|
|
442
707
|
|
|
443
708
|
function onKeyupEsc(event: KeyboardEvent) {
|
|
@@ -472,6 +737,85 @@ function onClickCell(cell: Cell, row: Row, key: string | number) {
|
|
|
472
737
|
emit("clickCell", cell, row, key);
|
|
473
738
|
}
|
|
474
739
|
|
|
740
|
+
function onBodyClick(event: MouseEvent) {
|
|
741
|
+
const target = event.target as HTMLElement;
|
|
742
|
+
|
|
743
|
+
const row = target.closest("tr");
|
|
744
|
+
|
|
745
|
+
if (!row) return;
|
|
746
|
+
|
|
747
|
+
const rowId = row.getAttribute("data-row-id");
|
|
748
|
+
|
|
749
|
+
if (!rowId) return;
|
|
750
|
+
|
|
751
|
+
const rowData = flatTableRows.value.find((r) => String(r.id) === rowId);
|
|
752
|
+
|
|
753
|
+
if (!rowData) return;
|
|
754
|
+
|
|
755
|
+
// Handle checkbox toggle via event delegation.
|
|
756
|
+
// When unchecking, UCheckbox.onIconClick() calls input.click() which dispatches
|
|
757
|
+
// a second (programmatic) click event with target=INPUT and isTrusted=false.
|
|
758
|
+
// Without this guard, onToggleRowCheckbox fires twice (deselect then reselect).
|
|
759
|
+
const checkboxCell = target.closest("td[data-checkbox-id]");
|
|
760
|
+
|
|
761
|
+
if (checkboxCell) {
|
|
762
|
+
if (target.tagName === "INPUT" && !event.isTrusted) return;
|
|
763
|
+
|
|
764
|
+
onToggleRowCheckbox(rowData);
|
|
765
|
+
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// Handle expand icon toggle via event delegation
|
|
770
|
+
const expandIconElement = target.closest("[data-expand-icon]");
|
|
771
|
+
|
|
772
|
+
if (expandIconElement) {
|
|
773
|
+
onToggleExpand(rowData);
|
|
774
|
+
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// Handle row click via event delegation
|
|
779
|
+
onClickRow(rowData);
|
|
780
|
+
|
|
781
|
+
// Handle cell click via event delegation
|
|
782
|
+
const cell = target.closest("td");
|
|
783
|
+
|
|
784
|
+
if (cell) {
|
|
785
|
+
const cellKey = cell.getAttribute("data-cell-key");
|
|
786
|
+
|
|
787
|
+
if (cellKey) {
|
|
788
|
+
const cellValue = rowData[cellKey];
|
|
789
|
+
|
|
790
|
+
onClickCell(cellValue, rowData, cellKey);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
function onBodyDoubleClick(event: MouseEvent) {
|
|
796
|
+
const target = event.target as HTMLElement;
|
|
797
|
+
|
|
798
|
+
const row = target.closest("tr");
|
|
799
|
+
|
|
800
|
+
if (!row) return;
|
|
801
|
+
|
|
802
|
+
const rowId = row.getAttribute("data-row-id");
|
|
803
|
+
|
|
804
|
+
if (!rowId) return;
|
|
805
|
+
|
|
806
|
+
const rowData = flatTableRows.value.find((r) => String(r.id) === rowId);
|
|
807
|
+
|
|
808
|
+
if (!rowData) return;
|
|
809
|
+
|
|
810
|
+
const selection = window.getSelection();
|
|
811
|
+
|
|
812
|
+
if (selection) {
|
|
813
|
+
selection.removeAllRanges();
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
onDoubleClickRow(rowData);
|
|
817
|
+
}
|
|
818
|
+
|
|
475
819
|
function onChangeSelectAll(selectAll: boolean) {
|
|
476
820
|
if (selectAll && canSelectAll.value) {
|
|
477
821
|
localSelectedRows.value = [...flatTableRows.value];
|
|
@@ -491,6 +835,7 @@ function onChangeLocalSelectedRows(selectedRows: Row[]) {
|
|
|
491
835
|
nextTick(setHeaderCellWidth);
|
|
492
836
|
}
|
|
493
837
|
|
|
838
|
+
// Set flag to prevent selectAll watcher from triggering
|
|
494
839
|
selectAll.value = !!selectedRows.length;
|
|
495
840
|
|
|
496
841
|
emit("update:selectedRows", localSelectedRows.value);
|
|
@@ -501,15 +846,15 @@ function clearSelectedItems() {
|
|
|
501
846
|
}
|
|
502
847
|
|
|
503
848
|
function onToggleExpand(row: Row) {
|
|
504
|
-
const
|
|
849
|
+
const expanded = localExpandedRows.value;
|
|
505
850
|
|
|
506
|
-
if (
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
851
|
+
if (expandedRowsSet.value.has(row.id)) {
|
|
852
|
+
const idsToRemove = new Set([row.id, ...getRowChildrenIds(row)]);
|
|
853
|
+
|
|
854
|
+
localExpandedRows.value = expanded.filter((id) => !idsToRemove.has(id));
|
|
510
855
|
emit("row-collapse", row);
|
|
511
856
|
} else {
|
|
512
|
-
localExpandedRows.value
|
|
857
|
+
localExpandedRows.value = [...expanded, row.id];
|
|
513
858
|
emit("row-expand", row);
|
|
514
859
|
}
|
|
515
860
|
|
|
@@ -533,7 +878,9 @@ function isRowSelectedWithin(rowIndex: number) {
|
|
|
533
878
|
function onToggleRowCheckbox(row: Row) {
|
|
534
879
|
const targetIndex = localSelectedRows.value.findIndex((selectedRow) => selectedRow.id === row.id);
|
|
535
880
|
|
|
536
|
-
|
|
881
|
+
localSelectedRows.value = ~targetIndex
|
|
882
|
+
? localSelectedRows.value.filter((_, i) => i !== targetIndex)
|
|
883
|
+
: [...localSelectedRows.value, row];
|
|
537
884
|
}
|
|
538
885
|
|
|
539
886
|
function getDateDividerConfig(row: Row, isSelected: boolean) {
|
|
@@ -550,7 +897,46 @@ function getDateDividerConfig(row: Row, isSelected: boolean) {
|
|
|
550
897
|
function isRowSelected(row: Row | undefined) {
|
|
551
898
|
if (!row) return false;
|
|
552
899
|
|
|
553
|
-
return
|
|
900
|
+
return selectedRowIds.value.has(row.id);
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
function getCellTextValue(cellValue: unknown): string {
|
|
904
|
+
if (cellValue == null) return "";
|
|
905
|
+
|
|
906
|
+
if (typeof cellValue === "object" && "value" in (cellValue as Record<string, unknown>)) {
|
|
907
|
+
const val = (cellValue as Record<string, unknown>).value;
|
|
908
|
+
|
|
909
|
+
return val != null ? String(val) : "";
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
return String(cellValue);
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
function getRowSearchMatchColumns(row: FlatRow): Set<string> | undefined {
|
|
916
|
+
return searchMatchColumnSets.value.get(row.id);
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
function getRowActiveSearchMatchColumn(row: FlatRow): string | undefined {
|
|
920
|
+
if (!activeMatch.value || activeMatch.value.rowId !== row.id) return undefined;
|
|
921
|
+
|
|
922
|
+
return activeMatch.value.columnKey;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
function scrollToRow(rowId: RowId) {
|
|
926
|
+
if (!tableWrapperRef.value) return;
|
|
927
|
+
|
|
928
|
+
const targetRow = tableWrapperRef.value.querySelector<HTMLTableRowElement>(
|
|
929
|
+
`tr[data-row-id="${rowId}"]`,
|
|
930
|
+
);
|
|
931
|
+
|
|
932
|
+
if (!targetRow) return;
|
|
933
|
+
|
|
934
|
+
const containerRect = tableWrapperRef.value.getBoundingClientRect();
|
|
935
|
+
const rowRect = targetRow.getBoundingClientRect();
|
|
936
|
+
|
|
937
|
+
if (rowRect.top < containerRect.top || rowRect.bottom > containerRect.bottom) {
|
|
938
|
+
targetRow.scrollIntoView({ block: "center" });
|
|
939
|
+
}
|
|
554
940
|
}
|
|
555
941
|
|
|
556
942
|
defineExpose({
|
|
@@ -567,6 +953,23 @@ defineExpose({
|
|
|
567
953
|
wrapperRef,
|
|
568
954
|
});
|
|
569
955
|
|
|
956
|
+
/* Cached slot-content checks to avoid re-creating VNodes on every render. */
|
|
957
|
+
const hasBeforeHeaderSlot = computed(() => {
|
|
958
|
+
return hasSlotContent(slots["before-header"]);
|
|
959
|
+
});
|
|
960
|
+
|
|
961
|
+
const hasBeforeFirstRowSlot = computed(() => {
|
|
962
|
+
return hasSlotContent(slots["before-first-row"]);
|
|
963
|
+
});
|
|
964
|
+
|
|
965
|
+
const hasAfterLastRowSlot = computed(() => {
|
|
966
|
+
return hasSlotContent(slots["after-last-row"]);
|
|
967
|
+
});
|
|
968
|
+
|
|
969
|
+
const hasFooterSlot = computed(() => {
|
|
970
|
+
return hasSlotContent(slots["footer"]);
|
|
971
|
+
});
|
|
972
|
+
|
|
570
973
|
/**
|
|
571
974
|
* Get element / nested component attributes for each config token ✨
|
|
572
975
|
* Applies: `class`, `config`, redefined default `props` and dev `vl-...` attributes.
|
|
@@ -629,7 +1032,93 @@ const {
|
|
|
629
1032
|
headerCellStickyRightAttrs,
|
|
630
1033
|
bodyCellStickyLeftAttrs,
|
|
631
1034
|
bodyCellStickyRightAttrs,
|
|
1035
|
+
bodyCellSearchMatchAttrs,
|
|
1036
|
+
bodyCellSearchMatchTextAttrs,
|
|
1037
|
+
bodyCellSearchMatchActiveAttrs,
|
|
1038
|
+
bodyCellSearchMatchTextActiveAttrs,
|
|
632
1039
|
} = useUI<Config>(defaultConfig, mutatedProps);
|
|
1040
|
+
|
|
1041
|
+
/* Plain object — inner refs are already reactive. */
|
|
1042
|
+
const tableRowAttrs = {
|
|
1043
|
+
bodyCellContentAttrs,
|
|
1044
|
+
bodyCellCheckboxAttrs,
|
|
1045
|
+
bodyCheckboxAttrs,
|
|
1046
|
+
bodyCellNestedAttrs,
|
|
1047
|
+
bodyCellNestedExpandIconAttrs,
|
|
1048
|
+
bodyCellNestedCollapseIconAttrs,
|
|
1049
|
+
bodyCellBaseAttrs,
|
|
1050
|
+
bodyCellNestedIconWrapperAttrs,
|
|
1051
|
+
bodyRowCheckedAttrs,
|
|
1052
|
+
bodyRowAttrs,
|
|
1053
|
+
bodyCellStickyLeftAttrs,
|
|
1054
|
+
bodyCellStickyRightAttrs,
|
|
1055
|
+
bodyCellSearchMatchAttrs,
|
|
1056
|
+
bodyCellSearchMatchTextAttrs,
|
|
1057
|
+
bodyCellSearchMatchActiveAttrs,
|
|
1058
|
+
bodyCellSearchMatchTextActiveAttrs,
|
|
1059
|
+
} as unknown as UTableRowAttrs;
|
|
1060
|
+
|
|
1061
|
+
function renderDateDividerRow(row: FlatRow, rowIndex: number): VNode | null {
|
|
1062
|
+
if (!isShownDateDivider(rowIndex) || !row.rowDate) return null;
|
|
1063
|
+
|
|
1064
|
+
const isSelected = isRowSelectedWithin(rowIndex);
|
|
1065
|
+
|
|
1066
|
+
const propsDateDivider = isSelected
|
|
1067
|
+
? bodyRowCheckedDateDividerAttrs.value
|
|
1068
|
+
: bodyRowDateDividerAttrs.value;
|
|
1069
|
+
|
|
1070
|
+
const dividerNode = h(UDivider, {
|
|
1071
|
+
label: getDateDividerData(row.rowDate).label,
|
|
1072
|
+
...(isSelected ? bodySelectedDateDividerAttrs.value : bodyDateDividerAttrs.value),
|
|
1073
|
+
config: getDateDividerConfig(row, isSelected),
|
|
1074
|
+
});
|
|
1075
|
+
|
|
1076
|
+
return h("tr", { ...propsDateDivider, key: `date-divider-${row.id}` }, [
|
|
1077
|
+
h(
|
|
1078
|
+
"td",
|
|
1079
|
+
{
|
|
1080
|
+
...bodyCellDateDividerAttrs.value,
|
|
1081
|
+
colspan: colsCount.value,
|
|
1082
|
+
},
|
|
1083
|
+
[dividerNode],
|
|
1084
|
+
),
|
|
1085
|
+
]);
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
function renderTableRow(row: FlatRow, rowIndex: number): VNode {
|
|
1089
|
+
return h(
|
|
1090
|
+
UTableRow,
|
|
1091
|
+
{
|
|
1092
|
+
key: row.id,
|
|
1093
|
+
selectable: props.selectable,
|
|
1094
|
+
rowIndex,
|
|
1095
|
+
row,
|
|
1096
|
+
columns: normalizedColumns.value,
|
|
1097
|
+
config: config.value,
|
|
1098
|
+
attrs: tableRowAttrs as unknown as UTableRowAttrs,
|
|
1099
|
+
colsCount: colsCount.value,
|
|
1100
|
+
nestedLevel: Number(row.nestedLevel || 0),
|
|
1101
|
+
emptyCellLabel: props.emptyCellLabel,
|
|
1102
|
+
"data-test": getDataTest("row"),
|
|
1103
|
+
"data-row-id": row.id,
|
|
1104
|
+
isExpanded: expandedRowsSet.value.has(row.id),
|
|
1105
|
+
isChecked: isRowSelected(row),
|
|
1106
|
+
isVisible: isRowVisible(row),
|
|
1107
|
+
columnPositions: columnPositions.value,
|
|
1108
|
+
search: props.search,
|
|
1109
|
+
searchMatchColumns: getRowSearchMatchColumns(row),
|
|
1110
|
+
activeSearchMatchColumn: getRowActiveSearchMatchColumn(row),
|
|
1111
|
+
textEllipsis: props.textEllipsis,
|
|
1112
|
+
} as unknown as UTableRowProps,
|
|
1113
|
+
slots,
|
|
1114
|
+
);
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
function renderRowTemplate(row: FlatRow, rowIndex: number): VNode[] {
|
|
1118
|
+
return [renderDateDividerRow(row, rowIndex), renderTableRow(row, rowIndex)].filter(
|
|
1119
|
+
Boolean,
|
|
1120
|
+
) as VNode[];
|
|
1121
|
+
}
|
|
633
1122
|
</script>
|
|
634
1123
|
|
|
635
1124
|
<template>
|
|
@@ -670,10 +1159,10 @@ const {
|
|
|
670
1159
|
>
|
|
671
1160
|
<template v-if="hasSlotContent($slots[`header-${column.key}`], { column, index })">
|
|
672
1161
|
<!--
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
1162
|
+
@slot Use it to customize needed header cell.
|
|
1163
|
+
@binding {object} column
|
|
1164
|
+
@binding {number} index
|
|
1165
|
+
-->
|
|
677
1166
|
<slot :name="`header-${column.key}`" :column="column" :index="index" />
|
|
678
1167
|
</template>
|
|
679
1168
|
|
|
@@ -749,15 +1238,10 @@ const {
|
|
|
749
1238
|
<ULoaderProgress :loading="loading" v-bind="stickyHeaderLoaderAttrs" />
|
|
750
1239
|
</div>
|
|
751
1240
|
|
|
752
|
-
<div ref="table-wrapper" v-bind="tableWrapperAttrs">
|
|
1241
|
+
<div ref="table-wrapper" v-bind="tableWrapperAttrs" @scroll="virtualScroll.onScroll">
|
|
753
1242
|
<table v-bind="tableAttrs">
|
|
754
1243
|
<thead v-bind="headerAttrs" :style="tableRowWidthStyle">
|
|
755
|
-
<tr
|
|
756
|
-
v-if="
|
|
757
|
-
hasSlotContent($slots['before-header'], { colsCount, classes: headerRowAttrs.class })
|
|
758
|
-
"
|
|
759
|
-
v-bind="beforeHeaderRowAttrs"
|
|
760
|
-
>
|
|
1244
|
+
<tr v-if="hasBeforeHeaderSlot" v-bind="beforeHeaderRowAttrs">
|
|
761
1245
|
<!--
|
|
762
1246
|
@slot Use it to add something before header row.
|
|
763
1247
|
@binding {number} cols-count
|
|
@@ -824,9 +1308,14 @@ const {
|
|
|
824
1308
|
<ULoaderProgress :loading="loading" v-bind="headerLoaderAttrs" />
|
|
825
1309
|
</thead>
|
|
826
1310
|
|
|
827
|
-
<tbody
|
|
1311
|
+
<tbody
|
|
1312
|
+
v-if="sortedRows.length"
|
|
1313
|
+
v-bind="bodyAttrs"
|
|
1314
|
+
@click="onBodyClick"
|
|
1315
|
+
@dblclick="onBodyDoubleClick"
|
|
1316
|
+
>
|
|
828
1317
|
<tr
|
|
829
|
-
v-if="
|
|
1318
|
+
v-if="hasBeforeFirstRowSlot"
|
|
830
1319
|
v-bind="isRowSelected(sortedRows[0]) ? beforeBodyRowCheckedAttrs : beforeBodyRowAttrs"
|
|
831
1320
|
>
|
|
832
1321
|
<td :colspan="colsCount" v-bind="beforeBodyRowCellAttrs">
|
|
@@ -835,115 +1324,25 @@ const {
|
|
|
835
1324
|
</td>
|
|
836
1325
|
</tr>
|
|
837
1326
|
|
|
838
|
-
<
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
>
|
|
844
|
-
<tr
|
|
845
|
-
v-if="isShownDateDivider(rowIndex) && !isRowSelectedWithin(rowIndex) && row.rowDate"
|
|
846
|
-
v-bind="bodyRowDateDividerAttrs"
|
|
847
|
-
>
|
|
848
|
-
<td v-bind="bodyCellDateDividerAttrs" :colspan="colsCount">
|
|
849
|
-
<UDivider
|
|
850
|
-
:label="getDateDividerData(row.rowDate).label"
|
|
851
|
-
v-bind="bodyDateDividerAttrs"
|
|
852
|
-
:config="getDateDividerConfig(row, false)"
|
|
853
|
-
/>
|
|
854
|
-
</td>
|
|
855
|
-
</tr>
|
|
856
|
-
|
|
857
|
-
<tr
|
|
858
|
-
v-if="isShownDateDivider(rowIndex) && isRowSelectedWithin(rowIndex) && row.rowDate"
|
|
859
|
-
v-bind="bodyRowCheckedDateDividerAttrs"
|
|
860
|
-
>
|
|
861
|
-
<td v-bind="bodyCellDateDividerAttrs" :colspan="colsCount">
|
|
862
|
-
<UDivider
|
|
863
|
-
:label="getDateDividerData(row.rowDate).label"
|
|
864
|
-
v-bind="bodySelectedDateDividerAttrs"
|
|
865
|
-
:config="getDateDividerConfig(row, true)"
|
|
866
|
-
/>
|
|
867
|
-
</td>
|
|
868
|
-
</tr>
|
|
869
|
-
|
|
870
|
-
<UTableRow
|
|
871
|
-
:selectable="selectable"
|
|
872
|
-
:row="row"
|
|
873
|
-
:columns="normalizedColumns"
|
|
874
|
-
:config="config"
|
|
875
|
-
:attrs="tableRowAttrs as unknown as UTableRowAttrs"
|
|
876
|
-
:cols-count="colsCount"
|
|
877
|
-
:nested-level="Number(row.nestedLevel || 0)"
|
|
878
|
-
:empty-cell-label="emptyCellLabel"
|
|
879
|
-
:data-test="getDataTest('row')"
|
|
880
|
-
:is-expanded="localExpandedRows.includes(row.id)"
|
|
881
|
-
:is-checked="isRowSelected(row)"
|
|
882
|
-
:column-positions="columnPositions"
|
|
883
|
-
@click="onClickRow"
|
|
884
|
-
@dblclick="onDoubleClickRow"
|
|
885
|
-
@click-cell="onClickCell"
|
|
886
|
-
@toggle-expand="onToggleExpand"
|
|
887
|
-
@toggle-checkbox="onToggleRowCheckbox"
|
|
888
|
-
>
|
|
889
|
-
<template
|
|
890
|
-
v-for="(value, key, cellIndex) in mapRowColumns(row, normalizedColumns)"
|
|
891
|
-
:key="`${rowIndex}-${cellIndex}`"
|
|
892
|
-
#[`cell-${key}`]="{ value: cellValue, row: cellRow }"
|
|
893
|
-
>
|
|
894
|
-
<!--
|
|
895
|
-
@slot Use it to customize needed table cell.
|
|
896
|
-
@binding {string} value
|
|
897
|
-
@binding {object} row
|
|
898
|
-
@binding {number} index
|
|
899
|
-
@binding {number} cellIndex
|
|
900
|
-
-->
|
|
901
|
-
<slot
|
|
902
|
-
:name="`cell-${key}`"
|
|
903
|
-
:value="cellValue"
|
|
904
|
-
:row="cellRow"
|
|
905
|
-
:index="rowIndex"
|
|
906
|
-
:cell-index="cellIndex"
|
|
907
|
-
/>
|
|
908
|
-
</template>
|
|
1327
|
+
<tr v-if="props.virtualScroll && virtualScroll.topSpacerHeight.value">
|
|
1328
|
+
<td
|
|
1329
|
+
:colspan="colsCount"
|
|
1330
|
+
:style="{ height: `${virtualScroll.topSpacerHeight.value}px` }"
|
|
1331
|
+
/>
|
|
1332
|
+
</tr>
|
|
909
1333
|
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
@binding {object} row
|
|
914
|
-
@binding {boolean} expanded
|
|
915
|
-
@binding {number} index
|
|
916
|
-
-->
|
|
917
|
-
<slot name="expand" :index="rowIndex" :row="expandedRow" :expanded="expanded" />
|
|
918
|
-
</template>
|
|
1334
|
+
<component
|
|
1335
|
+
:is="() => renderedRows.map((row, rowIndex) => renderRowTemplate(row, rowIndex)).flat()"
|
|
1336
|
+
/>
|
|
919
1337
|
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
-->
|
|
927
|
-
<slot
|
|
928
|
-
v-if="row"
|
|
929
|
-
name="nested-row"
|
|
930
|
-
:index="rowIndex"
|
|
931
|
-
:row="row"
|
|
932
|
-
:nested-level="Number(row.nestedLevel || 0)"
|
|
933
|
-
/>
|
|
934
|
-
</template>
|
|
935
|
-
</UTableRow>
|
|
936
|
-
</template>
|
|
1338
|
+
<tr v-if="props.virtualScroll && virtualScroll.bottomSpacerHeight.value > 0">
|
|
1339
|
+
<td
|
|
1340
|
+
:colspan="colsCount"
|
|
1341
|
+
:style="{ height: `${virtualScroll.bottomSpacerHeight.value}px` }"
|
|
1342
|
+
/>
|
|
1343
|
+
</tr>
|
|
937
1344
|
|
|
938
|
-
<tr
|
|
939
|
-
v-if="
|
|
940
|
-
hasSlotContent($slots['after-last-row'], {
|
|
941
|
-
colsCount,
|
|
942
|
-
classes: bodyCellBaseAttrs.class,
|
|
943
|
-
})
|
|
944
|
-
"
|
|
945
|
-
v-bind="afterBodyRowAttrs"
|
|
946
|
-
>
|
|
1345
|
+
<tr v-if="hasAfterLastRowSlot" v-bind="afterBodyRowAttrs">
|
|
947
1346
|
<!--
|
|
948
1347
|
@slot Use it to add something after last row.
|
|
949
1348
|
@binding {number} cols-count
|
|
@@ -973,7 +1372,7 @@ const {
|
|
|
973
1372
|
</tr>
|
|
974
1373
|
</tbody>
|
|
975
1374
|
|
|
976
|
-
<tfoot v-if="
|
|
1375
|
+
<tfoot v-if="hasFooterSlot" v-bind="footerAttrs">
|
|
977
1376
|
<tr ref="footer-row" v-bind="footerRowAttrs">
|
|
978
1377
|
<td v-if="selectable" />
|
|
979
1378
|
|