@meistrari/tela-build 1.42.1 → 1.43.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/components/tela/badge/badge.vue +1 -1
- package/components/tela/complex-table/complex-table-fallback-skeleton.vue +39 -0
- package/components/tela/complex-table/complex-table-header.vue +3 -1
- package/components/tela/complex-table/complex-table-row.vue +29 -17
- package/components/tela/complex-table/complex-table-virtualized.vue +149 -31
- package/components/tela/complex-table/complex-table.mdx +16 -1
- package/components/tela/complex-table/complex-table.stories.ts +1 -5
- package/components/tela/complex-table/complex-table.vue +85 -13
- package/components/tela/complex-table/composables/horizontal-virtual-columns.ts +94 -0
- package/components/tela/complex-table/composables/table-common.ts +0 -7
- package/components/tela/complex-table/composables/virtual-scroll.ts +268 -27
- package/components/tela/complex-table/types.ts +8 -0
- package/components/tela/filter/filter-bar.vue +11 -0
- package/components/tela/filter/filter-content-item.vue +12 -0
- package/components/tela/filter/filter-content.vue +320 -0
- package/components/tela/filter/filter-trigger.vue +54 -0
- package/components/tela/filter/filter.mdx +293 -0
- package/components/tela/filter/filter.stories.ts +597 -0
- package/components/tela/filter/filter.vue +221 -0
- package/components/tela/filter/types.ts +25 -0
- package/components/tela/input/input.vue +8 -5
- package/components/tela/select-menu/select-menu-trigger.vue +1 -1
- package/composables/__tests__/horizontal-virtual-columns.test.ts +77 -0
- package/composables/__tests__/virtual-scroll.test.ts +215 -0
- package/package.json +1 -1
|
@@ -22,7 +22,7 @@ const tag = computed(() => props.to ? NuxtLink : 'div')
|
|
|
22
22
|
v-bind="props.to ? { to: props.to, target: props.target } : {}"
|
|
23
23
|
:class="cn(
|
|
24
24
|
'inline-block px-[5px] py-[3px] rounded-[5px] select-none',
|
|
25
|
-
variant === 'outline' && 'border-[0.5px] border-border',
|
|
25
|
+
variant === 'outline' && 'bg-background border-[0.5px] border-border',
|
|
26
26
|
variant === 'filled' && 'bg-lowered',
|
|
27
27
|
props.class,
|
|
28
28
|
)"
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
defineOptions({
|
|
3
|
+
name: 'TelaComplexTableFallbackSkeleton',
|
|
4
|
+
})
|
|
5
|
+
|
|
6
|
+
const props = withDefaults(defineProps<{
|
|
7
|
+
rowHeight?: number
|
|
8
|
+
rows?: number
|
|
9
|
+
height?: number | string
|
|
10
|
+
}>(), {
|
|
11
|
+
rowHeight: 64,
|
|
12
|
+
rows: 1,
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
const skeletonStyle = computed(() => ({
|
|
16
|
+
'--complex-table-fallback-row-height': `${props.rowHeight}px`,
|
|
17
|
+
'height': typeof props.height === 'number' ? `${props.height}px` : props.height ?? `${props.rows * props.rowHeight}px`,
|
|
18
|
+
}))
|
|
19
|
+
</script>
|
|
20
|
+
|
|
21
|
+
<template>
|
|
22
|
+
<div
|
|
23
|
+
aria-hidden="true"
|
|
24
|
+
class="complex-table-fallback-skeleton"
|
|
25
|
+
:style="skeletonStyle"
|
|
26
|
+
/>
|
|
27
|
+
</template>
|
|
28
|
+
|
|
29
|
+
<style scoped>
|
|
30
|
+
.complex-table-fallback-skeleton {
|
|
31
|
+
width: 100%;
|
|
32
|
+
min-height: var(--complex-table-fallback-row-height);
|
|
33
|
+
background-color: white;
|
|
34
|
+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='1380' height='64' viewBox='0 0 1380 64'%3E%3Cg fill='%23EFF1F3'%3E%3Crect x='100' y='28' width='34' height='8' rx='4'/%3E%3Crect x='146' y='28' width='48' height='8' rx='4'/%3E%3Crect x='206' y='28' width='200' height='8' rx='4'/%3E%3Crect x='418' y='28' width='90' height='8' rx='4'/%3E%3Crect x='520' y='28' width='180' height='8' rx='4'/%3E%3Crect x='808' y='28' width='80' height='8' rx='4'/%3E%3Crect x='1012' y='28' width='80' height='8' rx='4'/%3E%3Crect x='1156' y='28' width='80' height='8' rx='4'/%3E%3Crect x='1300' y='28' width='80' height='8' rx='4'/%3E%3C/g%3E%3C/svg%3E");
|
|
35
|
+
background-position: -100px 0;
|
|
36
|
+
background-repeat: repeat-y;
|
|
37
|
+
background-size: 1380px var(--complex-table-fallback-row-height);
|
|
38
|
+
}
|
|
39
|
+
</style>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import type { Column, Row } from './types'
|
|
3
3
|
import TelaTableHeaderCell from './complex-table-header-cell.vue'
|
|
4
|
+
import { isHorizontalSpacerColumn } from './composables/horizontal-virtual-columns'
|
|
4
5
|
|
|
5
6
|
defineOptions({
|
|
6
7
|
name: 'TelaComplexTableHeader',
|
|
@@ -43,7 +44,8 @@ const emit = defineEmits(['selectAll'])
|
|
|
43
44
|
:class="headerClass"
|
|
44
45
|
:style="column.style"
|
|
45
46
|
>
|
|
46
|
-
<
|
|
47
|
+
<div v-if="isHorizontalSpacerColumn(column)" h-56px bg-white border-b-0.5px border-gray-200 />
|
|
48
|
+
<slot v-else-if="column.key && $slots[`header-${column.key}`] !== undefined" :name="`header-${column.key}`" :column="column" />
|
|
47
49
|
<TelaTableHeaderCell v-else :column="column" type="default" />
|
|
48
50
|
</th>
|
|
49
51
|
<th v-for="(_, idx) in (rows?.length ? 0 : 3)" :key="idx" sticky top-0px p-0px />
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import type { Column, Row, HoverMechanism } from './types'
|
|
3
3
|
import { HoverMechanismTypes } from './types'
|
|
4
4
|
import TelaTableCell from './complex-table-cell.vue'
|
|
5
|
+
import { isHorizontalSpacerColumn } from './composables/horizontal-virtual-columns'
|
|
5
6
|
import { isLoading, hasContent, hasError, getRowTitle, readRowPath } from './utils'
|
|
6
7
|
|
|
7
8
|
const props = defineProps<{
|
|
@@ -15,7 +16,6 @@ const props = defineProps<{
|
|
|
15
16
|
loading?: boolean
|
|
16
17
|
disableRowClick?: boolean
|
|
17
18
|
hoverMechanism?: HoverMechanism
|
|
18
|
-
hoveredRowIndex?: number | null
|
|
19
19
|
rowClass?: string | ((row: Row) => string)
|
|
20
20
|
rowIndexClass?: string
|
|
21
21
|
rowTitlePath?: string
|
|
@@ -26,12 +26,13 @@ const props = defineProps<{
|
|
|
26
26
|
totalRows?: number
|
|
27
27
|
hideError?: boolean
|
|
28
28
|
selectClass?: string
|
|
29
|
+
showBorder?: boolean
|
|
30
|
+
borderMode?: 'default' | 'visible'
|
|
29
31
|
}>()
|
|
30
32
|
|
|
31
33
|
const emit = defineEmits<{
|
|
32
34
|
select: [id: string]
|
|
33
35
|
open: [id: string]
|
|
34
|
-
hover: [index: number | null]
|
|
35
36
|
}>()
|
|
36
37
|
|
|
37
38
|
const slots = useSlots()
|
|
@@ -42,23 +43,27 @@ const computedRowClass = computed(() => {
|
|
|
42
43
|
// Base classes
|
|
43
44
|
classes.push('bg-white')
|
|
44
45
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
classes.push('b-b-0.5px b-t-0px b-gray-200!')
|
|
46
|
+
if (props.borderMode === 'visible') {
|
|
47
|
+
classes.push('b-b-0.5px! b-t-0px!', props.showBorder === false ? 'b-transparent!' : 'b-gray-200!')
|
|
48
48
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
49
|
+
else {
|
|
50
|
+
if (props.showBorder !== false) {
|
|
51
|
+
// Border classes
|
|
52
|
+
if (props.isFirstRow && props.totalRows && props.totalRows > 1) {
|
|
53
|
+
classes.push('b-b-0.5px b-t-0px b-gray-200!')
|
|
54
|
+
}
|
|
55
|
+
if (props.rowIndex % 2 !== 0 && props.rowIndex > 0) {
|
|
56
|
+
classes.push('b-y-0.5px b-gray-200!')
|
|
57
|
+
}
|
|
58
|
+
if (props.isLastRow) {
|
|
59
|
+
classes.push('b-b-0px!')
|
|
60
|
+
}
|
|
61
|
+
}
|
|
54
62
|
}
|
|
55
63
|
|
|
56
64
|
// Hover classes
|
|
57
65
|
if (props.hoverMechanism === HoverMechanismTypes.Row) {
|
|
58
|
-
classes.push('cursor-pointer')
|
|
59
|
-
if (props.hoveredRowIndex === props.rowIndex) {
|
|
60
|
-
classes.push('bg-gray-50!')
|
|
61
|
-
}
|
|
66
|
+
classes.push('cursor-pointer', 'hover:bg-gray-50!')
|
|
62
67
|
}
|
|
63
68
|
|
|
64
69
|
// Custom row class
|
|
@@ -93,15 +98,21 @@ function handleCheckboxChange() {
|
|
|
93
98
|
<tr
|
|
94
99
|
h-64px
|
|
95
100
|
:class="computedRowClass"
|
|
96
|
-
@mouseenter="emit('hover', rowIndex)"
|
|
97
|
-
@mouseleave="emit('hover', null)"
|
|
98
101
|
@click.stop="handleRowClick"
|
|
99
102
|
>
|
|
100
103
|
<td v-if="allowSelect" class="sticky left-0 bg-white z-9" :class="[selectClass]" @click.stop>
|
|
101
104
|
<div
|
|
102
105
|
flex="~" h-px items-center justify-center w-32px relative z-0
|
|
103
106
|
>
|
|
107
|
+
<TelaSkeleton
|
|
108
|
+
v-if="isLoading(row, loading || false) && !row.id"
|
|
109
|
+
h-16px
|
|
110
|
+
rounded="2px"
|
|
111
|
+
w-16px
|
|
112
|
+
bg="#E9ECEF"
|
|
113
|
+
/>
|
|
104
114
|
<TelaCheckbox
|
|
115
|
+
v-else
|
|
105
116
|
:model-value="isSelected || false"
|
|
106
117
|
@update:model-value="handleCheckboxChange"
|
|
107
118
|
/>
|
|
@@ -148,7 +159,8 @@ function handleCheckboxChange() {
|
|
|
148
159
|
:key="columnIdx"
|
|
149
160
|
:style="column.style"
|
|
150
161
|
>
|
|
151
|
-
<div v-if="
|
|
162
|
+
<div v-if="isHorizontalSpacerColumn(column)" h-64px :class="rowClass" />
|
|
163
|
+
<div v-else-if="isLoading(row, loading || false)" flex="~ col" gap-4px pl-12px h-64px justify-center :class="rowClass">
|
|
152
164
|
<TelaSkeleton w="70%" h-8px />
|
|
153
165
|
</div>
|
|
154
166
|
<template v-else>
|
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import type { Column,
|
|
2
|
+
import type { Column, ComplexTableType } from './types'
|
|
3
3
|
import { HoverMechanismTypes } from './types'
|
|
4
4
|
import TelaTableHeader from './complex-table-header.vue'
|
|
5
5
|
import TelaTableRow from './complex-table-row.vue'
|
|
6
|
+
import TelaComplexTableFallbackSkeleton from './complex-table-fallback-skeleton.vue'
|
|
7
|
+
import {
|
|
8
|
+
createHorizontalSpacerColumn,
|
|
9
|
+
getColumnVirtualWidth,
|
|
10
|
+
getHorizontalVirtualColumns,
|
|
11
|
+
HORIZONTAL_LEFT_SPACER_COLUMN_KEY,
|
|
12
|
+
HORIZONTAL_RIGHT_SPACER_COLUMN_KEY,
|
|
13
|
+
} from './composables/horizontal-virtual-columns'
|
|
6
14
|
import { useTableSelection } from './composables/table-selection'
|
|
7
15
|
import { useTableCommon } from './composables/table-common'
|
|
8
16
|
import { useVirtualScroll } from './composables/virtual-scroll'
|
|
@@ -36,13 +44,11 @@ const {
|
|
|
36
44
|
const selectedRows = computed(() => props.selectedRows ?? internalSelectedRows.value)
|
|
37
45
|
|
|
38
46
|
const {
|
|
39
|
-
hoveredRowIndex,
|
|
40
47
|
hasHorizontalScroll,
|
|
41
48
|
handleRowSelect,
|
|
42
49
|
handleSelectAllRows,
|
|
43
50
|
handleScroll,
|
|
44
51
|
updateScrollButtonsState,
|
|
45
|
-
handleRowHover,
|
|
46
52
|
} = useTableCommon({
|
|
47
53
|
props,
|
|
48
54
|
emit,
|
|
@@ -54,8 +60,44 @@ const {
|
|
|
54
60
|
})
|
|
55
61
|
|
|
56
62
|
const hoverMechanism = computed(() => props.hoverMechanism || HoverMechanismTypes.Cell)
|
|
57
|
-
const
|
|
63
|
+
const horizontalScrollLeft = ref(0)
|
|
64
|
+
const horizontalViewportWidth = ref(0)
|
|
65
|
+
const defaultFixedColumnsWidth = computed(() => {
|
|
66
|
+
const selectWidth = props.allowSelect && props.rows.length ? 32 : 0
|
|
67
|
+
const rowIndexWidth = props.allowRowIndex ? 48 : 0
|
|
68
|
+
return selectWidth + rowIndexWidth
|
|
69
|
+
})
|
|
70
|
+
const fixedColumnsWidth = computed(() => props.virtualizedFixedColumnsWidth ?? defaultFixedColumnsWidth.value)
|
|
71
|
+
const horizontalVirtualColumns = computed(() => getHorizontalVirtualColumns({
|
|
72
|
+
columns: props.columns,
|
|
73
|
+
getColumnWidth: column => getColumnVirtualWidth(column, props.virtualizedDefaultColumnWidth ?? 200),
|
|
74
|
+
scrollLeft: horizontalScrollLeft.value,
|
|
75
|
+
viewportWidth: horizontalViewportWidth.value,
|
|
76
|
+
fixedWidth: fixedColumnsWidth.value,
|
|
77
|
+
overscanPx: props.virtualizedColumnOverscan ?? 480,
|
|
78
|
+
}))
|
|
79
|
+
const renderedColumns = computed<Column[]>(() => {
|
|
80
|
+
if (!props.virtualizedColumns) {
|
|
81
|
+
return props.columns
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const result: Column[] = []
|
|
85
|
+
|
|
86
|
+
if (horizontalVirtualColumns.value.leftSpacerWidth > 0) {
|
|
87
|
+
result.push(createHorizontalSpacerColumn(HORIZONTAL_LEFT_SPACER_COLUMN_KEY, horizontalVirtualColumns.value.leftSpacerWidth))
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
result.push(...horizontalVirtualColumns.value.visibleColumns)
|
|
91
|
+
|
|
92
|
+
if (horizontalVirtualColumns.value.rightSpacerWidth > 0) {
|
|
93
|
+
result.push(createHorizontalSpacerColumn(HORIZONTAL_RIGHT_SPACER_COLUMN_KEY, horizontalVirtualColumns.value.rightSpacerWidth))
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return result
|
|
97
|
+
})
|
|
98
|
+
const columnsWithHeaders: ComputedRef<Column[]> = computed(() => renderedColumns.value.filter((column: Column) => slots[`header-${column.key}`] !== undefined))
|
|
58
99
|
const shouldHideScrollbar = computed(() => props.hideScrollbar || (!hasHorizontalScroll.value && !props.showVerticalScrollbar))
|
|
100
|
+
const rowHeight = computed(() => props.virtualizedRowHeight ?? 64)
|
|
59
101
|
|
|
60
102
|
// Track if we're currently loading more data to prevent duplicate requests
|
|
61
103
|
const isLoadingMore = ref(false)
|
|
@@ -65,35 +107,56 @@ const virtualScroll = props.useVirtualization !== false
|
|
|
65
107
|
? useVirtualScroll({
|
|
66
108
|
count: computed(() => props.rows.length),
|
|
67
109
|
getScrollElement: () => mainTableEl.value || null,
|
|
68
|
-
estimateSize:
|
|
110
|
+
estimateSize: rowHeight.value,
|
|
69
111
|
overscan: props.virtualizedOverscan ?? 5,
|
|
112
|
+
maxScrollItemsPerEvent: props.virtualizedMaxScrollRowsPerEvent,
|
|
70
113
|
initialBatchSize: props.virtualizedInitialBatchSize ?? 30,
|
|
71
114
|
})
|
|
72
115
|
: null
|
|
73
116
|
|
|
117
|
+
const spacerColspan = computed(() => renderedColumns.value.length + (props.allowSelect ? 1 : 0) + (props.allowRowIndex ? 1 : 0) + 2)
|
|
118
|
+
const shouldShowVirtualFallbackBackground = computed(() => Boolean(virtualScroll && props.virtualizedShowBlankStateLoading && props.rows.length))
|
|
119
|
+
const isVirtualFallbackBackgroundVisible = computed(() => shouldShowVirtualFallbackBackground.value && Boolean(virtualScroll?.isScrollingFast.value))
|
|
120
|
+
const virtualTableContainerStyle = computed(() => ({
|
|
121
|
+
maxHeight: `${props.maxHeight || 462.5}px`,
|
|
122
|
+
}))
|
|
123
|
+
const visibleRange = computed(() => virtualScroll?.visibleRange.value)
|
|
124
|
+
|
|
125
|
+
function isRowVisible(index: number) {
|
|
126
|
+
const range = visibleRange.value
|
|
127
|
+
return !range || (index >= range.startIndex && index <= range.endIndex)
|
|
128
|
+
}
|
|
129
|
+
|
|
74
130
|
const displayRows = computed(() => {
|
|
75
131
|
if (!virtualScroll) {
|
|
76
|
-
return props.rows.map((row, index) => ({ row, index }))
|
|
132
|
+
return props.rows.map((row, index) => ({ row, index, isVisible: true }))
|
|
77
133
|
}
|
|
78
134
|
|
|
79
|
-
return virtualScroll.virtualItems.value.
|
|
80
|
-
row
|
|
81
|
-
|
|
82
|
-
|
|
135
|
+
return virtualScroll.virtualItems.value.flatMap((virtualItem) => {
|
|
136
|
+
const row = props.rows[virtualItem.index]
|
|
137
|
+
|
|
138
|
+
return row
|
|
139
|
+
? [{
|
|
140
|
+
row,
|
|
141
|
+
index: virtualItem.index,
|
|
142
|
+
isVisible: isRowVisible(virtualItem.index),
|
|
143
|
+
}]
|
|
144
|
+
: []
|
|
145
|
+
})
|
|
83
146
|
})
|
|
84
147
|
|
|
85
148
|
const topSpacerHeight = computed(() => {
|
|
86
149
|
if (!virtualScroll)
|
|
87
150
|
return 0
|
|
88
151
|
const startIndex = virtualScroll.range.value.startIndex
|
|
89
|
-
return startIndex *
|
|
152
|
+
return startIndex * rowHeight.value
|
|
90
153
|
})
|
|
91
154
|
|
|
92
155
|
const bottomSpacerHeight = computed(() => {
|
|
93
156
|
if (!virtualScroll)
|
|
94
157
|
return 0
|
|
95
158
|
const endIndex = virtualScroll.range.value.endIndex
|
|
96
|
-
return (props.rows.length - endIndex - 1) *
|
|
159
|
+
return (props.rows.length - endIndex - 1) * rowHeight.value
|
|
97
160
|
})
|
|
98
161
|
|
|
99
162
|
const shouldShowTopSpacer = computed(() => {
|
|
@@ -104,11 +167,41 @@ const shouldShowBottomSpacer = computed(() => {
|
|
|
104
167
|
return virtualScroll && bottomSpacerHeight.value > 0
|
|
105
168
|
})
|
|
106
169
|
|
|
107
|
-
|
|
170
|
+
function updateTableMeasurements() {
|
|
171
|
+
updateScrollButtonsState()
|
|
172
|
+
updateHorizontalScrollState(mainTableEl.value)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function updateHorizontalScrollState(el?: HTMLElement) {
|
|
176
|
+
if (!el)
|
|
177
|
+
return
|
|
178
|
+
|
|
179
|
+
if (horizontalScrollLeft.value !== el.scrollLeft) {
|
|
180
|
+
horizontalScrollLeft.value = el.scrollLeft
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (horizontalViewportWidth.value !== el.clientWidth) {
|
|
184
|
+
horizontalViewportWidth.value = el.clientWidth
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function handleTableScroll(event: Event) {
|
|
189
|
+
updateHorizontalScrollState(event.target as HTMLElement)
|
|
190
|
+
handleScroll(event)
|
|
191
|
+
}
|
|
192
|
+
|
|
108
193
|
let lastLoadMoreTriggeredAt = 0
|
|
194
|
+
// Infinite scroll with virtualization support
|
|
109
195
|
watch(() => virtualScroll?.range.value, async (newRange) => {
|
|
110
|
-
if (!newRange || !virtualScroll
|
|
196
|
+
if (!newRange || !virtualScroll)
|
|
197
|
+
return
|
|
198
|
+
|
|
199
|
+
if (!mainTableEl.value?.clientHeight)
|
|
200
|
+
return
|
|
201
|
+
|
|
202
|
+
if (isLoadingMore.value)
|
|
111
203
|
return
|
|
204
|
+
|
|
112
205
|
// Check if we're near the end and haven't recently triggered a load
|
|
113
206
|
const now = Date.now()
|
|
114
207
|
if (newRange.endIndex >= props.rows.length - 10 && now - lastLoadMoreTriggeredAt > 500) {
|
|
@@ -127,7 +220,7 @@ watch(() => virtualScroll?.range.value, async (newRange) => {
|
|
|
127
220
|
emit('error', error instanceof Error ? error : new Error(String(error)))
|
|
128
221
|
}
|
|
129
222
|
}
|
|
130
|
-
}
|
|
223
|
+
})
|
|
131
224
|
|
|
132
225
|
// Fallback for non-virtualized mode
|
|
133
226
|
if (!virtualScroll) {
|
|
@@ -142,20 +235,27 @@ if (!virtualScroll) {
|
|
|
142
235
|
}
|
|
143
236
|
|
|
144
237
|
onMounted(() => {
|
|
145
|
-
|
|
146
|
-
window.addEventListener('resize',
|
|
238
|
+
updateTableMeasurements()
|
|
239
|
+
window.addEventListener('resize', updateTableMeasurements)
|
|
147
240
|
virtualScroll?.init()
|
|
148
241
|
})
|
|
149
242
|
|
|
150
243
|
onUnmounted(() => {
|
|
151
|
-
window.removeEventListener('resize',
|
|
244
|
+
window.removeEventListener('resize', updateTableMeasurements)
|
|
152
245
|
virtualScroll?.destroy()
|
|
153
246
|
})
|
|
154
247
|
|
|
155
248
|
watch(() => props.columns, () => {
|
|
156
|
-
nextTick(
|
|
249
|
+
nextTick(updateTableMeasurements)
|
|
157
250
|
}, { deep: true })
|
|
158
251
|
|
|
252
|
+
useResizeObserver(mainTableEl, (entries) => {
|
|
253
|
+
const width = entries[0]?.contentRect.width
|
|
254
|
+
if (width && horizontalViewportWidth.value !== width) {
|
|
255
|
+
horizontalViewportWidth.value = width
|
|
256
|
+
}
|
|
257
|
+
})
|
|
258
|
+
|
|
159
259
|
const hasRowIndex = computed(() => props.allowRowIndex && props.rows.length)
|
|
160
260
|
const hasSelect = computed(() => props.allowSelect && props.rows.length)
|
|
161
261
|
|
|
@@ -180,6 +280,7 @@ watch([hasRowIndex, hasSelect, mainTableEl], () => {
|
|
|
180
280
|
|
|
181
281
|
<template>
|
|
182
282
|
<div
|
|
283
|
+
class="relative"
|
|
183
284
|
rounded-12px flex="~"
|
|
184
285
|
b="0.5px gray-300"
|
|
185
286
|
overflow-hidden
|
|
@@ -188,18 +289,28 @@ watch([hasRowIndex, hasSelect, mainTableEl], () => {
|
|
|
188
289
|
boxShadow: '0px 2px 5px 0px #677F940D, 0px 3px 16px 0px #677F940A',
|
|
189
290
|
}"
|
|
190
291
|
>
|
|
292
|
+
<TelaComplexTableFallbackSkeleton
|
|
293
|
+
v-if="shouldShowVirtualFallbackBackground"
|
|
294
|
+
v-show="isVirtualFallbackBackgroundVisible"
|
|
295
|
+
class="virtual-scroll-fallback-skeleton"
|
|
296
|
+
:row-height="rowHeight"
|
|
297
|
+
height="100%"
|
|
298
|
+
/>
|
|
191
299
|
<div
|
|
192
300
|
ref="mainTableEl"
|
|
193
301
|
w-full
|
|
194
302
|
class="main-table-container overflow-auto"
|
|
195
303
|
relative
|
|
196
|
-
:class="{
|
|
197
|
-
|
|
198
|
-
|
|
304
|
+
:class="{
|
|
305
|
+
'hide-scrollbar': shouldHideScrollbar,
|
|
306
|
+
'show-vertical-scrollbar': props.showVerticalScrollbar,
|
|
307
|
+
}"
|
|
308
|
+
:style="virtualTableContainerStyle"
|
|
309
|
+
@scroll.passive="handleTableScroll"
|
|
199
310
|
>
|
|
200
311
|
<table z-2 class="relative w-full min-w-full">
|
|
201
312
|
<TelaTableHeader
|
|
202
|
-
:columns="
|
|
313
|
+
:columns="renderedColumns"
|
|
203
314
|
:rows="rows"
|
|
204
315
|
:selected-rows="selectedRows"
|
|
205
316
|
:allow-select="allowSelect"
|
|
@@ -223,15 +334,15 @@ watch([hasRowIndex, hasSelect, mainTableEl], () => {
|
|
|
223
334
|
|
|
224
335
|
<tbody v-if="rows.length">
|
|
225
336
|
<tr v-if="shouldShowTopSpacer" :style="{ height: `${topSpacerHeight}px` }">
|
|
226
|
-
<td :colspan="
|
|
337
|
+
<td :colspan="spacerColspan" />
|
|
227
338
|
</tr>
|
|
228
339
|
|
|
229
340
|
<TelaTableRow
|
|
230
|
-
v-for="{ row: rowData, index: idx } in displayRows"
|
|
341
|
+
v-for="{ row: rowData, index: idx, isVisible } in displayRows"
|
|
231
342
|
:key="rowData.id || idx"
|
|
232
343
|
:row="rowData"
|
|
233
344
|
:row-index="idx"
|
|
234
|
-
:columns="
|
|
345
|
+
:columns="renderedColumns"
|
|
235
346
|
:allow-select="allowSelect"
|
|
236
347
|
:allow-row-index="allowRowIndex"
|
|
237
348
|
:allow-row-action="$slots['row-action'] !== undefined"
|
|
@@ -239,7 +350,6 @@ watch([hasRowIndex, hasSelect, mainTableEl], () => {
|
|
|
239
350
|
:loading="props.loading"
|
|
240
351
|
:disable-row-click="props.disableRowClick?.(rowData)"
|
|
241
352
|
:hover-mechanism="hoverMechanism"
|
|
242
|
-
:hovered-row-index="hoveredRowIndex"
|
|
243
353
|
:row-class="props.rowClass"
|
|
244
354
|
:row-index-class="props.rowIndexClass"
|
|
245
355
|
:row-title-path="props.rowTitlePath"
|
|
@@ -249,15 +359,16 @@ watch([hasRowIndex, hasSelect, mainTableEl], () => {
|
|
|
249
359
|
:is-last-row="idx === rows.length - 1"
|
|
250
360
|
:hide-error="props.hideError"
|
|
251
361
|
:total-rows="rows.length"
|
|
362
|
+
:show-border="isVisible"
|
|
363
|
+
:border-mode="virtualScroll ? 'visible' : 'default'"
|
|
252
364
|
@select="handleRowSelect"
|
|
253
365
|
@open="(id: string) => emit('open', id)"
|
|
254
|
-
@hover="handleRowHover"
|
|
255
366
|
>
|
|
256
367
|
<template #leading="slotProps">
|
|
257
368
|
<slot name="leading" v-bind="slotProps" />
|
|
258
369
|
</template>
|
|
259
370
|
|
|
260
|
-
<template v-for="column in
|
|
371
|
+
<template v-for="column in renderedColumns" :key="column.key" #[column.key!]="slotProps">
|
|
261
372
|
<slot v-if="column.key" :name="column.key" v-bind="slotProps" />
|
|
262
373
|
</template>
|
|
263
374
|
|
|
@@ -267,11 +378,11 @@ watch([hasRowIndex, hasSelect, mainTableEl], () => {
|
|
|
267
378
|
</TelaTableRow>
|
|
268
379
|
|
|
269
380
|
<tr v-if="shouldShowBottomSpacer" :style="{ height: `${bottomSpacerHeight}px` }">
|
|
270
|
-
<td :colspan="
|
|
381
|
+
<td :colspan="spacerColspan" />
|
|
271
382
|
</tr>
|
|
272
383
|
|
|
273
384
|
<tr v-if="$slots.footer && !virtualScroll" b="b-0.5px gray-200">
|
|
274
|
-
<td :colspan="
|
|
385
|
+
<td :colspan="renderedColumns.length + 2">
|
|
275
386
|
<div flex items-center h-64px b="t-0.5px gray-200" :class="props.rowClass">
|
|
276
387
|
<slot name="footer" />
|
|
277
388
|
</div>
|
|
@@ -319,4 +430,11 @@ thead th[style*="left"] {
|
|
|
319
430
|
thead th.bg-white {
|
|
320
431
|
background-color: white !important;
|
|
321
432
|
}
|
|
433
|
+
|
|
434
|
+
.virtual-scroll-fallback-skeleton {
|
|
435
|
+
position: absolute;
|
|
436
|
+
inset: 0;
|
|
437
|
+
z-index: 0;
|
|
438
|
+
pointer-events: none;
|
|
439
|
+
}
|
|
322
440
|
</style>
|
|
@@ -361,6 +361,7 @@ const handleError = (error) => {
|
|
|
361
361
|
interface Column {
|
|
362
362
|
key?: string
|
|
363
363
|
title: string
|
|
364
|
+
width?: number
|
|
364
365
|
isDefault?: boolean
|
|
365
366
|
icon?: {
|
|
366
367
|
name: string
|
|
@@ -375,6 +376,7 @@ interface Column {
|
|
|
375
376
|
interface Row {
|
|
376
377
|
id?: string
|
|
377
378
|
index?: number
|
|
379
|
+
renderKey?: string | number
|
|
378
380
|
status: string
|
|
379
381
|
name: string
|
|
380
382
|
errorMessage?: string
|
|
@@ -414,6 +416,12 @@ type ComplexTableProps = {
|
|
|
414
416
|
virtualizedOverscan?: number
|
|
415
417
|
virtualizedRowHeight?: number
|
|
416
418
|
virtualizedInitialBatchSize?: number
|
|
419
|
+
virtualizedMaxScrollRowsPerEvent?: number
|
|
420
|
+
virtualizedShowBlankStateLoading?: boolean
|
|
421
|
+
virtualizedColumns?: boolean
|
|
422
|
+
virtualizedColumnOverscan?: number
|
|
423
|
+
virtualizedFixedColumnsWidth?: number
|
|
424
|
+
virtualizedDefaultColumnWidth?: number
|
|
417
425
|
}
|
|
418
426
|
```
|
|
419
427
|
|
|
@@ -436,6 +444,7 @@ The ComplexTable system consists of these sub-components:
|
|
|
436
444
|
- `TelaComplexTableRow` - Individual row component
|
|
437
445
|
- `TelaComplexTableCell` - Default cell component
|
|
438
446
|
- `TelaComplexTableHeaderCell` - Header cell component
|
|
447
|
+
- `TelaComplexTableFallbackSkeleton` - Table-shaped skeleton fallback for virtual blank states and loading empty states
|
|
439
448
|
|
|
440
449
|
## Features
|
|
441
450
|
|
|
@@ -446,6 +455,7 @@ The ComplexTable system consists of these sub-components:
|
|
|
446
455
|
- **Status Indicators**: Built-in support for loading, success, and error states
|
|
447
456
|
- **Custom Rendering**: Slot-based customization for headers, cells, and rows
|
|
448
457
|
- **Virtualization**: Automatic virtualization for large datasets with configurable options
|
|
458
|
+
- **Column Virtualization**: Optional horizontal virtualization for tables with many columns
|
|
449
459
|
- **Infinite Scroll**: Built-in infinite scroll support for lazy loading
|
|
450
460
|
- **Error Handling**: Display error messages for failed rows
|
|
451
461
|
- **Empty States**: Customizable empty state when no data is available
|
|
@@ -479,6 +489,12 @@ When `useVirtualization` is enabled, the table automatically switches to a virtu
|
|
|
479
489
|
- `virtualizedRowHeight` - Height of each row in pixels (default: 64)
|
|
480
490
|
- `virtualizedOverscan` - Number of rows to render outside the visible area (default: 5)
|
|
481
491
|
- `virtualizedInitialBatchSize` - Initial number of rows to render (default: 30)
|
|
492
|
+
- `virtualizedMaxScrollRowsPerEvent` - Maximum estimated rows a single scroll event can move before the offset is clamped (default: unlimited)
|
|
493
|
+
- `virtualizedShowBlankStateLoading` - Shows a table-shaped skeleton background in blank virtualized areas while rows are being repainted during fast scrolling (default: false)
|
|
494
|
+
- `virtualizedColumns` - Enables horizontal column virtualization for the `columns` array (default: false)
|
|
495
|
+
- `virtualizedColumnOverscan` - Horizontal overscan in pixels for keeping nearby columns mounted (default: 480)
|
|
496
|
+
- `virtualizedFixedColumnsWidth` - Total width before the virtualized columns start, including select/index/leading columns when those areas should remain mounted (default: select + index width)
|
|
497
|
+
- `virtualizedDefaultColumnWidth` - Width used when a column does not provide `width` or a pixel `style` width (default: 200)
|
|
482
498
|
|
|
483
499
|
## Accessibility
|
|
484
500
|
|
|
@@ -487,4 +503,3 @@ When `useVirtualization` is enabled, the table automatically switches to a virtu
|
|
|
487
503
|
- ARIA attributes for screen readers
|
|
488
504
|
- Focus management
|
|
489
505
|
- Proper table headers and relationships
|
|
490
|
-
|
|
@@ -150,15 +150,11 @@ export const WithRowHover: Story = {
|
|
|
150
150
|
render: args => ({
|
|
151
151
|
components: { TelaComplexTable },
|
|
152
152
|
setup() {
|
|
153
|
-
return {
|
|
154
|
-
args,
|
|
155
|
-
onHover: (rowData: any) => console.log('Hovering row:', rowData),
|
|
156
|
-
}
|
|
153
|
+
return { args }
|
|
157
154
|
},
|
|
158
155
|
template: `
|
|
159
156
|
<TelaComplexTable
|
|
160
157
|
v-bind="args"
|
|
161
|
-
@hover="onHover"
|
|
162
158
|
/>
|
|
163
159
|
`,
|
|
164
160
|
}),
|