@witchcraft/ui 0.3.14 → 0.3.16
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/module.mjs +2 -2
- package/dist/runtime/assets/utils.css +1 -1
- package/dist/runtime/components/LibTable/LibTable.d.vue.ts +62 -5
- package/dist/runtime/components/LibTable/LibTable.vue +310 -100
- package/dist/runtime/components/LibTable/LibTable.vue.d.ts +62 -5
- package/dist/runtime/directives/vResizableCols.js +107 -35
- 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 +4 -2
- package/src/module.ts +3 -0
- package/src/runtime/assets/utils.css +5 -0
- package/src/runtime/components/LibTable/LibTable.stories.ts +172 -47
- package/src/runtime/components/LibTable/LibTable.vue +385 -108
- package/src/runtime/directives/vResizableCols.ts +129 -38
- package/src/runtime/types/index.ts +7 -3
- package/src/runtime/utils/twMerge.ts +2 -1
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { castType } from "@alanscodelog/utils/castType"
|
|
2
2
|
import { override } from "@alanscodelog/utils/override"
|
|
3
|
-
import { throttle } from "@alanscodelog/utils/throttle"
|
|
4
3
|
import { unreachable } from "@alanscodelog/utils/unreachable"
|
|
5
4
|
import type { Directive, Ref } from "vue"
|
|
6
5
|
|
|
@@ -22,6 +21,10 @@ type Data = {
|
|
|
22
21
|
offset?: number
|
|
23
22
|
widths: Ref<string[]>
|
|
24
23
|
selector: string
|
|
24
|
+
onTeardown?: (el: Element) => void
|
|
25
|
+
fixedWidths?: Record<number, number>
|
|
26
|
+
fluidWidthsAsPercentOfFluidWidth?: Record<number, number>
|
|
27
|
+
justResized?: boolean
|
|
25
28
|
}
|
|
26
29
|
const elMap = new WeakMap<HTMLElement, Data>()
|
|
27
30
|
type RawOpts = { value: Partial<ResizableOptions> }
|
|
@@ -34,11 +37,19 @@ const defaultOpts: Omit<ResizableOptions, "colCount" | "widths" | "selector"> =
|
|
|
34
37
|
enabled: true
|
|
35
38
|
}
|
|
36
39
|
|
|
40
|
+
// note that while it would be nice to throttle this it seems to loose the reference to the original element
|
|
41
|
+
// haven't found where the issue is yet #future
|
|
37
42
|
const callback: ResizeCallback = (_rect: DOMRectReadOnly, el: Element): void => {
|
|
43
|
+
const $el = getElInfo(el as ResizableElement)
|
|
44
|
+
if ($el.justResized) return
|
|
38
45
|
setColWidths(el as ResizableElement)
|
|
39
|
-
|
|
46
|
+
$el.justResized = true
|
|
47
|
+
setTimeout(() => {
|
|
48
|
+
positionGrips(el as ResizableElement)
|
|
49
|
+
$el.justResized = false
|
|
50
|
+
}, 0)
|
|
40
51
|
}
|
|
41
|
-
|
|
52
|
+
|
|
42
53
|
/**
|
|
43
54
|
* Allow a table like element to be resized along it's columns.
|
|
44
55
|
*
|
|
@@ -103,31 +114,28 @@ export const vResizableCols: Directive = {
|
|
|
103
114
|
|
|
104
115
|
if (options.enabled) {
|
|
105
116
|
setupColumns(el, options)
|
|
106
|
-
observer.observe(el,
|
|
117
|
+
observer.observe(el, callback)
|
|
107
118
|
}
|
|
108
119
|
},
|
|
109
120
|
updated(el: ResizableElement, { value: opts = {} }: RawOpts) {
|
|
110
121
|
const options = override({ ...defaultOpts }, opts) as ResizableOptions
|
|
111
|
-
const info = el && getElInfo(el)
|
|
112
|
-
const hasGrips = el && elMap.get(el)
|
|
122
|
+
const info = el && options.enabled && getElInfo(el)
|
|
123
|
+
const hasGrips = el && options.enabled && elMap.get(el)?.grips
|
|
113
124
|
// todo, we should probably check by name
|
|
114
125
|
const colsNotEqual = (info && info.colCount !== options.colCount)
|
|
115
|
-
if (
|
|
126
|
+
if (!options.enabled || colsNotEqual) {
|
|
116
127
|
teardownColumns(el)
|
|
117
|
-
observer.unobserve(el,
|
|
128
|
+
observer.unobserve(el, callback)
|
|
118
129
|
}
|
|
119
130
|
|
|
120
131
|
if ((!hasGrips && options.enabled) || colsNotEqual) {
|
|
121
132
|
setupColumns(el, options)
|
|
122
|
-
observer.observe(el,
|
|
133
|
+
observer.observe(el, callback)
|
|
123
134
|
}
|
|
124
135
|
},
|
|
125
136
|
unmounted(el: ResizableElement) {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
teardownColumns(el)
|
|
129
|
-
globalResizeObserver.unobserve(el, throttledCallback)
|
|
130
|
-
}
|
|
137
|
+
teardownColumns(el)
|
|
138
|
+
globalResizeObserver.unobserve(el, callback)
|
|
131
139
|
},
|
|
132
140
|
getSSRProps() {
|
|
133
141
|
return {}
|
|
@@ -184,7 +192,7 @@ function createPointerDownHandler(el: ResizableElement) {
|
|
|
184
192
|
document.addEventListener("pointerup", $el.pointerUpHandler)
|
|
185
193
|
|
|
186
194
|
const { col, colNext } = getCols(el)
|
|
187
|
-
if (col === null || colNext === null) {
|
|
195
|
+
if (col === null || (colNext === null && $el.fitWidth)) {
|
|
188
196
|
el.classList.add("resizable-cols-error")
|
|
189
197
|
} else {
|
|
190
198
|
document.addEventListener("pointermove", $el.pointerMoveHandler)
|
|
@@ -202,6 +210,8 @@ function createPointerMoveHandler(el: ResizableElement) {
|
|
|
202
210
|
if ($el.isDragging) {
|
|
203
211
|
e.preventDefault()
|
|
204
212
|
|
|
213
|
+
$el.fluidWidthsAsPercentOfFluidWidth = undefined
|
|
214
|
+
|
|
205
215
|
const { col, colNext } = getCols(el)
|
|
206
216
|
|
|
207
217
|
if (col !== null) {
|
|
@@ -235,7 +245,11 @@ function createPointerMoveHandler(el: ResizableElement) {
|
|
|
235
245
|
}
|
|
236
246
|
}
|
|
237
247
|
|
|
238
|
-
|
|
248
|
+
$el.justResized = true
|
|
249
|
+
setTimeout(() => {
|
|
250
|
+
positionGrips(el)
|
|
251
|
+
$el.justResized = false
|
|
252
|
+
}, 0)
|
|
239
253
|
}
|
|
240
254
|
}
|
|
241
255
|
}
|
|
@@ -281,15 +295,15 @@ function getTestGripSize(el: ResizableElement): number {
|
|
|
281
295
|
return dynamicMinWidth
|
|
282
296
|
}
|
|
283
297
|
|
|
284
|
-
function getElInfo(el: ResizableElement): Data {
|
|
298
|
+
function getElInfo<T extends boolean = true>(el: ResizableElement, { throwIfMissing = true as T }: { throwIfMissing?: T } = {}): T extends true ? Data : Data | undefined {
|
|
285
299
|
const $el = elMap.get(el)
|
|
286
|
-
if (!$el) unreachable("El went missing.")
|
|
287
|
-
return $el
|
|
300
|
+
if (!$el && throwIfMissing) unreachable("El went missing.")
|
|
301
|
+
return $el as any
|
|
288
302
|
}
|
|
289
303
|
function getColEls(el: ResizableElement): HTMLElement[] {
|
|
290
304
|
const $el = elMap.get(el)
|
|
291
305
|
if (!$el) unreachable("El went missing.")
|
|
292
|
-
return [...el.querySelectorAll(`:scope ${$el.selector ? $el.selector : "tr > td"}`)] as any
|
|
306
|
+
return [...el.querySelectorAll(`:scope ${$el.selector ? $el.selector : "tr > th, tr > td"}`)] as any
|
|
293
307
|
}
|
|
294
308
|
|
|
295
309
|
function setupColumns(el: ResizableElement, opts: ResizableOptions): void {
|
|
@@ -305,7 +319,8 @@ function setupColumns(el: ResizableElement, opts: ResizableOptions): void {
|
|
|
305
319
|
margin: opts.margin === "dynamic" ? gripWidth : opts.margin,
|
|
306
320
|
colCount: opts.colCount,
|
|
307
321
|
widths: opts.widths,
|
|
308
|
-
selector: opts.selector
|
|
322
|
+
selector: opts.selector,
|
|
323
|
+
onTeardown: opts.onTeardown
|
|
309
324
|
}
|
|
310
325
|
elMap.set(el, $el)
|
|
311
326
|
const els = getColEls(el)
|
|
@@ -323,8 +338,13 @@ function setupColumns(el: ResizableElement, opts: ResizableOptions): void {
|
|
|
323
338
|
el.appendChild(grip)
|
|
324
339
|
$el.grips.set(grip, i)
|
|
325
340
|
}
|
|
326
|
-
|
|
327
|
-
|
|
341
|
+
$el.justResized = true
|
|
342
|
+
setTimeout(() => {
|
|
343
|
+
positionGrips(el)
|
|
344
|
+
$el.justResized = false
|
|
345
|
+
el.classList.add("resizable-cols-setup")
|
|
346
|
+
opts.onSetup?.(el)
|
|
347
|
+
}, 0)
|
|
328
348
|
}
|
|
329
349
|
|
|
330
350
|
function positionGrips(el: ResizableElement): void {
|
|
@@ -332,9 +352,9 @@ function positionGrips(el: ResizableElement): void {
|
|
|
332
352
|
const $el = getElInfo(el)
|
|
333
353
|
for (const grip of $el.grips.keys()) {
|
|
334
354
|
const col = $el.grips.get(grip)!
|
|
335
|
-
const
|
|
336
|
-
if (!
|
|
337
|
-
const colBox = getBox(
|
|
355
|
+
const colEl = getColEls(el)[col]
|
|
356
|
+
if (!colEl) unreachable()
|
|
357
|
+
const colBox = getBox(colEl)
|
|
338
358
|
const gripBox = getBox(grip)
|
|
339
359
|
|
|
340
360
|
grip.style.left = `${xPos + colBox.width - (gripBox.width / 2)}px`
|
|
@@ -346,8 +366,60 @@ function setColWidths(el: ResizableElement, children?: Element[]): void {
|
|
|
346
366
|
const $el = getElInfo(el)
|
|
347
367
|
const header = children ?? getColEls(el).slice(0, $el.colCount)
|
|
348
368
|
const len = $el.colCount
|
|
369
|
+
const elWidth = getBox(el).width
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
let fluidTotalPx = 0
|
|
373
|
+
const fluid: Record<number, number> = {}
|
|
374
|
+
|
|
375
|
+
const doCalculateFixed = $el.fixedWidths === undefined
|
|
376
|
+
const doCalculateFluid = $el.fluidWidthsAsPercentOfFluidWidth === undefined
|
|
377
|
+
|
|
378
|
+
if (doCalculateFixed) {
|
|
379
|
+
$el.fixedWidths = { [-1]: 0 } // fixedTotalWidth
|
|
380
|
+
}
|
|
381
|
+
if (doCalculateFluid) {
|
|
382
|
+
$el.fluidWidthsAsPercentOfFluidWidth = {}
|
|
383
|
+
}
|
|
384
|
+
for (let i = 0; i < len; i++) {
|
|
385
|
+
const col = header[i]
|
|
386
|
+
castType<HTMLElement>(col)
|
|
387
|
+
if (col.classList.contains("no-resize")) {
|
|
388
|
+
if (doCalculateFixed) {
|
|
389
|
+
const w = getBox(col).width
|
|
390
|
+
$el.fixedWidths![i] = w
|
|
391
|
+
$el.fixedWidths![-1]! += $el.fixedWidths![i]!
|
|
392
|
+
}
|
|
393
|
+
} else {
|
|
394
|
+
if (doCalculateFluid) {
|
|
395
|
+
const w = getBox(col).width
|
|
396
|
+
fluid[i] = w
|
|
397
|
+
fluidTotalPx += w
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const totalFluidCount = len - Object.keys($el.fixedWidths!).length
|
|
403
|
+
|
|
404
|
+
if (doCalculateFluid) {
|
|
405
|
+
for (let i = 0; i < len; i++) {
|
|
406
|
+
if ($el.fixedWidths![i] !== undefined) continue
|
|
407
|
+
$el.fluidWidthsAsPercentOfFluidWidth![i] = fluid[i]! / fluidTotalPx
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const fixedTotalPx = $el.fixedWidths![-1]!
|
|
412
|
+
const minFlexWidth = (totalFluidCount * $el.margin)
|
|
413
|
+
const minTotalWidth = minFlexWidth + fixedTotalPx
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
let leftOverFluidWidth = elWidth - fixedTotalPx
|
|
417
|
+
if (leftOverFluidWidth < minFlexWidth) {
|
|
418
|
+
leftOverFluidWidth = minFlexWidth
|
|
419
|
+
}
|
|
420
|
+
|
|
349
421
|
let width = 0
|
|
350
|
-
|
|
422
|
+
|
|
351
423
|
for (let i = 0; i < len; i++) {
|
|
352
424
|
const col = header[i]
|
|
353
425
|
castType<HTMLElement>(col)
|
|
@@ -355,9 +427,24 @@ function setColWidths(el: ResizableElement, children?: Element[]): void {
|
|
|
355
427
|
* only works if parent table does NOT use `box-sizing:border-box` and either has no border or does `width: calc(100% - BORDERWIDTH*2)`
|
|
356
428
|
*/
|
|
357
429
|
const colBox = getBox(col)
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
430
|
+
if ($el.fixedWidths![i] !== undefined) {
|
|
431
|
+
setWidth(col, $el.fixedWidths![i]!, el)
|
|
432
|
+
width += $el.fixedWidths![i]!
|
|
433
|
+
} else {
|
|
434
|
+
if ($el.fitWidth) {
|
|
435
|
+
if (!$el.widths.value[i]) {
|
|
436
|
+
setWidth(col, colBox.width, el)
|
|
437
|
+
width += getBox(col).width
|
|
438
|
+
continue
|
|
439
|
+
}
|
|
440
|
+
const newInPx = $el.fluidWidthsAsPercentOfFluidWidth![i]! * leftOverFluidWidth
|
|
441
|
+
setWidth(col, newInPx, el)
|
|
442
|
+
width += getBox(col).width
|
|
443
|
+
} else {
|
|
444
|
+
setWidth(col, colBox.width, el)
|
|
445
|
+
width += getBox(col).width
|
|
446
|
+
}
|
|
447
|
+
}
|
|
361
448
|
}
|
|
362
449
|
|
|
363
450
|
if (width < minTotalWidth) {
|
|
@@ -368,15 +455,19 @@ function setColWidths(el: ResizableElement, children?: Element[]): void {
|
|
|
368
455
|
}
|
|
369
456
|
|
|
370
457
|
function teardownColumns(el: ResizableElement): void {
|
|
371
|
-
const $el = getElInfo(el)
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
458
|
+
const $el = getElInfo(el, { throwIfMissing: false })
|
|
459
|
+
|
|
460
|
+
if ($el) {
|
|
461
|
+
el.removeEventListener("pointerdown", $el.pointerDownHandler)
|
|
462
|
+
document.removeEventListener("pointermove", $el.pointerMoveHandler)
|
|
463
|
+
document.removeEventListener("pointerup", $el.pointerUpHandler)
|
|
464
|
+
for (const key of Object.keys($el)) {
|
|
465
|
+
delete $el[key as keyof typeof $el]
|
|
466
|
+
}
|
|
467
|
+
$el.onTeardown?.(el)
|
|
468
|
+
elMap.delete(el)
|
|
378
469
|
}
|
|
379
|
-
|
|
470
|
+
|
|
380
471
|
el.classList.remove("resizable-cols-setup")
|
|
381
472
|
removeGrips(el)
|
|
382
473
|
}
|
|
@@ -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]
|