@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.
@@ -1,145 +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"
122
+ <tr
123
+ :class="twMerge(
124
+ `table--row`,
125
+ isPostSetup && mergedVirtualizerOpts.enabled && mergedVirtualizerOpts.method === 'dynamic'
126
+ && `flex w-full`
127
+ )"
64
128
  >
65
- <td
66
- :class="[
67
- extraClasses[`-1-${i}`],
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
- {{ (colConfig as any)[col]?.name ?? col }}
76
- </td>
77
- </slot>
78
- </template>
79
- </tr>
80
- </thead>
81
- <tbody class="table--body">
82
- <template
83
- v-for="item, i of values"
84
- :key="typeof itemKey === 'function' ? itemKey(item) : item[itemKey]"
85
- >
86
- <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
+ >
87
162
  <template
88
- v-for="col, j of cols"
89
- :key="(typeof itemKey === 'function' ? itemKey(item) : item[itemKey]) + col.toString()"
163
+ v-for="(virtual, index) in virtualList"
164
+ :key="virtual.key"
90
165
  >
91
- <slot
92
- :name="col"
93
- :item="item"
94
- :value="item[col]"
95
- :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"
96
184
  >
97
- <td :class="extraClasses[`${i}-${j}`] + 'table--cell cell'">
98
- {{ item[col] }}
99
- </td>
100
- </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>
101
205
  </template>
102
- </tr>
103
- </template>
104
- </tbody>
105
- </table>
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 { 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"
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 extraClasses = computed(() => Object.fromEntries([...Array(props.values.length + 1).keys()]
159
- .map(row => [...Array(props.cols.length).keys()]
160
- .map(col =>
161
- [
162
- `${row - 1}-${col}`,
163
- getExtraClasses(row <= 0 ? 0 : row - 1, col, row === 0).join(" ")
164
- ]))
165
- .flat()
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)!.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]