@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.
- package/README.md +53 -11
- package/package.json +5 -5
- package/src/BottomNav.vue +2 -2
- package/src/CircularProgress.vue +6 -2
- package/src/ContextMenu.vue +77 -0
- package/src/Copy.vue +2 -1
- package/src/FieldTip.ts +4 -4
- package/src/InfoBlock.vue +45 -0
- package/src/InfoTip.vue +33 -0
- package/src/MenuButton.vue +3 -1
- package/src/ModalOnClick.vue +13 -6
- package/src/RowSelect.ts +4 -27
- package/src/SplitPane.vue +85 -0
- package/src/SwitchRow.vue +74 -0
- package/src/TabBar.vue +11 -0
- package/src/TabPanels.vue +38 -0
- package/src/{FieldTip.scss → Tip.scss} +7 -7
- package/src/TreeView.vue +368 -69
- package/src/capturePointer.ts +15 -0
- package/src/main.ts +12 -3
- package/src/messages.ts +17 -7
- package/src/useSelection.ts +67 -72
- package/src/useWidthDrag.ts +38 -0
package/src/useSelection.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
9
|
-
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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(
|
|
30
|
+
function replace(key: Key): void {
|
|
35
31
|
selected.clear()
|
|
36
|
-
selected.add(
|
|
37
|
-
anchor =
|
|
38
|
-
cursor =
|
|
32
|
+
selected.add(getId(indexOf(key)))
|
|
33
|
+
anchor = key
|
|
34
|
+
cursor.value = key
|
|
39
35
|
}
|
|
40
36
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
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
|
|
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
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
95
|
-
|
|
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
|
+
}
|