@witchcraft/ui 0.3.13 → 0.3.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/module.json +1 -1
- package/dist/runtime/assets/utils.css +1 -1
- package/dist/runtime/components/LibColorInput/LibColorInput.d.vue.ts +2 -2
- package/dist/runtime/components/LibColorInput/LibColorInput.vue.d.ts +2 -2
- package/dist/runtime/components/LibColorPicker/LibColorPicker.d.vue.ts +3 -3
- package/dist/runtime/components/LibColorPicker/LibColorPicker.vue.d.ts +3 -3
- package/dist/runtime/components/LibInputDeprecated/LibInputDeprecated.d.vue.ts +3 -3
- package/dist/runtime/components/LibInputDeprecated/LibInputDeprecated.vue.d.ts +3 -3
- package/dist/runtime/components/LibPopup/LibPopup.d.vue.ts +1 -1
- package/dist/runtime/components/LibPopup/LibPopup.vue.d.ts +1 -1
- package/dist/runtime/components/LibTable/LibTable.d.vue.ts +63 -5
- package/dist/runtime/components/LibTable/LibTable.vue +302 -99
- package/dist/runtime/components/LibTable/LibTable.vue.d.ts +63 -5
- package/dist/runtime/directives/vResizableCols.js +8 -5
- package/dist/runtime/types/index.d.ts +7 -3
- package/dist/runtime/utils/twMerge.d.ts +1 -0
- package/dist/runtime/utils/twMerge.js +2 -1
- package/package.json +3 -1
- package/src/runtime/assets/utils.css +5 -0
- package/src/runtime/components/LibTable/LibTable.stories.ts +174 -10
- package/src/runtime/components/LibTable/LibTable.vue +378 -107
- package/src/runtime/directives/vResizableCols.ts +9 -5
- package/src/runtime/types/index.ts +7 -3
- package/src/runtime/utils/twMerge.ts +2 -1
|
@@ -1,145 +1,355 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<!--
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
2
|
+
<!--
|
|
3
|
+
- moving the border to the wrapper is to hide the little bits of border sticking out
|
|
4
|
+
added back the right straight border otherwise the scrollbar looks ass
|
|
5
|
+
this is ever so slightly visible if there is no scrollbar
|
|
6
|
+
|
|
7
|
+
- relative is for the sticky header in dynamic mode
|
|
8
|
+
|
|
9
|
+
- dynamic mode REQUIRES grid since otherwise the transforms don't work because of how tanstack calculates them
|
|
10
|
+
- tried pre-calculating the transforms to take into account the previous elements (e.g. virtual.start - (height of previous rows)) but this was way to slow and buggy
|
|
11
|
+
-->
|
|
12
|
+
<div
|
|
13
|
+
:class="twMerge(`
|
|
14
|
+
table--container
|
|
15
|
+
overflow-auto
|
|
16
|
+
`,
|
|
17
|
+
hasScrollbar.vertical && `has-scrollbar-vertical`,
|
|
18
|
+
hasScrollbar.horizontal && `has-scrollbar-horizontal`,
|
|
19
|
+
stickyHeader && `
|
|
20
|
+
[&_thead]:sticky
|
|
21
|
+
[&_thead]:top-0
|
|
22
|
+
[&_thead]:z-1
|
|
23
|
+
[&_.grip]:z-2
|
|
24
|
+
`,
|
|
25
|
+
isPostSetup && `resizable-cols-setup`,
|
|
25
26
|
border && `
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
27
|
+
border
|
|
28
|
+
border-neutral-500
|
|
29
|
+
`,
|
|
30
|
+
border && cellBorder && `
|
|
31
|
+
[&.has-scrollbar-horizontal_.last-row]:border-b
|
|
32
|
+
[&.has-scrollbar-horizontal_.last-row]:border-neutral-500
|
|
33
|
+
[&.has-scrollbar-vertical_.last-col]:border-r
|
|
34
|
+
[&.has-scrollbar-vertical_.last-col]:border-neutral-500
|
|
35
|
+
`,
|
|
36
|
+
(!resizableOptions.fitWidth || stickyHeader) && `
|
|
37
|
+
[&_td.tr]:rounded-tr-none!
|
|
38
|
+
[&_td.br]:rounded-br-none!
|
|
39
|
+
`,
|
|
40
|
+
// this combo prevents the x-scrollbar from showing up when it shouldn't
|
|
41
|
+
// and max-w-fit allows the border to shrink with the table columns
|
|
42
|
+
resizableOptions.fitWidth === false && `
|
|
43
|
+
[&_.grip]:last:translate-x-[-5px]
|
|
44
|
+
mr-1
|
|
45
|
+
max-w-fit
|
|
46
|
+
`,
|
|
35
47
|
rounded &&`
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
48
|
+
rounded-md
|
|
49
|
+
`,
|
|
50
|
+
mergedVirtualizerOpts.enabled && mergedVirtualizerOpts.method === 'dynamic' && `
|
|
51
|
+
relative
|
|
40
52
|
`,
|
|
41
|
-
($attrs as any).
|
|
42
|
-
|
|
53
|
+
($attrs as any).wrapperClass)"
|
|
54
|
+
ref="parentRef"
|
|
43
55
|
>
|
|
44
|
-
<
|
|
45
|
-
|
|
46
|
-
|
|
56
|
+
<div
|
|
57
|
+
class="table--inner-container"
|
|
58
|
+
:style="{
|
|
59
|
+
...(mergedVirtualizerOpts.enabled
|
|
60
|
+
? { height: `${totalSize}px` }
|
|
61
|
+
: {})
|
|
62
|
+
}"
|
|
47
63
|
>
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
64
|
+
<!-- https://github.com/TanStack/virtual/issues/640#issuecomment-2795731690 -->
|
|
65
|
+
<table
|
|
66
|
+
:style="{
|
|
67
|
+
...(stickyHeader && mergedVirtualizerOpts.enabled
|
|
68
|
+
? { '--table-sticky-fix': `${totalSize-tableHeight}px` }
|
|
69
|
+
: {}),
|
|
70
|
+
...($attrs as any).style ?? {}
|
|
71
|
+
}"
|
|
72
|
+
:class="twMerge(`
|
|
73
|
+
table
|
|
74
|
+
table-fixed
|
|
75
|
+
border-separate
|
|
76
|
+
border-spacing-0
|
|
77
|
+
scrollbar-hidden
|
|
78
|
+
[&_.grip]:w-[5px]
|
|
79
|
+
relative
|
|
80
|
+
w-full
|
|
81
|
+
box-content
|
|
82
|
+
[&_thead]:font-bold
|
|
83
|
+
[&_td]:p-1
|
|
84
|
+
[&_th]:p-1
|
|
85
|
+
[&.resizable-cols-error]:cursor-not-allowed
|
|
86
|
+
[&.resizable-cols-error]:user-select-none
|
|
87
|
+
[&_thead_th]:bg-neutral-200
|
|
88
|
+
dark:[&_thead_th]:bg-neutral-800
|
|
89
|
+
`,
|
|
90
|
+
isPostSetup && mergedVirtualizerOpts.enabled && mergedVirtualizerOpts.method === 'dynamic' && `
|
|
91
|
+
grid
|
|
92
|
+
`,
|
|
93
|
+
stickyHeader && mergedVirtualizerOpts.enabled && mergedVirtualizerOpts.method === 'fixed' && `
|
|
94
|
+
after:inline-block
|
|
95
|
+
after:h-(--table-sticky-fix)
|
|
96
|
+
`,
|
|
97
|
+
cellBorder && `
|
|
98
|
+
[&_td]:border-neutral-500
|
|
99
|
+
[&_td:not(.last-row)]:border-b
|
|
100
|
+
[&_td:not(.first-col)]:border-l
|
|
101
|
+
[&_th]:border-neutral-500
|
|
102
|
+
[&_th:not(.last-row)]:border-b
|
|
103
|
+
[&_th:not(.first-col)]:border-l
|
|
104
|
+
`,
|
|
105
|
+
!cellBorder && `
|
|
106
|
+
[&_.grip]:hover:bg-neutral-300
|
|
107
|
+
dark:[&_.grip]:hover:bg-neutral-700
|
|
108
|
+
`,
|
|
109
|
+
($attrs as any).class)"
|
|
110
|
+
v-resizable-cols="resizableOptions"
|
|
111
|
+
>
|
|
112
|
+
<thead
|
|
113
|
+
v-if="header"
|
|
114
|
+
:class="twMerge(
|
|
115
|
+
`table--header`,
|
|
116
|
+
isPostSetup && mergedVirtualizerOpts.enabled && mergedVirtualizerOpts.method === 'dynamic' && `
|
|
117
|
+
grid
|
|
118
|
+
top-0
|
|
119
|
+
`
|
|
120
|
+
)"
|
|
52
121
|
>
|
|
53
|
-
<
|
|
54
|
-
:
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
? 'no-resize'
|
|
60
|
-
: ''
|
|
61
|
-
].join(' ')"
|
|
62
|
-
:style="`width:${widths.length > 0 ? widths[i] : ``}; `"
|
|
63
|
-
:col-key="col"
|
|
122
|
+
<tr
|
|
123
|
+
:class="twMerge(
|
|
124
|
+
`table--row`,
|
|
125
|
+
isPostSetup && mergedVirtualizerOpts.enabled && mergedVirtualizerOpts.method === 'dynamic'
|
|
126
|
+
&& `flex w-full`
|
|
127
|
+
)"
|
|
64
128
|
>
|
|
65
|
-
<
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
'cell table--header-cell',
|
|
69
|
-
(colConfig as any)[col]?.resizable === false
|
|
70
|
-
? 'no-resize'
|
|
71
|
-
: ''
|
|
72
|
-
].join(' ')"
|
|
73
|
-
:style="`width:${widths.length > 0 ? widths[i] : ``}; `"
|
|
129
|
+
<template
|
|
130
|
+
v-for="col, i of cols"
|
|
131
|
+
:key="col"
|
|
74
132
|
>
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
133
|
+
<slot
|
|
134
|
+
:name="`header-${col.toString()}`"
|
|
135
|
+
:class="classes[`-1-${i}`]"
|
|
136
|
+
:style="{ width: widths[i] }"
|
|
137
|
+
:col-key="col"
|
|
138
|
+
:config="(colConfig as any)[col]"
|
|
139
|
+
>
|
|
140
|
+
<th
|
|
141
|
+
:class="classes[`-1-${i}`]"
|
|
142
|
+
:style="{ width: widths[i] }"
|
|
143
|
+
>
|
|
144
|
+
{{ (colConfig as any)[col]?.name ?? col }}
|
|
145
|
+
</th>
|
|
146
|
+
</slot>
|
|
147
|
+
</template>
|
|
148
|
+
</tr>
|
|
149
|
+
</thead>
|
|
150
|
+
<tbody
|
|
151
|
+
:class="twMerge(
|
|
152
|
+
`table--body`,
|
|
153
|
+
isPostSetup && mergedVirtualizerOpts.enabled && mergedVirtualizerOpts.method === 'dynamic'
|
|
154
|
+
&& `grid relative`
|
|
155
|
+
)"
|
|
156
|
+
:style="{
|
|
157
|
+
...(mergedVirtualizerOpts.enabled && mergedVirtualizerOpts.method === 'dynamic'
|
|
158
|
+
? { height: `${totalSize}px` }
|
|
159
|
+
: {})
|
|
160
|
+
}"
|
|
161
|
+
>
|
|
87
162
|
<template
|
|
88
|
-
v-for="
|
|
89
|
-
:key="
|
|
163
|
+
v-for="(virtual, index) in virtualList"
|
|
164
|
+
:key="virtual.key"
|
|
90
165
|
>
|
|
91
|
-
<
|
|
92
|
-
:
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
166
|
+
<tr
|
|
167
|
+
:class="twMerge(`
|
|
168
|
+
table--row
|
|
169
|
+
`, isPostSetup && mergedVirtualizerOpts.enabled && mergedVirtualizerOpts.method === 'dynamic'
|
|
170
|
+
&& `flex absolute w-full`
|
|
171
|
+
)"
|
|
172
|
+
:style="{
|
|
173
|
+
...(mergedVirtualizerOpts.enabled
|
|
174
|
+
? {
|
|
175
|
+
transform: mergedVirtualizerOpts.method === 'fixed'
|
|
176
|
+
? `translateY(${virtual.start - index * virtual.size!}px)`
|
|
177
|
+
: `translateY(${virtual.start}px)`,
|
|
178
|
+
height: virtual.size
|
|
179
|
+
}
|
|
180
|
+
: {})
|
|
181
|
+
}"
|
|
182
|
+
:data-index="virtual.index"
|
|
183
|
+
:ref="measureElement"
|
|
96
184
|
>
|
|
97
|
-
<
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
185
|
+
<template
|
|
186
|
+
v-for="col, j of cols"
|
|
187
|
+
:key="virtual.key + '-' + col.toString()"
|
|
188
|
+
>
|
|
189
|
+
<slot
|
|
190
|
+
:name="col"
|
|
191
|
+
:item="values[virtual.index]"
|
|
192
|
+
:value="values[virtual.index][col]"
|
|
193
|
+
:style="{ width: widths[j] }"
|
|
194
|
+
:class="classes[`${virtual.index}-${j}`]"
|
|
195
|
+
>
|
|
196
|
+
<td
|
|
197
|
+
:style="{ width: widths[j] }"
|
|
198
|
+
:class="classes[`${virtual.index}-${j}`]"
|
|
199
|
+
>
|
|
200
|
+
{{ values[virtual.index][col] }}
|
|
201
|
+
</td>
|
|
202
|
+
</slot>
|
|
203
|
+
</template>
|
|
204
|
+
</tr>
|
|
101
205
|
</template>
|
|
102
|
-
</
|
|
103
|
-
</
|
|
104
|
-
</
|
|
105
|
-
</
|
|
206
|
+
</tbody>
|
|
207
|
+
</table>
|
|
208
|
+
</div>
|
|
209
|
+
</div>
|
|
106
210
|
</template>
|
|
107
211
|
|
|
108
212
|
<!-- generic="T extends Record<string, any> -->"
|
|
109
213
|
<script setup lang="ts" generic="T">
|
|
110
214
|
import type { MakeRequired } from "@alanscodelog/utils"
|
|
111
215
|
import { keys } from "@alanscodelog/utils/keys"
|
|
112
|
-
import {
|
|
216
|
+
import { throttle } from "@alanscodelog/utils/throttle"
|
|
217
|
+
import { useVirtualizer, type VirtualizerOptions } from "@tanstack/vue-virtual"
|
|
218
|
+
import { computed, onMounted, ref, type TableHTMLAttributes } from "vue"
|
|
113
219
|
|
|
220
|
+
import { useGlobalResizeObserver } from "../../composables/useGlobalResizeObserver.js"
|
|
114
221
|
import { vResizableCols } from "../../directives/vResizableCols.js"
|
|
115
222
|
import type { ResizableOptions, TableColConfig } from "../../types/index.js"
|
|
116
223
|
import { twMerge } from "../../utils/twMerge.js"
|
|
117
224
|
import type { TailwindClassProp } from "../shared/props.js"
|
|
118
225
|
|
|
119
226
|
defineOptions({
|
|
120
|
-
name: "LibTable"
|
|
227
|
+
name: "LibTable",
|
|
228
|
+
inheritAttrs: false
|
|
121
229
|
})
|
|
122
230
|
|
|
123
231
|
const props = withDefaults(defineProps<Props>(), {
|
|
124
232
|
resizable: () => ({}),
|
|
125
233
|
values: () => [] as T[],
|
|
126
|
-
itemKey: "",
|
|
127
234
|
cols: () => [] as (keyof T)[],
|
|
128
235
|
rounded: true,
|
|
129
236
|
border: true,
|
|
130
237
|
cellBorder: true,
|
|
131
238
|
header: true,
|
|
132
|
-
colConfig: () => ({})
|
|
239
|
+
colConfig: () => ({}),
|
|
240
|
+
virtualizerOptions: () => ({ }),
|
|
241
|
+
enableStickyHeader: false,
|
|
242
|
+
itemKey: ""
|
|
133
243
|
})
|
|
134
244
|
|
|
135
245
|
const widths = ref([])
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
const isPostSetup = ref(false)
|
|
136
249
|
const resizableOptions = computed<MakeRequired<Partial<ResizableOptions>, "colCount" | "widths">>(() => ({
|
|
137
250
|
colCount: props.cols.length,
|
|
138
251
|
widths,
|
|
139
252
|
selector: ".cell",
|
|
140
|
-
...props.resizable
|
|
253
|
+
...props.resizable,
|
|
254
|
+
onSetup: el => {
|
|
255
|
+
isPostSetup.value = true
|
|
256
|
+
if (props.resizable.onSetup) {
|
|
257
|
+
props.resizable.onSetup(el)
|
|
258
|
+
}
|
|
259
|
+
},
|
|
260
|
+
onTeardown: el => {
|
|
261
|
+
isPostSetup.value = false
|
|
262
|
+
if (props.resizable.onTeardown) {
|
|
263
|
+
props.resizable.onTeardown(el)
|
|
264
|
+
}
|
|
265
|
+
}
|
|
141
266
|
}))
|
|
142
267
|
|
|
268
|
+
|
|
269
|
+
const parentRef = ref<HTMLElement | null>(null)
|
|
270
|
+
const mergedVirtualizerOpts = computed(() => {
|
|
271
|
+
return {
|
|
272
|
+
// we have to put the defaults here as they can't reference local variables
|
|
273
|
+
count: props.values.length,
|
|
274
|
+
getScrollElement: () => parentRef.value,
|
|
275
|
+
estimateSize: () => { return 33 },
|
|
276
|
+
overscan: props.virtualizerOptions?.overscan ?? (props.virtualizerOptions?.method === "dynamic" ? 10 : 50),
|
|
277
|
+
method: "fixed",
|
|
278
|
+
enabled: false,
|
|
279
|
+
...props.virtualizerOptions
|
|
280
|
+
} satisfies Partial<VirtualizerOptions<any, any>> & { method: "fixed" | "dynamic" }
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
const rowVirtualizer = useVirtualizer(mergedVirtualizerOpts)
|
|
284
|
+
|
|
285
|
+
const virtualList = computed(() => {
|
|
286
|
+
return mergedVirtualizerOpts.value.enabled
|
|
287
|
+
? rowVirtualizer.value.getVirtualItems()
|
|
288
|
+
: props.values.map((_, i) => ({
|
|
289
|
+
index: i,
|
|
290
|
+
size: undefined,
|
|
291
|
+
start: 0,
|
|
292
|
+
end: 0,
|
|
293
|
+
key: typeof props.itemKey === "function"
|
|
294
|
+
? props.itemKey(_)
|
|
295
|
+
: props.itemKey
|
|
296
|
+
? props.values[props.itemKey as keyof typeof props.values]
|
|
297
|
+
: i
|
|
298
|
+
}))
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
const totalSize = computed(() => rowVirtualizer.value.getTotalSize())
|
|
302
|
+
|
|
303
|
+
function measureElement(el: any): void {
|
|
304
|
+
if (!el || !mergedVirtualizerOpts.value.enabled) return
|
|
305
|
+
if (mergedVirtualizerOpts.value?.method === "dynamic") {
|
|
306
|
+
rowVirtualizer.value.measureElement(el)
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function forceRecalculateFixedVirtualizer() {
|
|
311
|
+
if (mergedVirtualizerOpts.value?.method === "dynamic" || !mergedVirtualizerOpts.value.enabled) return
|
|
312
|
+
if (!parentRef.value) {
|
|
313
|
+
throw new Error("forceRecalculateFixedVirtualizer cannot be called before the table is mounted.")
|
|
314
|
+
}
|
|
315
|
+
const height = parentRef.value.querySelector("td")?.getBoundingClientRect().height
|
|
316
|
+
if (!height) return
|
|
317
|
+
for (let i = 0; i < props.values.length; i++) {
|
|
318
|
+
rowVirtualizer.value.resizeItem(i, height)
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const tableHeight = ref(0)
|
|
323
|
+
function updateTableHeight(): void {
|
|
324
|
+
if (!parentRef.value) return
|
|
325
|
+
const el = parentRef.value.querySelector("tbody")
|
|
326
|
+
if (!el) return
|
|
327
|
+
if (tableHeight.value === el.getBoundingClientRect().height) return
|
|
328
|
+
tableHeight.value = el.getBoundingClientRect().height
|
|
329
|
+
}
|
|
330
|
+
const throttledUpdateTableHeight = throttle(updateTableHeight, 100, { leading: true })
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
onMounted(() => {
|
|
334
|
+
throttledUpdateTableHeight()
|
|
335
|
+
forceRecalculateFixedVirtualizer()
|
|
336
|
+
useGlobalResizeObserver(parentRef, onResize)
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
const hasScrollbar = ref({ vertical: false, horizontal: false })
|
|
341
|
+
function onResize(): void {
|
|
342
|
+
const el = parentRef.value
|
|
343
|
+
if (!el) return
|
|
344
|
+
hasScrollbar.value = {
|
|
345
|
+
vertical: el.scrollHeight > el.clientHeight,
|
|
346
|
+
horizontal: el.scrollWidth > el.clientWidth
|
|
347
|
+
}
|
|
348
|
+
if (hasScrollbar.value.vertical) {
|
|
349
|
+
throttledUpdateTableHeight()
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
143
353
|
/* props.values.length instead of `props.values.length - 1` because we're creating an artificial first row for the header */
|
|
144
354
|
const getExtraClasses = (row: number, col: number, isHeader: boolean): string[] => {
|
|
145
355
|
const res = {
|
|
@@ -155,15 +365,29 @@ const getExtraClasses = (row: number, col: number, isHeader: boolean): string[]
|
|
|
155
365
|
return keys(res).filter(key => res[key])
|
|
156
366
|
}
|
|
157
367
|
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
]
|
|
165
|
-
|
|
166
|
-
|
|
368
|
+
const classes = computed(() => {
|
|
369
|
+
const res: Record<string, string> = {}
|
|
370
|
+
const headerTdClass = `table--header-cell cell truncate`
|
|
371
|
+
const bodyTdClass = `table--cell cell truncate`
|
|
372
|
+
for (let i = -1; i < props.values.length + 1; i++) {
|
|
373
|
+
for (let j = 0; j < props.cols.length; j++) {
|
|
374
|
+
const col = props.cols[j]!
|
|
375
|
+
const colConfig = props.colConfig[col]
|
|
376
|
+
const key = `${i}-${j}`
|
|
377
|
+
res[key] = twMerge(
|
|
378
|
+
getExtraClasses(i, j, i === -1).join(" "),
|
|
379
|
+
i === -1 ? headerTdClass : bodyTdClass,
|
|
380
|
+
i === -1 ? colConfig?.resizable === false && `no-resize` : undefined,
|
|
381
|
+
i !== -1 && mergedVirtualizerOpts.value.enabled && mergedVirtualizerOpts.value.method === "dynamic" && `flex`
|
|
382
|
+
)
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
return res
|
|
386
|
+
})
|
|
387
|
+
|
|
388
|
+
defineExpose({
|
|
389
|
+
forceRecalculateFixedVirtualizer
|
|
390
|
+
})
|
|
167
391
|
</script>
|
|
168
392
|
|
|
169
393
|
<script lang="ts">
|
|
@@ -173,7 +397,6 @@ type T = any
|
|
|
173
397
|
type RealProps = {
|
|
174
398
|
resizable?: Partial<ResizableOptions>
|
|
175
399
|
values?: T[]
|
|
176
|
-
itemKey?: keyof T | ((item: T) => string)
|
|
177
400
|
/** Let's the table know the shape of the data since values might be empty. */
|
|
178
401
|
cols?: (keyof T)[]
|
|
179
402
|
rounded?: boolean
|
|
@@ -181,6 +404,54 @@ type RealProps = {
|
|
|
181
404
|
cellBorder?: boolean
|
|
182
405
|
header?: boolean
|
|
183
406
|
colConfig?: TableColConfig<T>
|
|
407
|
+
/**
|
|
408
|
+
* See tanstack/vue-virtual {@link https://tanstack.com/virtual/latest/docs/api/virtualizer}
|
|
409
|
+
*
|
|
410
|
+
* The defaults are:
|
|
411
|
+
*
|
|
412
|
+
* - enabled: false
|
|
413
|
+
* - method: "fixed"
|
|
414
|
+
* - overscan: (50 if fixed, 10 if dynamic)
|
|
415
|
+
* - estimateSize: () => { return 33 }
|
|
416
|
+
*
|
|
417
|
+
* This also has an additional option, `method`, which can be set to `fixed` or `dynamic` (experimental).
|
|
418
|
+
*
|
|
419
|
+
* Notes:
|
|
420
|
+
*
|
|
421
|
+
* - Because of how virtualization works, initial layout (before .resizable-cols-setup class is applied) will only have access to the headers and not the rows. This can cause cols to look very small, especially if using resizable.fitWidth false.
|
|
422
|
+
*
|
|
423
|
+
* ### Fixed
|
|
424
|
+
*
|
|
425
|
+
* `fixed` is the default and will set the height of ALL items to the height of the first item onMounted (tanstack does not do this and if your estimateSize if off, the scrolling is weird).
|
|
426
|
+
*
|
|
427
|
+
* Since the table now truncates rows by default, they will always be the same height unless you change the inner styling. In fixed mode, `forceRecalculateFixedVirtualizer` is exposed if you need to force re-calculation.
|
|
428
|
+
*
|
|
429
|
+
* If using slots, be sure to at least pass the `class` slot prop to the td element. `style` with width is also supplied but is not required if you're displaying the table as a table.
|
|
430
|
+
*
|
|
431
|
+
* ### Dynamic (experimental)
|
|
432
|
+
*
|
|
433
|
+
* In `dynamic` mode we use tanstack's measureElement method. This is more expensive, but it will work with any heights.
|
|
434
|
+
*
|
|
435
|
+
* Dynamic mode also requires the table displays itself using grid and flex post setup as otherwise dynamic mode doesn't work.
|
|
436
|
+
*
|
|
437
|
+
* You don't need to do anything unless using slots. If using slots, pass the given `ref` slot prop to ref (internally this is tanstack's measureElement) and the class and style slot props at the very least:
|
|
438
|
+
* ```vue
|
|
439
|
+
* <template #[`${colName}`]="slotProps">
|
|
440
|
+
* <td
|
|
441
|
+
* :ref="slotProps.ref"
|
|
442
|
+
* :class="slotProps.class"
|
|
443
|
+
* :style="slotProps.style"
|
|
444
|
+
* >
|
|
445
|
+
* {{ slotProps.value }}
|
|
446
|
+
* </td>
|
|
447
|
+
* </template>
|
|
448
|
+
* ```
|
|
449
|
+
*/
|
|
450
|
+
virtualizerOptions?: Partial<VirtualizerOptions<any, any>> & { method?: "fixed" | "dynamic" }
|
|
451
|
+
/** Whether to enable sticky header styles. This requires `border:false`. This moves the border to the wrapper and styles a straight border between the scroll bar and the rounded border. */
|
|
452
|
+
stickyHeader?: boolean
|
|
453
|
+
/** Which key to use for the rows (only if not using virtualization). */
|
|
454
|
+
itemKey?: keyof T | ((item: T) => string)
|
|
184
455
|
}
|
|
185
456
|
interface Props
|
|
186
457
|
extends
|
|
@@ -22,6 +22,7 @@ type Data = {
|
|
|
22
22
|
offset?: number
|
|
23
23
|
widths: Ref<string[]>
|
|
24
24
|
selector: string
|
|
25
|
+
onTeardown?: (el: Element) => void
|
|
25
26
|
}
|
|
26
27
|
const elMap = new WeakMap<HTMLElement, Data>()
|
|
27
28
|
type RawOpts = { value: Partial<ResizableOptions> }
|
|
@@ -108,8 +109,8 @@ export const vResizableCols: Directive = {
|
|
|
108
109
|
},
|
|
109
110
|
updated(el: ResizableElement, { value: opts = {} }: RawOpts) {
|
|
110
111
|
const options = override({ ...defaultOpts }, opts) as ResizableOptions
|
|
111
|
-
const info = el && getElInfo(el)
|
|
112
|
-
const hasGrips = el && elMap.get(el)
|
|
112
|
+
const info = el && options.enabled && getElInfo(el)
|
|
113
|
+
const hasGrips = el && options.enabled && elMap.get(el)?.grips
|
|
113
114
|
// todo, we should probably check by name
|
|
114
115
|
const colsNotEqual = (info && info.colCount !== options.colCount)
|
|
115
116
|
if ((hasGrips && !options.enabled) || colsNotEqual) {
|
|
@@ -184,7 +185,7 @@ function createPointerDownHandler(el: ResizableElement) {
|
|
|
184
185
|
document.addEventListener("pointerup", $el.pointerUpHandler)
|
|
185
186
|
|
|
186
187
|
const { col, colNext } = getCols(el)
|
|
187
|
-
if (col === null || colNext === null) {
|
|
188
|
+
if (col === null || (colNext === null && $el.fitWidth)) {
|
|
188
189
|
el.classList.add("resizable-cols-error")
|
|
189
190
|
} else {
|
|
190
191
|
document.addEventListener("pointermove", $el.pointerMoveHandler)
|
|
@@ -289,7 +290,7 @@ function getElInfo(el: ResizableElement): Data {
|
|
|
289
290
|
function getColEls(el: ResizableElement): HTMLElement[] {
|
|
290
291
|
const $el = elMap.get(el)
|
|
291
292
|
if (!$el) unreachable("El went missing.")
|
|
292
|
-
return [...el.querySelectorAll(`:scope ${$el.selector ? $el.selector : "tr > td"}`)] as any
|
|
293
|
+
return [...el.querySelectorAll(`:scope ${$el.selector ? $el.selector : "tr > th, tr > td"}`)] as any
|
|
293
294
|
}
|
|
294
295
|
|
|
295
296
|
function setupColumns(el: ResizableElement, opts: ResizableOptions): void {
|
|
@@ -305,7 +306,8 @@ function setupColumns(el: ResizableElement, opts: ResizableOptions): void {
|
|
|
305
306
|
margin: opts.margin === "dynamic" ? gripWidth : opts.margin,
|
|
306
307
|
colCount: opts.colCount,
|
|
307
308
|
widths: opts.widths,
|
|
308
|
-
selector: opts.selector
|
|
309
|
+
selector: opts.selector,
|
|
310
|
+
onTeardown: opts.onTeardown
|
|
309
311
|
}
|
|
310
312
|
elMap.set(el, $el)
|
|
311
313
|
const els = getColEls(el)
|
|
@@ -325,6 +327,7 @@ function setupColumns(el: ResizableElement, opts: ResizableOptions): void {
|
|
|
325
327
|
}
|
|
326
328
|
positionGrips(el)
|
|
327
329
|
el.classList.add("resizable-cols-setup")
|
|
330
|
+
opts.onSetup?.(el)
|
|
328
331
|
}
|
|
329
332
|
|
|
330
333
|
function positionGrips(el: ResizableElement): void {
|
|
@@ -379,4 +382,5 @@ function teardownColumns(el: ResizableElement): void {
|
|
|
379
382
|
elMap.delete(el)
|
|
380
383
|
el.classList.remove("resizable-cols-setup")
|
|
381
384
|
removeGrips(el)
|
|
385
|
+
$el.onTeardown?.(el)
|
|
382
386
|
}
|
|
@@ -3,9 +3,7 @@ import type { Ref } from "vue"
|
|
|
3
3
|
|
|
4
4
|
export type ResizableOptions = {
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* ### true
|
|
6
|
+
* ### true (default)
|
|
9
7
|
* The directive will shrink/expand the columns when the table is resized and will use percentage widths on the table cells. This disables resizing of the last column (from the right handle).
|
|
10
8
|
*
|
|
11
9
|
* Additionally because of the way `table-layout:fixed` works, a min-width cannot be set on the elements via css, so instead, if the table shrinks past `opts.margin * col #`, `min-width` is set on the table until it's resized larger.
|
|
@@ -17,6 +15,8 @@ export type ResizableOptions = {
|
|
|
17
15
|
* The table can be resized past it's normal width and uses pixel widths on the table cells. You might want to set `overscroll-x: scroll` on a parent wrapping element.
|
|
18
16
|
*
|
|
19
17
|
* This will set the table width to `min-content`, else it doesn't work. Note that it does this after the initial reading/setting of sizes so you can, for example, layout the table with `width: 100%`.
|
|
18
|
+
*
|
|
19
|
+
* @default true
|
|
20
20
|
*/
|
|
21
21
|
fitWidth: boolean
|
|
22
22
|
/**
|
|
@@ -43,6 +43,10 @@ export type ResizableOptions = {
|
|
|
43
43
|
widths: Ref<string[]>
|
|
44
44
|
/** The selector to use for the cells. "tr > td" by default. */
|
|
45
45
|
selector: string
|
|
46
|
+
/** Is called just after the `resizable-cols-setup` class is added. Can be useful for controlling the styling of wrappers or doing additional things post-setup. The default table element uses it to set the class on the wrapper also. */
|
|
47
|
+
onSetup?: (el: Element) => void
|
|
48
|
+
/** Is called on teardown (after the `resizable-cols-setup` class is removed). */
|
|
49
|
+
onTeardown?: (el: Element) => void
|
|
46
50
|
}
|
|
47
51
|
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
|
48
52
|
export type TableColConfig<T = {}> = Record<keyof T, { name?: string, resizable?: boolean }>
|
|
@@ -3,7 +3,8 @@ import { extendTailwindMerge } from "tailwind-merge"
|
|
|
3
3
|
const _twMergeExtend = {
|
|
4
4
|
extend: {
|
|
5
5
|
classGroups: {
|
|
6
|
-
"focus-outline": [{ "focus-outline": ["", "no-offset", "none"] }]
|
|
6
|
+
"focus-outline": [{ "focus-outline": ["", "no-offset", "none"] }],
|
|
7
|
+
"no-truncate": ["truncate", "no-truncate"]
|
|
7
8
|
}
|
|
8
9
|
}
|
|
9
10
|
} satisfies Parameters<typeof extendTailwindMerge>[0]
|