vueless 1.3.9-beta.13 → 1.3.9-beta.14
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/package.json +1 -1
- package/ui.data-table/UTable.vue +343 -127
- package/ui.data-table/UTableRow.vue +224 -148
- package/ui.data-table/tests/UTable.test.ts +18 -6
- package/ui.data-table/tests/UTableRow.test.ts +9 -104
- package/ui.data-table/types.ts +2 -0
- 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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vueless",
|
|
3
|
-
"version": "1.3.9-beta.
|
|
3
|
+
"version": "1.3.9-beta.14",
|
|
4
4
|
"description": "Vue Styleless UI Component Library, powered by Tailwind CSS.",
|
|
5
5
|
"author": "Johnny Grid <hello@vueless.com> (https://vueless.com)",
|
|
6
6
|
"homepage": "https://vueless.com",
|
package/ui.data-table/UTable.vue
CHANGED
|
@@ -4,11 +4,13 @@ import {
|
|
|
4
4
|
ref,
|
|
5
5
|
computed,
|
|
6
6
|
watch,
|
|
7
|
+
watchEffect,
|
|
7
8
|
useSlots,
|
|
8
9
|
nextTick,
|
|
9
10
|
onMounted,
|
|
10
11
|
onBeforeUnmount,
|
|
11
12
|
useTemplateRef,
|
|
13
|
+
h,
|
|
12
14
|
} from "vue";
|
|
13
15
|
import { isEqual } from "lodash-es";
|
|
14
16
|
|
|
@@ -30,7 +32,7 @@ import { normalizeColumns, mapRowColumns, getFlatRows, getRowChildrenIds } from
|
|
|
30
32
|
import { PX_IN_REM } from "../constants";
|
|
31
33
|
import { COMPONENT_NAME } from "./constants";
|
|
32
34
|
|
|
33
|
-
import type { ComputedRef } from "vue";
|
|
35
|
+
import type { ComputedRef, VNode } from "vue";
|
|
34
36
|
import type { Config as UDividerConfig } from "../ui.container-divider/types";
|
|
35
37
|
import type {
|
|
36
38
|
Cell,
|
|
@@ -43,6 +45,7 @@ import type {
|
|
|
43
45
|
FlatRow,
|
|
44
46
|
ColumnObject,
|
|
45
47
|
SearchMatch,
|
|
48
|
+
UTableRowProps,
|
|
46
49
|
} from "./types";
|
|
47
50
|
import { StickySide } from "./types";
|
|
48
51
|
|
|
@@ -310,40 +313,135 @@ const totalSearchMatches = computed(() => {
|
|
|
310
313
|
return count;
|
|
311
314
|
});
|
|
312
315
|
|
|
313
|
-
|
|
314
|
-
|
|
316
|
+
const selectedRowIds = computed(() => {
|
|
317
|
+
return new Set(localSelectedRows.value.map((row) => row.id));
|
|
315
318
|
});
|
|
316
319
|
|
|
317
|
-
|
|
318
|
-
|
|
320
|
+
const isSelectedAllRows = computed(() => {
|
|
321
|
+
return localSelectedRows.value.length === flatTableRows.value.length;
|
|
322
|
+
});
|
|
319
323
|
|
|
320
|
-
|
|
321
|
-
|
|
324
|
+
// Conditional watchers - only create listeners when their associated features are enabled
|
|
325
|
+
// Using watchEffect to dynamically create/destroy watchers based on feature state
|
|
326
|
+
|
|
327
|
+
// Search-related watchers
|
|
328
|
+
// Note: totalSearchMatches watcher always runs to emit events (even when search is cleared)
|
|
329
|
+
watch(totalSearchMatches, (count) => emit("search", count), { flush: "post" });
|
|
330
|
+
|
|
331
|
+
// activeMatch watcher - only created when search is active
|
|
332
|
+
let stopActiveMatchWatch: (() => void) | null = null;
|
|
333
|
+
|
|
334
|
+
watchEffect(() => {
|
|
335
|
+
if (props.search) {
|
|
336
|
+
// Create watcher if it doesn't exist
|
|
337
|
+
if (!stopActiveMatchWatch) {
|
|
338
|
+
stopActiveMatchWatch = watch(
|
|
339
|
+
activeMatch,
|
|
340
|
+
(match) => {
|
|
341
|
+
if (!match) return;
|
|
342
|
+
|
|
343
|
+
if (props.virtualScroll) {
|
|
344
|
+
const rowIndex = visibleFlatRows.value.findIndex((row) => row.id === match.rowId);
|
|
345
|
+
|
|
346
|
+
if (rowIndex === -1) return;
|
|
347
|
+
|
|
348
|
+
virtualScroll.scrollToIndex(rowIndex);
|
|
349
|
+
} else {
|
|
350
|
+
scrollToRow(match.rowId);
|
|
351
|
+
}
|
|
352
|
+
},
|
|
353
|
+
{ flush: "post" },
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
} else {
|
|
357
|
+
// Cleanup watcher if it exists
|
|
358
|
+
if (stopActiveMatchWatch) {
|
|
359
|
+
stopActiveMatchWatch();
|
|
360
|
+
stopActiveMatchWatch = null;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
// Selection-related watchers (only created when selectable is enabled)
|
|
366
|
+
let stopLocalSelectedRowsWatch: (() => void) | null = null;
|
|
367
|
+
let stopSelectedRowsWatch: (() => void) | null = null;
|
|
368
|
+
let stopSelectAllWatch: (() => void) | null = null;
|
|
369
|
+
|
|
370
|
+
watchEffect(() => {
|
|
371
|
+
if (props.selectable) {
|
|
372
|
+
// Create watchers if they don't exist
|
|
373
|
+
if (!stopLocalSelectedRowsWatch) {
|
|
374
|
+
stopLocalSelectedRowsWatch = watch(localSelectedRows, onChangeLocalSelectedRows);
|
|
375
|
+
}
|
|
322
376
|
|
|
323
|
-
if (
|
|
377
|
+
if (!stopSelectedRowsWatch) {
|
|
378
|
+
stopSelectedRowsWatch = watch(() => props.selectedRows, onChangeSelectedRows, {
|
|
379
|
+
immediate: true,
|
|
380
|
+
});
|
|
381
|
+
}
|
|
324
382
|
|
|
325
|
-
|
|
383
|
+
if (!stopSelectAllWatch) {
|
|
384
|
+
stopSelectAllWatch = watch(selectAll, onChangeSelectAll);
|
|
385
|
+
}
|
|
326
386
|
} else {
|
|
327
|
-
|
|
387
|
+
// Cleanup watchers if they exist
|
|
388
|
+
if (stopLocalSelectedRowsWatch) {
|
|
389
|
+
stopLocalSelectedRowsWatch();
|
|
390
|
+
stopLocalSelectedRowsWatch = null;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (stopSelectedRowsWatch) {
|
|
394
|
+
stopSelectedRowsWatch();
|
|
395
|
+
stopSelectedRowsWatch = null;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (stopSelectAllWatch) {
|
|
399
|
+
stopSelectAllWatch();
|
|
400
|
+
stopSelectAllWatch = null;
|
|
401
|
+
}
|
|
328
402
|
}
|
|
329
403
|
});
|
|
330
404
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
});
|
|
405
|
+
// Expansion watcher (always register as it's a core feature)
|
|
406
|
+
watch(() => props.expandedRows, onChangeExpandedRows, { immediate: true });
|
|
334
407
|
|
|
335
|
-
|
|
336
|
-
|
|
408
|
+
// Sticky header watcher (only created when stickyHeader is enabled)
|
|
409
|
+
let stopHeaderStickyWatch: (() => void) | null = null;
|
|
410
|
+
|
|
411
|
+
watchEffect(() => {
|
|
412
|
+
if (props.stickyHeader) {
|
|
413
|
+
// Create watcher if it doesn't exist
|
|
414
|
+
if (!stopHeaderStickyWatch) {
|
|
415
|
+
stopHeaderStickyWatch = watch(isHeaderSticky, setHeaderCellWidth);
|
|
416
|
+
}
|
|
417
|
+
} else {
|
|
418
|
+
// Cleanup watcher if it exists
|
|
419
|
+
if (stopHeaderStickyWatch) {
|
|
420
|
+
stopHeaderStickyWatch();
|
|
421
|
+
stopHeaderStickyWatch = null;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
337
424
|
});
|
|
338
425
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
)
|
|
426
|
+
// Sticky footer watcher (only created when stickyFooter is enabled)
|
|
427
|
+
let stopFooterStickyWatch: (() => void) | null = null;
|
|
428
|
+
|
|
429
|
+
watchEffect(() => {
|
|
430
|
+
if (props.stickyFooter) {
|
|
431
|
+
// Create watcher if it doesn't exist
|
|
432
|
+
if (!stopFooterStickyWatch) {
|
|
433
|
+
stopFooterStickyWatch = watch(isFooterSticky, (newValue) =>
|
|
434
|
+
newValue ? nextTick(setFooterCellWidth) : setFooterCellWidth(null),
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
} else {
|
|
438
|
+
// Cleanup watcher if it exists
|
|
439
|
+
if (stopFooterStickyWatch) {
|
|
440
|
+
stopFooterStickyWatch();
|
|
441
|
+
stopFooterStickyWatch = null;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
});
|
|
347
445
|
|
|
348
446
|
let resizeObserver: ResizeObserver | null = null;
|
|
349
447
|
let scrollRafId: number | null = null;
|
|
@@ -612,6 +710,85 @@ function onClickCell(cell: Cell, row: Row, key: string | number) {
|
|
|
612
710
|
emit("clickCell", cell, row, key);
|
|
613
711
|
}
|
|
614
712
|
|
|
713
|
+
function onBodyClick(event: MouseEvent) {
|
|
714
|
+
const target = event.target as HTMLElement;
|
|
715
|
+
|
|
716
|
+
const row = target.closest("tr");
|
|
717
|
+
|
|
718
|
+
if (!row) return;
|
|
719
|
+
|
|
720
|
+
const rowId = row.getAttribute("data-row-id");
|
|
721
|
+
|
|
722
|
+
if (!rowId) return;
|
|
723
|
+
|
|
724
|
+
const rowData = visibleFlatRows.value.find((r) => String(r.id) === rowId);
|
|
725
|
+
|
|
726
|
+
if (!rowData) return;
|
|
727
|
+
|
|
728
|
+
// Handle checkbox toggle via event delegation.
|
|
729
|
+
// When unchecking, UCheckbox.onIconClick() calls input.click() which dispatches
|
|
730
|
+
// a second (programmatic) click event with target=INPUT and isTrusted=false.
|
|
731
|
+
// Without this guard, onToggleRowCheckbox fires twice (deselect then reselect).
|
|
732
|
+
const checkboxCell = target.closest("td[data-checkbox-id]");
|
|
733
|
+
|
|
734
|
+
if (checkboxCell) {
|
|
735
|
+
if (target.tagName === "INPUT" && !event.isTrusted) return;
|
|
736
|
+
|
|
737
|
+
onToggleRowCheckbox(rowData);
|
|
738
|
+
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// Handle expand icon toggle via event delegation
|
|
743
|
+
const expandIconElement = target.closest("[data-expand-icon]");
|
|
744
|
+
|
|
745
|
+
if (expandIconElement) {
|
|
746
|
+
onToggleExpand(rowData);
|
|
747
|
+
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// Handle row click via event delegation
|
|
752
|
+
onClickRow(rowData);
|
|
753
|
+
|
|
754
|
+
// Handle cell click via event delegation
|
|
755
|
+
const cell = target.closest("td");
|
|
756
|
+
|
|
757
|
+
if (cell) {
|
|
758
|
+
const cellKey = cell.getAttribute("data-cell-key");
|
|
759
|
+
|
|
760
|
+
if (cellKey) {
|
|
761
|
+
const cellValue = rowData[cellKey];
|
|
762
|
+
|
|
763
|
+
onClickCell(cellValue, rowData, cellKey);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
function onBodyDoubleClick(event: MouseEvent) {
|
|
769
|
+
const target = event.target as HTMLElement;
|
|
770
|
+
|
|
771
|
+
const row = target.closest("tr");
|
|
772
|
+
|
|
773
|
+
if (!row) return;
|
|
774
|
+
|
|
775
|
+
const rowId = row.getAttribute("data-row-id");
|
|
776
|
+
|
|
777
|
+
if (!rowId) return;
|
|
778
|
+
|
|
779
|
+
const rowData = visibleFlatRows.value.find((r) => String(r.id) === rowId);
|
|
780
|
+
|
|
781
|
+
if (!rowData) return;
|
|
782
|
+
|
|
783
|
+
const selection = window.getSelection();
|
|
784
|
+
|
|
785
|
+
if (selection) {
|
|
786
|
+
selection.removeAllRanges();
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
onDoubleClickRow(rowData);
|
|
790
|
+
}
|
|
791
|
+
|
|
615
792
|
function onChangeSelectAll(selectAll: boolean) {
|
|
616
793
|
if (selectAll && canSelectAll.value) {
|
|
617
794
|
localSelectedRows.value = [...flatTableRows.value];
|
|
@@ -631,6 +808,7 @@ function onChangeLocalSelectedRows(selectedRows: Row[]) {
|
|
|
631
808
|
nextTick(setHeaderCellWidth);
|
|
632
809
|
}
|
|
633
810
|
|
|
811
|
+
// Set flag to prevent selectAll watcher from triggering
|
|
634
812
|
selectAll.value = !!selectedRows.length;
|
|
635
813
|
|
|
636
814
|
emit("update:selectedRows", localSelectedRows.value);
|
|
@@ -853,6 +1031,135 @@ const tableRowAttrs = {
|
|
|
853
1031
|
bodyCellSearchMatchActiveAttrs,
|
|
854
1032
|
bodyCellSearchMatchTextActiveAttrs,
|
|
855
1033
|
} as unknown as UTableRowAttrs;
|
|
1034
|
+
|
|
1035
|
+
function renderDateDividerRow(row: FlatRow, rowIndex: number): VNode | null {
|
|
1036
|
+
if (!isShownDateDivider(rowIndex) || !row.rowDate) return null;
|
|
1037
|
+
|
|
1038
|
+
const isSelected = isRowSelectedWithin(rowIndex);
|
|
1039
|
+
|
|
1040
|
+
const propsDateDivider = isSelected
|
|
1041
|
+
? bodyRowCheckedDateDividerAttrs.value
|
|
1042
|
+
: bodyRowDateDividerAttrs.value;
|
|
1043
|
+
|
|
1044
|
+
const dividerNode = h(UDivider, {
|
|
1045
|
+
label: getDateDividerData(row.rowDate).label,
|
|
1046
|
+
...(isSelected ? bodySelectedDateDividerAttrs.value : bodyDateDividerAttrs.value),
|
|
1047
|
+
config: getDateDividerConfig(row, isSelected),
|
|
1048
|
+
});
|
|
1049
|
+
|
|
1050
|
+
return h("tr", propsDateDivider, [
|
|
1051
|
+
h(
|
|
1052
|
+
"td",
|
|
1053
|
+
{
|
|
1054
|
+
...bodyCellDateDividerAttrs.value,
|
|
1055
|
+
colspan: colsCount.value,
|
|
1056
|
+
},
|
|
1057
|
+
[dividerNode],
|
|
1058
|
+
),
|
|
1059
|
+
]);
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
function renderTableRowSlots(row: FlatRow, rowIndex: number) {
|
|
1063
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
|
1064
|
+
const cellSlots: Record<string, Function> = {};
|
|
1065
|
+
|
|
1066
|
+
// Create cell slots
|
|
1067
|
+
Object.entries(mapRowColumns(row, normalizedColumns.value)).forEach(([key], cellIndex) => {
|
|
1068
|
+
cellSlots[`cell-${key}`] = ({
|
|
1069
|
+
value: cellValue,
|
|
1070
|
+
row: cellRow,
|
|
1071
|
+
}: {
|
|
1072
|
+
value: Cell;
|
|
1073
|
+
row: FlatRow;
|
|
1074
|
+
}) => {
|
|
1075
|
+
const hasCellSlot = hasSlotContent(slots[`cell-${key}`], {
|
|
1076
|
+
value: cellValue,
|
|
1077
|
+
row: cellRow,
|
|
1078
|
+
index: rowIndex,
|
|
1079
|
+
cellIndex,
|
|
1080
|
+
});
|
|
1081
|
+
|
|
1082
|
+
if (hasCellSlot) {
|
|
1083
|
+
return slots[`cell-${key}`]?.({
|
|
1084
|
+
value: cellValue,
|
|
1085
|
+
row: cellRow,
|
|
1086
|
+
index: rowIndex,
|
|
1087
|
+
cellIndex,
|
|
1088
|
+
});
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
return null;
|
|
1092
|
+
};
|
|
1093
|
+
});
|
|
1094
|
+
|
|
1095
|
+
// Add expand slot
|
|
1096
|
+
cellSlots.expand = ({ row: expandedRow, expanded }: { row: FlatRow; expanded: boolean }) => {
|
|
1097
|
+
const hasExpandSlot = hasSlotContent(slots.expand, {
|
|
1098
|
+
index: rowIndex,
|
|
1099
|
+
row: expandedRow,
|
|
1100
|
+
expanded,
|
|
1101
|
+
});
|
|
1102
|
+
|
|
1103
|
+
if (hasExpandSlot) {
|
|
1104
|
+
return slots.expand?.({ index: rowIndex, row: expandedRow, expanded });
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
return null;
|
|
1108
|
+
};
|
|
1109
|
+
|
|
1110
|
+
// Add nested-row slot
|
|
1111
|
+
cellSlots["nested-row"] = () => {
|
|
1112
|
+
const hasNestedRowSlot = hasSlotContent(slots["nested-row"], {
|
|
1113
|
+
index: rowIndex,
|
|
1114
|
+
row,
|
|
1115
|
+
nestedLevel: Number(row.nestedLevel || 0),
|
|
1116
|
+
});
|
|
1117
|
+
|
|
1118
|
+
if (hasNestedRowSlot && row) {
|
|
1119
|
+
return slots["nested-row"]?.({
|
|
1120
|
+
index: rowIndex,
|
|
1121
|
+
row,
|
|
1122
|
+
nestedLevel: Number(row.nestedLevel || 0),
|
|
1123
|
+
});
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
return null;
|
|
1127
|
+
};
|
|
1128
|
+
|
|
1129
|
+
return cellSlots;
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
function renderTableRow(row: FlatRow, rowIndex: number): VNode {
|
|
1133
|
+
return h(
|
|
1134
|
+
UTableRow,
|
|
1135
|
+
{
|
|
1136
|
+
selectable: props.selectable,
|
|
1137
|
+
row,
|
|
1138
|
+
columns: normalizedColumns.value,
|
|
1139
|
+
config: config.value,
|
|
1140
|
+
attrs: tableRowAttrs as unknown as UTableRowAttrs,
|
|
1141
|
+
colsCount: colsCount.value,
|
|
1142
|
+
nestedLevel: Number(row.nestedLevel || 0),
|
|
1143
|
+
emptyCellLabel: props.emptyCellLabel,
|
|
1144
|
+
"data-test": getDataTest("row"),
|
|
1145
|
+
"data-row-id": row.id,
|
|
1146
|
+
isExpanded: localExpandedRows.value.includes(row.id),
|
|
1147
|
+
isChecked: isRowSelected(row),
|
|
1148
|
+
columnPositions: columnPositions.value,
|
|
1149
|
+
search: props.search,
|
|
1150
|
+
searchMatchColumns: getRowSearchMatchColumns(row),
|
|
1151
|
+
activeSearchMatchColumn: getRowActiveSearchMatchColumn(row),
|
|
1152
|
+
textEllipsis: props.textEllipsis,
|
|
1153
|
+
} as unknown as UTableRowProps,
|
|
1154
|
+
renderTableRowSlots(row, rowIndex),
|
|
1155
|
+
);
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
function renderRowTemplate(row: FlatRow, rowIndex: number): VNode[] {
|
|
1159
|
+
return [renderDateDividerRow(row, rowIndex), renderTableRow(row, rowIndex)].filter(
|
|
1160
|
+
Boolean,
|
|
1161
|
+
) as VNode[];
|
|
1162
|
+
}
|
|
856
1163
|
</script>
|
|
857
1164
|
|
|
858
1165
|
<template>
|
|
@@ -893,10 +1200,10 @@ const tableRowAttrs = {
|
|
|
893
1200
|
>
|
|
894
1201
|
<template v-if="hasSlotContent($slots[`header-${column.key}`], { column, index })">
|
|
895
1202
|
<!--
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
1203
|
+
@slot Use it to customize needed header cell.
|
|
1204
|
+
@binding {object} column
|
|
1205
|
+
@binding {number} index
|
|
1206
|
+
-->
|
|
900
1207
|
<slot :name="`header-${column.key}`" :column="column" :index="index" />
|
|
901
1208
|
</template>
|
|
902
1209
|
|
|
@@ -1042,7 +1349,12 @@ const tableRowAttrs = {
|
|
|
1042
1349
|
<ULoaderProgress :loading="loading" v-bind="headerLoaderAttrs" />
|
|
1043
1350
|
</thead>
|
|
1044
1351
|
|
|
1045
|
-
<tbody
|
|
1352
|
+
<tbody
|
|
1353
|
+
v-if="sortedRows.length"
|
|
1354
|
+
v-bind="bodyAttrs"
|
|
1355
|
+
@click="onBodyClick"
|
|
1356
|
+
@dblclick="onBodyDoubleClick"
|
|
1357
|
+
>
|
|
1046
1358
|
<tr
|
|
1047
1359
|
v-if="hasBeforeFirstRowSlot"
|
|
1048
1360
|
v-bind="isRowSelected(sortedRows[0]) ? beforeBodyRowCheckedAttrs : beforeBodyRowAttrs"
|
|
@@ -1060,105 +1372,9 @@ const tableRowAttrs = {
|
|
|
1060
1372
|
/>
|
|
1061
1373
|
</tr>
|
|
1062
1374
|
|
|
1063
|
-
<
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
v-bind="bodyRowDateDividerAttrs"
|
|
1067
|
-
>
|
|
1068
|
-
<td v-bind="bodyCellDateDividerAttrs" :colspan="colsCount">
|
|
1069
|
-
<UDivider
|
|
1070
|
-
:label="getDateDividerData(row.rowDate).label"
|
|
1071
|
-
v-bind="bodyDateDividerAttrs"
|
|
1072
|
-
:config="getDateDividerConfig(row, false)"
|
|
1073
|
-
/>
|
|
1074
|
-
</td>
|
|
1075
|
-
</tr>
|
|
1076
|
-
|
|
1077
|
-
<tr
|
|
1078
|
-
v-if="isShownDateDivider(rowIndex) && isRowSelectedWithin(rowIndex) && row.rowDate"
|
|
1079
|
-
v-bind="bodyRowCheckedDateDividerAttrs"
|
|
1080
|
-
>
|
|
1081
|
-
<td v-bind="bodyCellDateDividerAttrs" :colspan="colsCount">
|
|
1082
|
-
<UDivider
|
|
1083
|
-
:label="getDateDividerData(row.rowDate).label"
|
|
1084
|
-
v-bind="bodySelectedDateDividerAttrs"
|
|
1085
|
-
:config="getDateDividerConfig(row, true)"
|
|
1086
|
-
/>
|
|
1087
|
-
</td>
|
|
1088
|
-
</tr>
|
|
1089
|
-
|
|
1090
|
-
<UTableRow
|
|
1091
|
-
:selectable="selectable"
|
|
1092
|
-
:row="row"
|
|
1093
|
-
:columns="normalizedColumns"
|
|
1094
|
-
:config="config"
|
|
1095
|
-
:attrs="tableRowAttrs as unknown as UTableRowAttrs"
|
|
1096
|
-
:cols-count="colsCount"
|
|
1097
|
-
:nested-level="Number(row.nestedLevel || 0)"
|
|
1098
|
-
:empty-cell-label="emptyCellLabel"
|
|
1099
|
-
:data-test="getDataTest('row')"
|
|
1100
|
-
:data-row-id="row.id"
|
|
1101
|
-
:is-expanded="localExpandedRows.includes(row.id)"
|
|
1102
|
-
:is-checked="isRowSelected(row)"
|
|
1103
|
-
:column-positions="columnPositions"
|
|
1104
|
-
:search="search"
|
|
1105
|
-
:search-match-columns="getRowSearchMatchColumns(row)"
|
|
1106
|
-
:active-search-match-column="getRowActiveSearchMatchColumn(row)"
|
|
1107
|
-
:text-ellipsis="textEllipsis"
|
|
1108
|
-
@click="onClickRow"
|
|
1109
|
-
@dblclick="onDoubleClickRow"
|
|
1110
|
-
@click-cell="onClickCell"
|
|
1111
|
-
@toggle-expand="onToggleExpand"
|
|
1112
|
-
@toggle-checkbox="onToggleRowCheckbox"
|
|
1113
|
-
>
|
|
1114
|
-
<template
|
|
1115
|
-
v-for="(value, key, cellIndex) in mapRowColumns(row, normalizedColumns)"
|
|
1116
|
-
:key="`${rowIndex}-${cellIndex}`"
|
|
1117
|
-
#[`cell-${key}`]="{ value: cellValue, row: cellRow }"
|
|
1118
|
-
>
|
|
1119
|
-
<!--
|
|
1120
|
-
@slot Use it to customize needed table cell.
|
|
1121
|
-
@binding {string} value
|
|
1122
|
-
@binding {object} row
|
|
1123
|
-
@binding {number} index
|
|
1124
|
-
@binding {number} cellIndex
|
|
1125
|
-
-->
|
|
1126
|
-
<slot
|
|
1127
|
-
:name="`cell-${key}`"
|
|
1128
|
-
:value="cellValue"
|
|
1129
|
-
:row="cellRow"
|
|
1130
|
-
:index="rowIndex"
|
|
1131
|
-
:cell-index="cellIndex"
|
|
1132
|
-
/>
|
|
1133
|
-
</template>
|
|
1134
|
-
|
|
1135
|
-
<template #expand="{ row: expandedRow, expanded }">
|
|
1136
|
-
<!--
|
|
1137
|
-
@slot Use it to customize row expand icon.
|
|
1138
|
-
@binding {object} row
|
|
1139
|
-
@binding {boolean} expanded
|
|
1140
|
-
@binding {number} index
|
|
1141
|
-
-->
|
|
1142
|
-
<slot name="expand" :index="rowIndex" :row="expandedRow" :expanded="expanded" />
|
|
1143
|
-
</template>
|
|
1144
|
-
|
|
1145
|
-
<template #nested-row>
|
|
1146
|
-
<!--
|
|
1147
|
-
@slot Use it to add inside nested row.
|
|
1148
|
-
@binding {object} row
|
|
1149
|
-
@binding {number} index
|
|
1150
|
-
@binding {number} nestedLevel
|
|
1151
|
-
-->
|
|
1152
|
-
<slot
|
|
1153
|
-
v-if="row"
|
|
1154
|
-
name="nested-row"
|
|
1155
|
-
:index="rowIndex"
|
|
1156
|
-
:row="row"
|
|
1157
|
-
:nested-level="Number(row.nestedLevel || 0)"
|
|
1158
|
-
/>
|
|
1159
|
-
</template>
|
|
1160
|
-
</UTableRow>
|
|
1161
|
-
</template>
|
|
1375
|
+
<component
|
|
1376
|
+
:is="() => renderedRows.map((row, rowIndex) => renderRowTemplate(row, rowIndex)).flat()"
|
|
1377
|
+
/>
|
|
1162
1378
|
|
|
1163
1379
|
<tr v-if="props.virtualScroll && virtualScroll.bottomSpacerHeight.value > 0">
|
|
1164
1380
|
<td
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { computed, onMounted, useTemplateRef } from "vue";
|
|
3
|
-
import { cx } from "../utils/ui";
|
|
2
|
+
import { computed, onMounted, useTemplateRef, useSlots, useAttrs, h } from "vue";
|
|
4
3
|
import { hasSlotContent, isEmptyValue } from "../utils/helper";
|
|
4
|
+
import { cx } from "../utils/ui";
|
|
5
5
|
|
|
6
6
|
import { PX_IN_REM } from "../constants";
|
|
7
7
|
import { mapRowColumns } from "./utilTable";
|
|
8
8
|
|
|
9
|
-
import { useMutationObserver } from "../composables/useMutationObserver";
|
|
10
9
|
import { useUI } from "../composables/useUI";
|
|
10
|
+
import { useMutationObserver } from "../composables/useMutationObserver";
|
|
11
11
|
|
|
12
12
|
import UIcon from "../ui.image-icon/UIcon.vue";
|
|
13
13
|
import UCheckbox from "../ui.form-checkbox/UCheckbox.vue";
|
|
@@ -15,6 +15,7 @@ import UCheckbox from "../ui.form-checkbox/UCheckbox.vue";
|
|
|
15
15
|
import defaultConfig from "./config";
|
|
16
16
|
|
|
17
17
|
import { StickySide } from "./types";
|
|
18
|
+
import type { VNode } from "vue";
|
|
18
19
|
import type { Cell, CellObject, Row, UTableRowProps, Config, ColumnObject } from "./types";
|
|
19
20
|
|
|
20
21
|
const NESTED_ROW_SHIFT_REM = 1.5;
|
|
@@ -24,7 +25,8 @@ defineOptions({ internal: true });
|
|
|
24
25
|
|
|
25
26
|
const props = defineProps<UTableRowProps>();
|
|
26
27
|
|
|
27
|
-
const
|
|
28
|
+
const slots = useSlots();
|
|
29
|
+
const attrs = useAttrs();
|
|
28
30
|
|
|
29
31
|
const cellRef = useTemplateRef<HTMLDivElement[]>("cell");
|
|
30
32
|
const toggleWrapperRef = useTemplateRef<HTMLDivElement[]>("toggle-wrapper");
|
|
@@ -109,20 +111,6 @@ function getNestedCheckboxShift() {
|
|
|
109
111
|
return { transform: `translateX(${props.nestedLevel * LAST_NESTED_ROW_SHIFT_REM}rem)` };
|
|
110
112
|
}
|
|
111
113
|
|
|
112
|
-
function onClick(row: Row) {
|
|
113
|
-
emit("click", row);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function onDoubleClick(row: Row) {
|
|
117
|
-
const selection = window.getSelection();
|
|
118
|
-
|
|
119
|
-
if (selection) {
|
|
120
|
-
selection.removeAllRanges();
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
emit("dblclick", row);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
114
|
function setCellTitle(mutations: MutationRecord[]) {
|
|
127
115
|
mutations.forEach((mutation) => {
|
|
128
116
|
const { target } = mutation;
|
|
@@ -147,10 +135,6 @@ function setElementTitle(element: HTMLElement) {
|
|
|
147
135
|
}
|
|
148
136
|
}
|
|
149
137
|
|
|
150
|
-
function onClickCell(cell: unknown | string | number, row: Row, key: string | number) {
|
|
151
|
-
emit("clickCell", cell, row, key);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
138
|
function getRowClasses(row: Row) {
|
|
155
139
|
const rowClasses = row?.class || "";
|
|
156
140
|
|
|
@@ -161,14 +145,6 @@ function getRowAttrs() {
|
|
|
161
145
|
return props.isChecked ? props.attrs.bodyRowCheckedAttrs.value : props.attrs.bodyRowAttrs.value;
|
|
162
146
|
}
|
|
163
147
|
|
|
164
|
-
function onToggleExpand(row: Row) {
|
|
165
|
-
emit("toggleExpand", row);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
function onInputCheckbox(row: Row) {
|
|
169
|
-
emit("toggleCheckbox", row);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
148
|
function getStickyColumnStyle(column: ColumnObject) {
|
|
173
149
|
const position = props.columnPositions.get(column.key);
|
|
174
150
|
|
|
@@ -254,6 +230,10 @@ function escapeHtml(text: string): string {
|
|
|
254
230
|
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
255
231
|
}
|
|
256
232
|
|
|
233
|
+
function isNestedFirstCell(index: number): boolean {
|
|
234
|
+
return (Boolean(props.row.row) || Boolean(props.nestedLevel)) && index === 0;
|
|
235
|
+
}
|
|
236
|
+
|
|
257
237
|
function shouldRenderCellWrapper(row: Row, key: string): boolean {
|
|
258
238
|
return Boolean(
|
|
259
239
|
props.textEllipsis ||
|
|
@@ -262,126 +242,222 @@ function shouldRenderCellWrapper(row: Row, key: string): boolean {
|
|
|
262
242
|
);
|
|
263
243
|
}
|
|
264
244
|
|
|
245
|
+
function renderCellContent(value: Cell, key: string, index: number): VNode | VNode[] | string {
|
|
246
|
+
const keyStr = String(key);
|
|
247
|
+
const hasCellSlot = hasSlotContent(slots[`cell-${key}`], { value, row: props.row, index });
|
|
248
|
+
|
|
249
|
+
// Check if slot exists
|
|
250
|
+
if (hasCellSlot) {
|
|
251
|
+
return slots[`cell-${key}`]?.({ value, row: props.row, index }) || "";
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Render cell wrapper with highlighted HTML
|
|
255
|
+
if (shouldRenderCellWrapper(props.row, keyStr)) {
|
|
256
|
+
return h("div", {
|
|
257
|
+
ref: cellRef,
|
|
258
|
+
...props.attrs.bodyCellContentAttrs.value,
|
|
259
|
+
class: cx([
|
|
260
|
+
props.attrs.bodyCellContentAttrs.value.class,
|
|
261
|
+
getCellContentClass(props.row, keyStr),
|
|
262
|
+
]),
|
|
263
|
+
innerHTML: getHighlightedHtml(value, keyStr),
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Render plain text
|
|
268
|
+
return formatCellValue(value);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function renderNestedFirstCell(value: Cell, key: string, index: number): VNode {
|
|
272
|
+
const keyStr = String(key);
|
|
273
|
+
const hasExpandSlot = hasSlotContent(slots?.expand, {
|
|
274
|
+
row: props.row,
|
|
275
|
+
expanded: props.isExpanded,
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
const toggleIconNode = h(UIcon, {
|
|
279
|
+
size: "xs",
|
|
280
|
+
interactive: true,
|
|
281
|
+
name: getToggleIconName(),
|
|
282
|
+
color: "primary",
|
|
283
|
+
...toggleIconConfig.value,
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
const toggleWrapperNode = h(
|
|
287
|
+
"div",
|
|
288
|
+
{
|
|
289
|
+
ref: toggleWrapperRef,
|
|
290
|
+
...props.attrs.bodyCellNestedIconWrapperAttrs.value,
|
|
291
|
+
style: { width: getIconWidth() },
|
|
292
|
+
},
|
|
293
|
+
[toggleIconNode],
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
const toggleIconWrapperNode = h(
|
|
297
|
+
"div",
|
|
298
|
+
{
|
|
299
|
+
"data-row-toggle-icon": props.row.id,
|
|
300
|
+
"data-expand-icon": props.row.id,
|
|
301
|
+
onDblclick: (e: Event) => e.stopPropagation(),
|
|
302
|
+
},
|
|
303
|
+
hasExpandSlot
|
|
304
|
+
? slots?.expand?.({ row: props.row, expanded: props.isExpanded })
|
|
305
|
+
: [toggleWrapperNode],
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
const hasCellSlot = hasSlotContent(slots[`cell-${key}`], { value, row: props.row, index });
|
|
309
|
+
|
|
310
|
+
return h(
|
|
311
|
+
"div",
|
|
312
|
+
{
|
|
313
|
+
style: getNestedShift(),
|
|
314
|
+
...props.attrs.bodyCellNestedAttrs.value,
|
|
315
|
+
},
|
|
316
|
+
[
|
|
317
|
+
props.row.row ? toggleIconWrapperNode : null,
|
|
318
|
+
// Cell content
|
|
319
|
+
...(() => {
|
|
320
|
+
if (hasCellSlot) {
|
|
321
|
+
const slotContent = slots[`cell-${key}`]?.({ value, row: props.row, index });
|
|
322
|
+
|
|
323
|
+
return Array.isArray(slotContent) ? slotContent : [slotContent];
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (shouldRenderCellWrapper(props.row, keyStr)) {
|
|
327
|
+
return [
|
|
328
|
+
h("div", {
|
|
329
|
+
ref: cellRef,
|
|
330
|
+
...props.attrs.bodyCellContentAttrs.value,
|
|
331
|
+
class: cx([
|
|
332
|
+
props.attrs.bodyCellContentAttrs.value.class,
|
|
333
|
+
getCellContentClass(props.row, keyStr),
|
|
334
|
+
]),
|
|
335
|
+
"data-test": getDataTest(`${key}-cell`),
|
|
336
|
+
innerHTML: getHighlightedHtml(value, keyStr),
|
|
337
|
+
}),
|
|
338
|
+
];
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return [formatCellValue(value)];
|
|
342
|
+
})(),
|
|
343
|
+
].filter(Boolean),
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function renderTableCell(value: Cell, key: string, index: number): VNode {
|
|
348
|
+
const keyStr = String(key);
|
|
349
|
+
|
|
350
|
+
const nestedCellNode = isNestedFirstCell(index)
|
|
351
|
+
? renderNestedFirstCell(value, key, index)
|
|
352
|
+
: renderCellContent(value, key, index);
|
|
353
|
+
|
|
354
|
+
return h(
|
|
355
|
+
"td",
|
|
356
|
+
{
|
|
357
|
+
key: index,
|
|
358
|
+
...props.attrs.bodyCellBaseAttrs.value,
|
|
359
|
+
class: cx([
|
|
360
|
+
props.attrs.bodyCellBaseAttrs.value.class,
|
|
361
|
+
props.columns[index].tdClass,
|
|
362
|
+
getCellClasses(props.row, keyStr),
|
|
363
|
+
getStickyColumnClass(props.columns[index]),
|
|
364
|
+
getSearchMatchCellClass(keyStr),
|
|
365
|
+
]),
|
|
366
|
+
style: getStickyColumnStyle(props.columns[index]),
|
|
367
|
+
"data-cell-key": key,
|
|
368
|
+
"data-test": getDataTest(`${key}-cell`),
|
|
369
|
+
},
|
|
370
|
+
[nestedCellNode],
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function renderCheckboxCell(): VNode | null {
|
|
375
|
+
if (!props.selectable) return null;
|
|
376
|
+
|
|
377
|
+
const checkboxNode = h(UCheckbox, {
|
|
378
|
+
modelValue: props.isChecked,
|
|
379
|
+
size: "md",
|
|
380
|
+
...props.attrs.bodyCheckboxAttrs.value,
|
|
381
|
+
"data-id": props.row.id,
|
|
382
|
+
"data-checkbox-id": props.row.id,
|
|
383
|
+
"data-test": getDataTest("body-checkbox"),
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
return h(
|
|
387
|
+
"td",
|
|
388
|
+
{
|
|
389
|
+
...props.attrs.bodyCellCheckboxAttrs.value,
|
|
390
|
+
"data-checkbox-id": props.row.id,
|
|
391
|
+
class: cx([
|
|
392
|
+
props.attrs.bodyCellCheckboxAttrs.value.class,
|
|
393
|
+
props.columns[0]?.sticky === StickySide.Left
|
|
394
|
+
? props.attrs.bodyCellStickyLeftAttrs.value.class
|
|
395
|
+
: "",
|
|
396
|
+
]),
|
|
397
|
+
style: {
|
|
398
|
+
...getNestedCheckboxShift(),
|
|
399
|
+
...(props.columns[0]?.sticky === StickySide.Left ? { left: "0" } : {}),
|
|
400
|
+
},
|
|
401
|
+
onDblclick: (e: Event) => e.stopPropagation(),
|
|
402
|
+
},
|
|
403
|
+
[checkboxNode],
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function renderMainRow(): VNode | null {
|
|
408
|
+
const hasNestedRowSlot = hasSlotContent(slots["nested-row"], {
|
|
409
|
+
row: props.row,
|
|
410
|
+
nestedLevel: props.nestedLevel,
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
if (hasNestedRowSlot && props.row.parentRowId) {
|
|
414
|
+
return null;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const cells = Object.entries(mapRowColumns(props.row, props.columns)).map(
|
|
418
|
+
([key, value], index) => {
|
|
419
|
+
return renderTableCell(value, key, index);
|
|
420
|
+
},
|
|
421
|
+
);
|
|
422
|
+
|
|
423
|
+
return h(
|
|
424
|
+
"tr",
|
|
425
|
+
{
|
|
426
|
+
...attrs,
|
|
427
|
+
...getRowAttrs(),
|
|
428
|
+
class: cx([getRowAttrs().class, getRowClasses(props.row)]),
|
|
429
|
+
},
|
|
430
|
+
[renderCheckboxCell(), ...cells].filter(Boolean),
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function renderNestedRow(): VNode | null {
|
|
435
|
+
const hasNestedRowSlot = hasSlotContent(slots["nested-row"], {
|
|
436
|
+
row: props.row,
|
|
437
|
+
nestedLevel: props.nestedLevel,
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
if (!hasNestedRowSlot || !props.row.parentRowId) {
|
|
441
|
+
return null;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const nestedRowSlotContent = slots["nested-row"]?.({
|
|
445
|
+
row: props.row,
|
|
446
|
+
nestedLevel: props.nestedLevel,
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
const tdNode = h(
|
|
450
|
+
"td",
|
|
451
|
+
{ colspan: props.columns.length + Number(props.selectable) },
|
|
452
|
+
nestedRowSlotContent,
|
|
453
|
+
);
|
|
454
|
+
|
|
455
|
+
return h("tr", { class: props.row.class }, [tdNode]);
|
|
456
|
+
}
|
|
457
|
+
|
|
265
458
|
const { getDataTest } = useUI<Config>(defaultConfig);
|
|
266
459
|
</script>
|
|
267
460
|
|
|
268
461
|
<template>
|
|
269
|
-
<
|
|
270
|
-
v-if="!row.parentRowId || !hasSlotContent($slots['nested-row'])"
|
|
271
|
-
v-bind="{ ...$attrs, ...getRowAttrs() }"
|
|
272
|
-
:class="cx([getRowAttrs().class, getRowClasses(row)])"
|
|
273
|
-
@click="onClick(props.row)"
|
|
274
|
-
@dblclick="onDoubleClick(props.row)"
|
|
275
|
-
>
|
|
276
|
-
<td
|
|
277
|
-
v-if="selectable"
|
|
278
|
-
:style="{
|
|
279
|
-
...getNestedCheckboxShift(),
|
|
280
|
-
...(columns[0]?.sticky === StickySide.Left ? { left: '0' } : {}),
|
|
281
|
-
}"
|
|
282
|
-
v-bind="attrs.bodyCellCheckboxAttrs.value"
|
|
283
|
-
:class="
|
|
284
|
-
cx([
|
|
285
|
-
attrs.bodyCellCheckboxAttrs.value.class,
|
|
286
|
-
columns[0]?.sticky === StickySide.Left ? attrs.bodyCellStickyLeftAttrs.value.class : '',
|
|
287
|
-
])
|
|
288
|
-
"
|
|
289
|
-
@click.stop
|
|
290
|
-
@dblclick.stop
|
|
291
|
-
>
|
|
292
|
-
<UCheckbox
|
|
293
|
-
:model-value="isChecked"
|
|
294
|
-
size="md"
|
|
295
|
-
v-bind="attrs.bodyCheckboxAttrs.value"
|
|
296
|
-
:data-id="row.id"
|
|
297
|
-
:data-test="getDataTest('body-checkbox')"
|
|
298
|
-
@input="onInputCheckbox(row)"
|
|
299
|
-
/>
|
|
300
|
-
</td>
|
|
301
|
-
|
|
302
|
-
<td
|
|
303
|
-
v-for="(value, key, index) in mapRowColumns(row, columns)"
|
|
304
|
-
:key="index"
|
|
305
|
-
v-bind="attrs.bodyCellBaseAttrs.value"
|
|
306
|
-
:class="
|
|
307
|
-
cx([
|
|
308
|
-
columns[index].tdClass,
|
|
309
|
-
getCellClasses(row, String(key)),
|
|
310
|
-
getStickyColumnClass(columns[index]),
|
|
311
|
-
getSearchMatchCellClass(String(key)),
|
|
312
|
-
])
|
|
313
|
-
"
|
|
314
|
-
:style="getStickyColumnStyle(columns[index])"
|
|
315
|
-
@click="onClickCell(value, row, key)"
|
|
316
|
-
>
|
|
317
|
-
<div
|
|
318
|
-
v-if="(row.row || nestedLevel) && index === 0"
|
|
319
|
-
:style="getNestedShift()"
|
|
320
|
-
v-bind="attrs.bodyCellNestedAttrs.value"
|
|
321
|
-
>
|
|
322
|
-
<div
|
|
323
|
-
v-if="row.row"
|
|
324
|
-
:data-row-toggle-icon="row.id"
|
|
325
|
-
@dblclick.stop
|
|
326
|
-
@click.stop="onToggleExpand(row)"
|
|
327
|
-
>
|
|
328
|
-
<slot name="expand" :row="row" :expanded="isExpanded">
|
|
329
|
-
<div
|
|
330
|
-
ref="toggle-wrapper"
|
|
331
|
-
v-bind="attrs.bodyCellNestedIconWrapperAttrs.value"
|
|
332
|
-
:style="{ width: getIconWidth() }"
|
|
333
|
-
>
|
|
334
|
-
<UIcon
|
|
335
|
-
size="xs"
|
|
336
|
-
interactive
|
|
337
|
-
:name="getToggleIconName()"
|
|
338
|
-
color="primary"
|
|
339
|
-
v-bind="toggleIconConfig"
|
|
340
|
-
/>
|
|
341
|
-
</div>
|
|
342
|
-
</slot>
|
|
343
|
-
</div>
|
|
344
|
-
<slot :name="`cell-${key}`" :value="value" :row="row" :index="index">
|
|
345
|
-
<!-- eslint-disable vue/no-v-html -->
|
|
346
|
-
<div
|
|
347
|
-
v-if="shouldRenderCellWrapper(row, String(key))"
|
|
348
|
-
ref="cell"
|
|
349
|
-
v-bind="attrs.bodyCellContentAttrs.value"
|
|
350
|
-
:class="
|
|
351
|
-
cx([attrs.bodyCellContentAttrs.value.class, getCellContentClass(row, String(key))])
|
|
352
|
-
"
|
|
353
|
-
:data-test="getDataTest(`${key}-cell`)"
|
|
354
|
-
v-html="getHighlightedHtml(value, String(key))"
|
|
355
|
-
/>
|
|
356
|
-
<span v-else :data-test="getDataTest(`${key}-cell`)">{{ formatCellValue(value) }}</span>
|
|
357
|
-
</slot>
|
|
358
|
-
</div>
|
|
359
|
-
|
|
360
|
-
<template v-else>
|
|
361
|
-
<slot :name="`cell-${key}`" :value="value" :row="row" :index="index">
|
|
362
|
-
<!-- eslint-disable vue/no-v-html -->
|
|
363
|
-
<div
|
|
364
|
-
v-if="shouldRenderCellWrapper(row, String(key))"
|
|
365
|
-
v-bind="attrs.bodyCellContentAttrs.value"
|
|
366
|
-
ref="cell"
|
|
367
|
-
:class="
|
|
368
|
-
cx([attrs.bodyCellContentAttrs.value.class, getCellContentClass(row, String(key))])
|
|
369
|
-
"
|
|
370
|
-
:data-test="getDataTest(`${key}-cell`)"
|
|
371
|
-
v-html="getHighlightedHtml(value, String(key))"
|
|
372
|
-
/>
|
|
373
|
-
<span v-else :data-test="getDataTest(`${key}-cell`)">{{ formatCellValue(value) }}</span>
|
|
374
|
-
</slot>
|
|
375
|
-
</template>
|
|
376
|
-
</td>
|
|
377
|
-
</tr>
|
|
378
|
-
|
|
379
|
-
<tr
|
|
380
|
-
v-if="row.parentRowId && hasSlotContent($slots['nested-row'], { row, nestedLevel })"
|
|
381
|
-
:class="row.class"
|
|
382
|
-
>
|
|
383
|
-
<td :colspan="columns.length + Number(selectable)">
|
|
384
|
-
<slot name="nested-row" :row="row" :nested-level="nestedLevel" />
|
|
385
|
-
</td>
|
|
386
|
-
</tr>
|
|
462
|
+
<component :is="() => [renderMainRow(), renderNestedRow()].filter(Boolean)" />
|
|
387
463
|
</template>
|
|
@@ -775,9 +775,9 @@ describe("UTable.vue", () => {
|
|
|
775
775
|
it("Toggle Row Checkbox – emits update:selectedRows when row checkbox is clicked", async () => {
|
|
776
776
|
const component = mountUTable(getDefaultProps({ selectable: true }));
|
|
777
777
|
|
|
778
|
-
const
|
|
778
|
+
const checkboxCell = component.find("tbody tr td[data-checkbox-id]");
|
|
779
779
|
|
|
780
|
-
await
|
|
780
|
+
await checkboxCell.trigger("click");
|
|
781
781
|
|
|
782
782
|
expect(component.emitted("update:selectedRows")).toBeTruthy();
|
|
783
783
|
const emittedRows = component.emitted("update:selectedRows")![0][0] as Row[];
|
|
@@ -836,12 +836,24 @@ describe("UTable.vue", () => {
|
|
|
836
836
|
it("Multiple Row Selection – emits update:selectedRows with all selected rows", async () => {
|
|
837
837
|
const component = mountUTable(getDefaultProps({ selectable: true }));
|
|
838
838
|
|
|
839
|
-
const tableRows = component.findAll("tbody tr");
|
|
840
|
-
|
|
841
839
|
// Select first row
|
|
842
|
-
|
|
840
|
+
let tableRows = component.findAll("tbody tr");
|
|
841
|
+
|
|
842
|
+
await tableRows[0].find("td[data-checkbox-id]").trigger("click");
|
|
843
|
+
await nextTick();
|
|
844
|
+
|
|
845
|
+
// Update props with the first selected row
|
|
846
|
+
const firstEmit = component.emitted("update:selectedRows")![0][0] as Row[];
|
|
847
|
+
|
|
848
|
+
await component.setProps({ selectedRows: firstEmit });
|
|
849
|
+
await nextTick();
|
|
850
|
+
|
|
851
|
+
// Re-query the DOM after props update
|
|
852
|
+
tableRows = component.findAll("tbody tr");
|
|
853
|
+
|
|
843
854
|
// Select second row
|
|
844
|
-
await tableRows[1].find("
|
|
855
|
+
await tableRows[1].find("td[data-checkbox-id]").trigger("click");
|
|
856
|
+
await nextTick();
|
|
845
857
|
|
|
846
858
|
const emittedEvents = component.emitted("update:selectedRows");
|
|
847
859
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { flushPromises, mount } from "@vue/test-utils";
|
|
2
2
|
import { describe, it, expect, vi } from "vitest";
|
|
3
|
-
import { ref } from "vue";
|
|
3
|
+
import { ref, nextTick } from "vue";
|
|
4
4
|
|
|
5
5
|
import UTableRow from "../UTableRow.vue";
|
|
6
6
|
import UIcon from "../../ui.image-icon/UIcon.vue";
|
|
@@ -264,14 +264,17 @@ describe("UTableRow.vue", () => {
|
|
|
264
264
|
}),
|
|
265
265
|
});
|
|
266
266
|
|
|
267
|
-
|
|
267
|
+
let icon = component.findComponent(UIcon);
|
|
268
268
|
|
|
269
269
|
expect(icon.props("name")).toBe(defaultConfig.defaults.expandIcon);
|
|
270
270
|
|
|
271
|
-
component.setProps({ isExpanded: true });
|
|
272
|
-
|
|
271
|
+
await component.setProps({ isExpanded: true });
|
|
272
|
+
await nextTick();
|
|
273
273
|
await flushPromises();
|
|
274
274
|
|
|
275
|
+
// Re-query the icon after props update
|
|
276
|
+
icon = component.findComponent(UIcon);
|
|
277
|
+
|
|
275
278
|
expect(icon.props("name")).toBe(defaultConfig.defaults.collapseIcon);
|
|
276
279
|
});
|
|
277
280
|
|
|
@@ -290,106 +293,8 @@ describe("UTableRow.vue", () => {
|
|
|
290
293
|
});
|
|
291
294
|
});
|
|
292
295
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
const component = mount(UTableRow, {
|
|
296
|
-
props: getDefaultProps(),
|
|
297
|
-
});
|
|
298
|
-
|
|
299
|
-
await component.get("tr").trigger("click");
|
|
300
|
-
|
|
301
|
-
expect(component.emitted("click")).toBeTruthy();
|
|
302
|
-
expect(component.emitted("click")![0][0]).toEqual(defaultRow);
|
|
303
|
-
});
|
|
304
|
-
|
|
305
|
-
it("Double Click – emits dblclick event when row is double-clicked", async () => {
|
|
306
|
-
const component = mount(UTableRow, {
|
|
307
|
-
props: getDefaultProps(),
|
|
308
|
-
});
|
|
309
|
-
|
|
310
|
-
await component.get("tr").trigger("dblclick");
|
|
311
|
-
|
|
312
|
-
expect(component.emitted("dblclick")).toBeTruthy();
|
|
313
|
-
expect(component.emitted("dblclick")![0][0]).toEqual(defaultRow);
|
|
314
|
-
});
|
|
315
|
-
|
|
316
|
-
it("Click Cell – emits clickCell event when cell is clicked", async () => {
|
|
317
|
-
const component = mount(UTableRow, {
|
|
318
|
-
props: getDefaultProps(),
|
|
319
|
-
});
|
|
320
|
-
|
|
321
|
-
const firstCell = component.find("td");
|
|
322
|
-
|
|
323
|
-
await firstCell.trigger("click");
|
|
324
|
-
|
|
325
|
-
expect(component.emitted("clickCell")).toBeTruthy();
|
|
326
|
-
expect(component.emitted("clickCell")![0]).toEqual(["John Doe", defaultRow, "name"]);
|
|
327
|
-
});
|
|
328
|
-
|
|
329
|
-
it("Toggle Expand – emits toggleExpand event when expand icon is clicked", async () => {
|
|
330
|
-
const expandableRow: FlatRow = {
|
|
331
|
-
...defaultRow,
|
|
332
|
-
row: [{ id: "2", name: "Child", nestedLeveL: 1 }],
|
|
333
|
-
};
|
|
334
|
-
|
|
335
|
-
const component = mount(UTableRow, {
|
|
336
|
-
props: getDefaultProps({ row: expandableRow }),
|
|
337
|
-
});
|
|
338
|
-
|
|
339
|
-
const expandIcon = component.find("[data-row-toggle-icon='1']");
|
|
340
|
-
|
|
341
|
-
await expandIcon.trigger("click");
|
|
342
|
-
|
|
343
|
-
expect(component.emitted("toggleExpand")).toBeTruthy();
|
|
344
|
-
expect(component.emitted("toggleExpand")![0][0]).toEqual(expandableRow);
|
|
345
|
-
});
|
|
346
|
-
|
|
347
|
-
it("Toggle Checkbox – emits toggleCheckbox event when checkbox is changed", async () => {
|
|
348
|
-
const component = mount(UTableRow, {
|
|
349
|
-
props: getDefaultProps({ selectable: true }),
|
|
350
|
-
});
|
|
351
|
-
|
|
352
|
-
const checkbox = component.getComponent(UCheckbox);
|
|
353
|
-
|
|
354
|
-
await checkbox.vm.$emit("input", defaultRow);
|
|
355
|
-
|
|
356
|
-
expect(component.emitted("toggleCheckbox")).toBeTruthy();
|
|
357
|
-
expect(component.emitted("toggleCheckbox")![0][0]).toEqual(defaultRow);
|
|
358
|
-
});
|
|
359
|
-
|
|
360
|
-
it("Checkbox Cell – prevents row click events when checkbox cell is clicked", async () => {
|
|
361
|
-
const component = mount(UTableRow, {
|
|
362
|
-
props: getDefaultProps({ selectable: true }),
|
|
363
|
-
});
|
|
364
|
-
|
|
365
|
-
const checkboxCell = component.find("td");
|
|
366
|
-
|
|
367
|
-
await checkboxCell.trigger("click");
|
|
368
|
-
await checkboxCell.trigger("dblclick");
|
|
369
|
-
|
|
370
|
-
expect(component.emitted("click")).toBeFalsy();
|
|
371
|
-
expect(component.emitted("dblclick")).toBeFalsy();
|
|
372
|
-
});
|
|
373
|
-
|
|
374
|
-
it("Expand Icon – prevents row click events when expand icon is clicked", async () => {
|
|
375
|
-
const expandableRow: FlatRow = {
|
|
376
|
-
...defaultRow,
|
|
377
|
-
row: [{ id: "2", name: "Child", nestedLeveL: 1 }],
|
|
378
|
-
};
|
|
379
|
-
|
|
380
|
-
const component = mount(UTableRow, {
|
|
381
|
-
props: getDefaultProps({ row: expandableRow }),
|
|
382
|
-
});
|
|
383
|
-
|
|
384
|
-
const expandIcon = component.find("[data-row-toggle-icon='1']");
|
|
385
|
-
|
|
386
|
-
await expandIcon.trigger("click");
|
|
387
|
-
await expandIcon.trigger("dblclick");
|
|
388
|
-
|
|
389
|
-
expect(component.emitted("click")).toBeFalsy();
|
|
390
|
-
expect(component.emitted("dblclick")).toBeFalsy();
|
|
391
|
-
});
|
|
392
|
-
});
|
|
296
|
+
// Events are now handled by UTable via event delegation
|
|
297
|
+
// UTableRow no longer emits click, dblclick, clickCell, toggleExpand, or toggleCheckbox events
|
|
393
298
|
|
|
394
299
|
describe("Slots", () => {
|
|
395
300
|
it("Cell Slot – renders custom content from cell slot", () => {
|
package/ui.data-table/types.ts
CHANGED
|
@@ -48,6 +48,38 @@ const preparedNumber = computed(() => {
|
|
|
48
48
|
);
|
|
49
49
|
});
|
|
50
50
|
|
|
51
|
+
const formattedNumber = computed(() => {
|
|
52
|
+
let result = "";
|
|
53
|
+
|
|
54
|
+
if (props.currencyAlign === "left" && props.currency) {
|
|
55
|
+
result += props.currency;
|
|
56
|
+
|
|
57
|
+
if (props.currencySpace) {
|
|
58
|
+
result += " ";
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (props.value) {
|
|
63
|
+
result += mathSign.value;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
result += preparedNumber.value.integer;
|
|
67
|
+
|
|
68
|
+
if (props.maxFractionDigits > 0) {
|
|
69
|
+
result += preparedNumber.value.decimalSeparator + preparedNumber.value.fraction;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (props.currencyAlign === "right" && props.currency) {
|
|
73
|
+
if (props.currencySpace) {
|
|
74
|
+
result += " ";
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
result += props.currency;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return result;
|
|
81
|
+
});
|
|
82
|
+
|
|
51
83
|
defineExpose({
|
|
52
84
|
/**
|
|
53
85
|
* A reference to the component's wrapper element for direct DOM manipulation.
|
|
@@ -76,10 +108,12 @@ const {
|
|
|
76
108
|
<!-- @slot Use it to add something before the number. -->
|
|
77
109
|
<slot name="left" />
|
|
78
110
|
|
|
79
|
-
<
|
|
111
|
+
<template v-if="raw">{{ formattedNumber }}</template>
|
|
112
|
+
|
|
113
|
+
<div v-else v-bind="numberAttrs" :data-test="getDataTest()">
|
|
80
114
|
<span v-if="currencyAlign === 'left' && currency" v-bind="currencyAttrs" v-text="currency" />
|
|
81
115
|
|
|
82
|
-
<span v-if="value" v-bind="mathSignAttrs" v-text="mathSign" />
|
|
116
|
+
<span v-if="value && mathSign" v-bind="mathSignAttrs" v-text="mathSign" />
|
|
83
117
|
|
|
84
118
|
<span v-bind="integerAttrs" v-text="preparedNumber.integer" />
|
|
85
119
|
|
package/ui.text-number/config.ts
CHANGED
|
@@ -92,6 +92,17 @@ Sizes.args = { enum: "size" };
|
|
|
92
92
|
export const CurrencyAlign = EnumTemplate.bind({});
|
|
93
93
|
CurrencyAlign.args = { enum: "currencyAlign", currency: "USD", currencySpace: true };
|
|
94
94
|
|
|
95
|
+
export const Raw = DefaultTemplate.bind({});
|
|
96
|
+
Raw.args = { raw: true, currency: "USD", currencySpace: true };
|
|
97
|
+
Raw.parameters = {
|
|
98
|
+
docs: {
|
|
99
|
+
description: {
|
|
100
|
+
story:
|
|
101
|
+
"When `raw` is enabled, the number is displayed without formatting and html tags, showing only the raw value.",
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
|
|
95
106
|
export const LimitFractionDigits: StoryFn<UNumberArgs> = (args: UNumberArgs) => ({
|
|
96
107
|
components: { UNumber, UCol },
|
|
97
108
|
setup: () => ({ args }),
|
|
@@ -192,6 +192,96 @@ describe("UNumber.vue", () => {
|
|
|
192
192
|
});
|
|
193
193
|
});
|
|
194
194
|
|
|
195
|
+
it("Raw – renders formatted number as plain text without HTML elements when raw is true", () => {
|
|
196
|
+
const component = mount(UNumber, {
|
|
197
|
+
props: {
|
|
198
|
+
value,
|
|
199
|
+
raw: true,
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// Should render plain text without the number div structure
|
|
204
|
+
expect(component.find("[vl-key='number']").exists()).toBe(false);
|
|
205
|
+
expect(component.text()).toContain("1 234,56");
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("Raw – renders with currency when raw is true and currency is set", () => {
|
|
209
|
+
const currency = "$";
|
|
210
|
+
|
|
211
|
+
const component = mount(UNumber, {
|
|
212
|
+
props: {
|
|
213
|
+
value,
|
|
214
|
+
currency,
|
|
215
|
+
currencyAlign: "left",
|
|
216
|
+
raw: true,
|
|
217
|
+
},
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
expect(component.text()).toBe("$1 234,56");
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it("Raw – renders with currency and space when raw is true, currency is set, and currencySpace is true", () => {
|
|
224
|
+
const currency = "$";
|
|
225
|
+
|
|
226
|
+
const component = mount(UNumber, {
|
|
227
|
+
props: {
|
|
228
|
+
value,
|
|
229
|
+
currency,
|
|
230
|
+
currencyAlign: "left",
|
|
231
|
+
currencySpace: true,
|
|
232
|
+
raw: true,
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
expect(component.text()).toBe("$ 1 234,56");
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("Raw – renders with currency on right when raw is true and currencyAlign is right", () => {
|
|
240
|
+
const currency = "$";
|
|
241
|
+
|
|
242
|
+
const component = mount(UNumber, {
|
|
243
|
+
props: {
|
|
244
|
+
value,
|
|
245
|
+
currency,
|
|
246
|
+
currencyAlign: "right",
|
|
247
|
+
raw: true,
|
|
248
|
+
},
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
expect(component.text()).toBe("1 234,56$");
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// eslint-disable-next-line vue/max-len
|
|
255
|
+
it("Raw – renders with currency on right and space when raw is true, currencyAlign is right, and currencySpace is true", () => {
|
|
256
|
+
const currency = "$";
|
|
257
|
+
|
|
258
|
+
const component = mount(UNumber, {
|
|
259
|
+
props: {
|
|
260
|
+
value,
|
|
261
|
+
currency,
|
|
262
|
+
currencyAlign: "right",
|
|
263
|
+
currencySpace: true,
|
|
264
|
+
raw: true,
|
|
265
|
+
},
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
expect(component.text()).toBe("1 234,56 $");
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it("Raw – renders with sign when raw is true and sign is set", () => {
|
|
272
|
+
const testNegativeValue = -123;
|
|
273
|
+
|
|
274
|
+
const component = mount(UNumber, {
|
|
275
|
+
props: {
|
|
276
|
+
value: testNegativeValue,
|
|
277
|
+
sign: MATH_SIGN_TYPE.auto as Props["sign"],
|
|
278
|
+
raw: true,
|
|
279
|
+
},
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
expect(component.text()).toContain(MATH_SIGN.MINUS);
|
|
283
|
+
});
|
|
284
|
+
|
|
195
285
|
it("MinFractionDigits – adds zeros to meet the minimum fraction digits requirement", () => {
|
|
196
286
|
const value = 123;
|
|
197
287
|
const minFractionDigits = 2;
|
package/ui.text-number/types.ts
CHANGED
|
@@ -48,6 +48,11 @@ export interface Props {
|
|
|
48
48
|
*/
|
|
49
49
|
currencySpace?: boolean;
|
|
50
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Show formatted number as plain text without HTML elements.
|
|
53
|
+
*/
|
|
54
|
+
raw?: boolean;
|
|
55
|
+
|
|
51
56
|
/**
|
|
52
57
|
* Minimal number of signs after the decimal separator (fractional part of a number).
|
|
53
58
|
*/
|