@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.
@@ -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
- positionGrips(el as ResizableElement)
46
+ $el.justResized = true
47
+ setTimeout(() => {
48
+ positionGrips(el as ResizableElement)
49
+ $el.justResized = false
50
+ }, 0)
40
51
  }
41
- const throttledCallback = throttle(callback)
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, throttledCallback)
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)!.grips
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 ((hasGrips && !options.enabled) || colsNotEqual) {
126
+ if (!options.enabled || colsNotEqual) {
116
127
  teardownColumns(el)
117
- observer.unobserve(el, throttledCallback)
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, throttledCallback)
133
+ observer.observe(el, callback)
123
134
  }
124
135
  },
125
136
  unmounted(el: ResizableElement) {
126
- const hasGrips = elMap.has(el) && elMap.get(el)!.grips
127
- if (hasGrips) {
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
- positionGrips(el)
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
- positionGrips(el)
327
- el.classList.add("resizable-cols-setup")
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 colEls = getColEls(el)[col]
336
- if (!colEls) unreachable()
337
- const colBox = getBox(colEls)
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
- const minTotalWidth = len * $el.margin
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
- setWidth(col, colBox.width, el)
360
- width += getBox(col).width
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
- el.removeEventListener("pointerdown", $el.pointerDownHandler)
374
- document.removeEventListener("pointermove", $el.pointerMoveHandler)
375
- document.removeEventListener("pointerup", $el.pointerUpHandler)
376
- for (const key of Object.keys($el)) {
377
- delete $el[key as keyof typeof $el]
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
- elMap.delete(el)
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
- * 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]