@witchcraft/ui 0.3.14 → 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.
@@ -1,146 +1,355 @@
1
1
  <template>
2
- <!-- Assumes no scrollbars on children -->
3
- <table
4
- :class="twMerge(`table
5
- table-fixed
6
- border-separate
7
- border-spacing-0
8
- overflow-x-scroll
9
- scrollbar-hidden
10
- [&_.grip]:w-[5px]
11
- relative
12
- w-full
13
- box-content
14
- [&_thead]:font-bold
15
- [&_td]:p-1
16
- [&_td]:overflow-x-hidden
17
- [&.resizable-cols-error]:cursor-not-allowed
18
- [&.resizable-cols-error]:user-select-none
19
- `,
20
- cellBorder && `
21
- [&_td]:border-neutral-500
22
- [&_td:not(.last-row)]:border-b
23
- [&_td:not(.first-col)]:border-l
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
- [&_thead_td]:bg-neutral-200
27
- [&_td]:border-neutral-500
28
- dark:[&_thead_td]:bg-neutral-800
29
- dark:[&_td]:border-neutral-500
30
- [&_td.first-row]:border-t
31
- [&_td.last-row]:border-b
32
- [&_td.last-col]:border-r
33
- [&_td.first-col]:border-l
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
- [&_td.br]:rounded-br-sm
37
- [&_td.bl]:rounded-bl-sm
38
- [&_td.tr]:rounded-tr-sm
39
- [&_td.tl]:rounded-tl-sm
48
+ rounded-md
49
+ `,
50
+ mergedVirtualizerOpts.enabled && mergedVirtualizerOpts.method === 'dynamic' && `
51
+ relative
40
52
  `,
41
- ($attrs as any).class)"
42
- v-resizable-cols="resizableOptions"
53
+ ($attrs as any).wrapperClass)"
54
+ ref="parentRef"
43
55
  >
44
- <thead
45
- v-if="header"
46
- class="table--header"
56
+ <div
57
+ class="table--inner-container"
58
+ :style="{
59
+ ...(mergedVirtualizerOpts.enabled
60
+ ? { height: `${totalSize}px` }
61
+ : {})
62
+ }"
47
63
  >
48
- <tr class="table--row">
49
- <template
50
- v-for="col, i of cols"
51
- :key="col"
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
- <slot
54
- :name="`header-${col.toString()}`"
55
- :class="[
56
- extraClasses[`-1-${i}`],
57
- 'cell table--header-cell',
58
- (colConfig as any)[col]?.resizable === false
59
- ? 'no-resize'
60
- : ''
61
- ].join(' ')"
62
- :style="`width:${widths.length > 0 ? widths[i] : ``}; `"
63
- :col-key="col"
64
- :config="(colConfig as any)[col]"
122
+ <tr
123
+ :class="twMerge(
124
+ `table--row`,
125
+ isPostSetup && mergedVirtualizerOpts.enabled && mergedVirtualizerOpts.method === 'dynamic'
126
+ && `flex w-full`
127
+ )"
65
128
  >
66
- <td
67
- :class="[
68
- extraClasses[`-1-${i}`],
69
- 'cell table--header-cell',
70
- (colConfig as any)[col]?.resizable === false
71
- ? 'no-resize'
72
- : ''
73
- ].join(' ')"
74
- :style="`width:${widths.length > 0 ? widths[i] : ``}; `"
129
+ <template
130
+ v-for="col, i of cols"
131
+ :key="col"
75
132
  >
76
- {{ (colConfig as any)[col]?.name ?? col }}
77
- </td>
78
- </slot>
79
- </template>
80
- </tr>
81
- </thead>
82
- <tbody class="table--body">
83
- <template
84
- v-for="item, i of values"
85
- :key="typeof itemKey === 'function' ? itemKey(item) : item[itemKey]"
86
- >
87
- <tr class="table--row">
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
+ >
88
162
  <template
89
- v-for="col, j of cols"
90
- :key="(typeof itemKey === 'function' ? itemKey(item) : item[itemKey]) + col.toString()"
163
+ v-for="(virtual, index) in virtualList"
164
+ :key="virtual.key"
91
165
  >
92
- <slot
93
- :name="col"
94
- :item="item"
95
- :value="item[col]"
96
- :class="extraClasses[`${i}-${j}`] + 'table--cell cell'"
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"
97
184
  >
98
- <td :class="extraClasses[`${i}-${j}`] + 'table--cell cell'">
99
- {{ item[col] }}
100
- </td>
101
- </slot>
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>
102
205
  </template>
103
- </tr>
104
- </template>
105
- </tbody>
106
- </table>
206
+ </tbody>
207
+ </table>
208
+ </div>
209
+ </div>
107
210
  </template>
108
211
 
109
212
  <!-- generic="T extends Record<string, any> -->"
110
213
  <script setup lang="ts" generic="T">
111
214
  import type { MakeRequired } from "@alanscodelog/utils"
112
215
  import { keys } from "@alanscodelog/utils/keys"
113
- import { computed, ref, type TableHTMLAttributes } from "vue"
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"
114
219
 
220
+ import { useGlobalResizeObserver } from "../../composables/useGlobalResizeObserver.js"
115
221
  import { vResizableCols } from "../../directives/vResizableCols.js"
116
222
  import type { ResizableOptions, TableColConfig } from "../../types/index.js"
117
223
  import { twMerge } from "../../utils/twMerge.js"
118
224
  import type { TailwindClassProp } from "../shared/props.js"
119
225
 
120
226
  defineOptions({
121
- name: "LibTable"
227
+ name: "LibTable",
228
+ inheritAttrs: false
122
229
  })
123
230
 
124
231
  const props = withDefaults(defineProps<Props>(), {
125
232
  resizable: () => ({}),
126
233
  values: () => [] as T[],
127
- itemKey: "",
128
234
  cols: () => [] as (keyof T)[],
129
235
  rounded: true,
130
236
  border: true,
131
237
  cellBorder: true,
132
238
  header: true,
133
- colConfig: () => ({})
239
+ colConfig: () => ({}),
240
+ virtualizerOptions: () => ({ }),
241
+ enableStickyHeader: false,
242
+ itemKey: ""
134
243
  })
135
244
 
136
245
  const widths = ref([])
246
+
247
+
248
+ const isPostSetup = ref(false)
137
249
  const resizableOptions = computed<MakeRequired<Partial<ResizableOptions>, "colCount" | "widths">>(() => ({
138
250
  colCount: props.cols.length,
139
251
  widths,
140
252
  selector: ".cell",
141
- ...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
+ }
142
266
  }))
143
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
+
144
353
  /* props.values.length instead of `props.values.length - 1` because we're creating an artificial first row for the header */
145
354
  const getExtraClasses = (row: number, col: number, isHeader: boolean): string[] => {
146
355
  const res = {
@@ -156,15 +365,29 @@ const getExtraClasses = (row: number, col: number, isHeader: boolean): string[]
156
365
  return keys(res).filter(key => res[key])
157
366
  }
158
367
 
159
- const extraClasses = computed(() => Object.fromEntries([...Array(props.values.length + 1).keys()]
160
- .map(row => [...Array(props.cols.length).keys()]
161
- .map(col =>
162
- [
163
- `${row - 1}-${col}`,
164
- " " + getExtraClasses(row <= 0 ? 0 : row - 1, col, row === 0).join(" ") + " "
165
- ]))
166
- .flat()
167
- ))
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
+ })
168
391
  </script>
169
392
 
170
393
  <script lang="ts">
@@ -174,7 +397,6 @@ type T = any
174
397
  type RealProps = {
175
398
  resizable?: Partial<ResizableOptions>
176
399
  values?: T[]
177
- itemKey?: keyof T | ((item: T) => string)
178
400
  /** Let's the table know the shape of the data since values might be empty. */
179
401
  cols?: (keyof T)[]
180
402
  rounded?: boolean
@@ -182,6 +404,54 @@ type RealProps = {
182
404
  cellBorder?: boolean
183
405
  header?: boolean
184
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)
185
455
  }
186
456
  interface Props
187
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)!.grips
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
- * Defaults to true.
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]