@greendrake/ui 0.6.0 → 0.8.0

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,99 +1,94 @@
1
- import { shallowReactive, type Ref, type ComputedRef } from 'vue'
1
+ import { shallowReactive, shallowRef, toValue, type MaybeRefOrGetter, type Ref, type ComputedRef } from 'vue'
2
+
3
+ type Key = string | number
4
+
5
+ // The arrows' steps through the list.
6
+ const arrowSteps: Record<string, number> = { ArrowUp: -1, ArrowDown: 1 }
2
7
 
3
8
  // Flat, index-addressable multi-selection over an ordered item list: single
4
9
  // (replace), ctrl-toggle, shift-range from an anchor, keyboard cursor moves,
5
- // select-all. The selected Set holds item ids read from items[i][idField].
6
- export default function useSelection<Id extends string | number>(items: Ref<Record<string, unknown>[]> | ComputedRef<Record<string, unknown>[]>, idField: string) {
10
+ // select-all. The selected Set holds item ids read from items[i][idField];
11
+ // the anchor and the cursor are rows, addressed by items[i][keyField] — the
12
+ // id itself unless an item shows in several rows (a tree node under two
13
+ // parents), whose keys tell them apart. Without multi, every pick replaces.
14
+ export default function useSelection<Id extends Key>(items: Ref<Record<string, unknown>[]> | ComputedRef<Record<string, unknown>[]>, idField: string, { keyField = idField, multi = true }: { keyField?: string; multi?: MaybeRefOrGetter<boolean> } = {}) {
7
15
  const selected: Set<Id> = shallowReactive(new Set())
8
- let anchor: Id | null = null
9
- let cursor: Id | null = null
10
-
11
- function getId(index: number): Id {
12
- return items.value[index][idField] as Id
13
- }
14
-
15
- function indexOf(id: Id): number {
16
- return items.value.findIndex(item => (item[idField] as Id) === id)
17
- }
16
+ // The row the keyboard moves from; the anchor is the one ranges extend from.
17
+ const cursor = shallowRef<Key | null>(null)
18
+ let anchor: Key | null = null
18
19
 
19
- function addRange(fromId: Id, toId: Id): void {
20
- const a = indexOf(fromId)
21
- const b = indexOf(toId)
22
- if (a === -1 || b === -1) return
23
- const [lo, hi] = a <= b ? [a, b] : [b, a]
24
- for (let i = lo; i <= hi; i++) selected.add(getId(i))
25
- }
20
+ const getId = (index: number): Id => items.value[index][idField] as Id
21
+ const keyAt = (index: number): Key => items.value[index][keyField] as Key
22
+ const indexOf = (key: Key): number => items.value.findIndex(item => item[keyField] === key)
26
23
 
27
- function toggle(id: Id): void {
28
- if (selected.has(id)) selected.delete(id)
29
- else selected.add(id)
30
- anchor = id
31
- cursor = id
24
+ // Selects the rows from index a to index b, either way round, alone.
25
+ function selectRange(a: number, b: number): void {
26
+ selected.clear()
27
+ for (let i = Math.min(a, b); i <= Math.max(a, b); i++) selected.add(getId(i))
32
28
  }
33
29
 
34
- function replace(id: Id): void {
30
+ function replace(key: Key): void {
35
31
  selected.clear()
36
- selected.add(id)
37
- anchor = id
38
- cursor = id
32
+ selected.add(getId(indexOf(key)))
33
+ anchor = key
34
+ cursor.value = key
39
35
  }
40
36
 
41
- function rangeTo(id: Id): void {
42
- if (anchor === null) {
43
- toggle(id)
44
- return
37
+ /**
38
+ * A pointer's pick of a row: Shift selects the range from the anchor,
39
+ * Ctrl/Cmd toggles the row — as Shift does with no anchor in the list —
40
+ * a plain pick selects it alone.
41
+ */
42
+ function pick(key: Key, e: MouseEvent): void {
43
+ const multiple = toValue(multi)
44
+ const from = multiple && e.shiftKey && anchor !== null ? indexOf(anchor) : -1
45
+ if (from !== -1) {
46
+ selectRange(from, indexOf(key))
47
+ cursor.value = key
48
+ } else if (multiple && (e.shiftKey || e.ctrlKey || e.metaKey)) {
49
+ const id = getId(indexOf(key))
50
+ if (selected.has(id)) selected.delete(id)
51
+ else selected.add(id)
52
+ anchor = key
53
+ cursor.value = key
54
+ } else {
55
+ replace(key)
45
56
  }
46
- selected.clear()
47
- addRange(anchor, id)
48
- cursor = id
49
57
  }
50
58
 
51
59
  /**
52
- * Move cursor up or down.
53
- * If extend is true (shift held), add to selection.
54
- * If extend is false, replace selection with single item.
55
- * Returns the new cursor index (useful for scrolling into view), or -1 if no items.
60
+ * Ctrl/Cmd+A selects all; the arrows move the cursor, selecting its row
61
+ * alone, or with Shift the range from the anchor to it. Returns the
62
+ * cursor's new index, for the host to scroll into view, or -1.
56
63
  */
57
- function moveCursor(direction: 'up' | 'down', extend: boolean): number {
64
+ function onKeyDown(e: KeyboardEvent): number {
65
+ if (toValue(multi) && (e.ctrlKey || e.metaKey) && e.key === 'a') {
66
+ e.preventDefault()
67
+ for (let i = 0; i < items.value.length; i++) selected.add(getId(i))
68
+ return -1
69
+ }
70
+ const step = arrowSteps[e.key]
71
+ if (step === undefined) return -1
72
+ e.preventDefault()
58
73
  if (!items.value.length) return -1
59
- const max = items.value.length - 1
60
- const curIdx = cursor !== null ? indexOf(cursor) : -1
61
- const next = direction === 'up' ? Math.max(0, curIdx <= 0 ? 0 : curIdx - 1) : Math.min(max, curIdx === -1 ? 0 : curIdx + 1)
62
- const nextId = getId(next)
63
-
64
- if (extend) {
65
- selected.add(nextId)
66
- cursor = nextId
67
- if (anchor === null) anchor = nextId
68
- } else {
69
- selected.clear()
70
- selected.add(nextId)
71
- anchor = nextId
72
- cursor = nextId
74
+ const at = cursor.value === null ? -1 : indexOf(cursor.value)
75
+ const next = at === -1 ? 0 : Math.min(Math.max(at + step, 0), items.value.length - 1)
76
+ if (!toValue(multi) || !e.shiftKey) {
77
+ replace(keyAt(next))
78
+ return next
73
79
  }
80
+ if (anchor === null || indexOf(anchor) === -1) anchor = keyAt(at === -1 ? next : at)
81
+ selectRange(indexOf(anchor), next)
82
+ cursor.value = keyAt(next)
74
83
  return next
75
84
  }
76
85
 
77
- function selectAll(): void {
78
- for (let i = 0; i < items.value.length; i++) selected.add(getId(i))
79
- }
80
-
81
- function clear(): void {
82
- selected.clear()
83
- anchor = null
84
- cursor = null
85
- }
86
-
87
86
  return {
88
87
  selected,
88
+ cursor,
89
89
  getId,
90
- indexOf,
91
- addRange,
92
- toggle,
93
90
  replace,
94
- rangeTo,
95
- moveCursor,
96
- selectAll,
97
- clear
91
+ pick,
92
+ onKeyDown
98
93
  }
99
94
  }
@@ -0,0 +1,38 @@
1
+ import { computed, shallowRef, toValue, type ComputedRef, type MaybeRefOrGetter } from 'vue'
2
+ import { capturePointer } from './capturePointer'
3
+
4
+ export interface WidthDrag {
5
+ /** Whether a drag is in progress. */
6
+ active: ComputedRef<boolean>
7
+ /** The handle's pointerdown: captures the pointer and drags from `width`, handing each new width to `set`. */
8
+ start(event: PointerEvent, width: number, set: (width: number) => void): void
9
+ /** The handle's pointermove. */
10
+ move(event: PointerEvent): void
11
+ /** The handle's pointerup and pointercancel. */
12
+ end(): void
13
+ }
14
+
15
+ // A width dragged by a handle at its end edge: the width follows the pointer's
16
+ // horizontal travel, never below `minWidth`.
17
+ export default function useWidthDrag(minWidth: MaybeRefOrGetter<number>): WidthDrag {
18
+ const drag = shallowRef<{ startX: number; startWidth: number; set: (width: number) => void } | null>(null)
19
+ return {
20
+ active: computed(() => drag.value !== null),
21
+ start(event, width, set) {
22
+ event.preventDefault()
23
+ capturePointer(event)
24
+ drag.value = {
25
+ startX: event.clientX,
26
+ startWidth: width,
27
+ set
28
+ }
29
+ },
30
+ move(event) {
31
+ const d = drag.value
32
+ if (d) d.set(Math.max(toValue(minWidth), d.startWidth + event.clientX - d.startX))
33
+ },
34
+ end() {
35
+ drag.value = null
36
+ }
37
+ }
38
+ }