@rokkit/states 1.0.0-next.106 → 1.0.0-next.108
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/package.json +2 -2
- package/src/base-proxy.svelte.js +243 -0
- package/src/derive.svelte.js +58 -0
- package/src/index.js +6 -1
- package/src/list-controller-dup.svelte.js +155 -0
- package/src/list-controller.svelte.js +176 -0
- package/src/list-proxy.svelte.js +127 -0
- package/src/nested-controller.svelte.js +100 -0
- package/src/nested-proxy.svelte.js +286 -0
- package/src/node-proxy.svelte.js +293 -0
- package/src/proxy.svelte.js +59 -0
- package/src/nested.svelte.js +0 -267
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rokkit/states",
|
|
3
|
-
"version": "1.0.0-next.
|
|
3
|
+
"version": "1.0.0-next.108",
|
|
4
4
|
"description": "Contains generic data manipulation functions that can be used in various components.",
|
|
5
5
|
"author": "Jerry Thomas <me@jerrythomas.name>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@lukeed/uuid": "^2.0.1",
|
|
34
|
-
"@rokkit/core": "1.0.0-next.
|
|
34
|
+
"@rokkit/core": "1.0.0-next.106",
|
|
35
35
|
"d3-array": "^3.2.4",
|
|
36
36
|
"d3-collection": "^1.0.7",
|
|
37
37
|
"ramda": "^0.30.1",
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { defaultFields } from '@rokkit/core'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Base class for all proxy models that provides a common interface
|
|
5
|
+
* for navigation and selection operations.
|
|
6
|
+
*
|
|
7
|
+
* @abstract
|
|
8
|
+
*/
|
|
9
|
+
export class BaseProxy {
|
|
10
|
+
/** @type {any[]} Original data array */
|
|
11
|
+
data = $state(null)
|
|
12
|
+
/** @type {NodeProxy|null} Currently focused node */
|
|
13
|
+
nodes = $state([])
|
|
14
|
+
/** @type {NodeProxy[]} Flattened array of all visible nodes */
|
|
15
|
+
visibleNodes = $state([])
|
|
16
|
+
|
|
17
|
+
/** @type {NodeProxy|null} Currently focused node */
|
|
18
|
+
currentNode = $state(null)
|
|
19
|
+
/** @type {Object|null} Currently focused node */
|
|
20
|
+
value = $state(null)
|
|
21
|
+
/** @type {Map<string, NodeProxy>} Map of selected nodes by id */
|
|
22
|
+
selectedNodes = $state(new Map())
|
|
23
|
+
|
|
24
|
+
/** @type {Object} Field mappings */
|
|
25
|
+
fields
|
|
26
|
+
|
|
27
|
+
/** @type {Object} Configuration options */
|
|
28
|
+
options = {
|
|
29
|
+
multiSelect: false,
|
|
30
|
+
keyboardNavigation: true
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Creates a new proxy instance
|
|
35
|
+
*
|
|
36
|
+
* @param {any[]} data - Original data
|
|
37
|
+
* @param {Object} fields - Field mappings
|
|
38
|
+
* @param {Object} options - Configuration options
|
|
39
|
+
*/
|
|
40
|
+
constructor(data, value, fields = {}, options = {}) {
|
|
41
|
+
this.fields = { ...defaultFields, ...fields }
|
|
42
|
+
this.options = { ...this.options, ...options }
|
|
43
|
+
this.value = value
|
|
44
|
+
|
|
45
|
+
this.update(data)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Updates the proxy with new data
|
|
50
|
+
*
|
|
51
|
+
* @param {any} data - New data to use
|
|
52
|
+
* @returns {BaseProxy} - This proxy for method chaining
|
|
53
|
+
*/
|
|
54
|
+
update(data) {
|
|
55
|
+
this.data = data
|
|
56
|
+
this.reset()
|
|
57
|
+
return this
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Resets selection state
|
|
62
|
+
*
|
|
63
|
+
* @returns {BaseProxy} - This proxy for method chaining
|
|
64
|
+
*/
|
|
65
|
+
reset() {
|
|
66
|
+
this.selectedNodes.clear()
|
|
67
|
+
this.currentNode = null
|
|
68
|
+
return this
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Move to a specific target (index, path, or item)
|
|
73
|
+
*
|
|
74
|
+
* @abstract
|
|
75
|
+
* @param {number|number[]} target - Target to move to
|
|
76
|
+
* @returns {boolean} - True if moved, false otherwise
|
|
77
|
+
*/
|
|
78
|
+
moveTo() {
|
|
79
|
+
throw new Error('moveTo() must be implemented by subclass')
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Move to the next item
|
|
84
|
+
*
|
|
85
|
+
* @abstract
|
|
86
|
+
* @returns {boolean} - True if moved, false otherwise
|
|
87
|
+
*/
|
|
88
|
+
moveNext() {
|
|
89
|
+
throw new Error('moveNext() must be implemented by subclass')
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Move to the previous item
|
|
94
|
+
*
|
|
95
|
+
* @abstract
|
|
96
|
+
* @returns {boolean} - True if moved, false otherwise
|
|
97
|
+
*/
|
|
98
|
+
movePrev() {
|
|
99
|
+
throw new Error('movePrev() must be implemented by subclass')
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Expand the current node
|
|
104
|
+
*
|
|
105
|
+
* @returns {boolean} - True if expanded, false otherwise
|
|
106
|
+
*/
|
|
107
|
+
expand() {
|
|
108
|
+
// Default implementation for flat lists (no-op)
|
|
109
|
+
return false
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Collapse the current node
|
|
114
|
+
*
|
|
115
|
+
* @returns {boolean} - True if collapsed, false otherwise
|
|
116
|
+
*/
|
|
117
|
+
collapse() {
|
|
118
|
+
// Default implementation for flat lists (no-op)
|
|
119
|
+
return false
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Toggle expanded/collapsed state
|
|
124
|
+
*
|
|
125
|
+
* @returns {boolean} - True if state changed, false otherwise
|
|
126
|
+
*/
|
|
127
|
+
toggleExpansion() {
|
|
128
|
+
// Default implementation for flat lists (no-op)
|
|
129
|
+
return false
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Finds a node by a custom condition
|
|
134
|
+
*
|
|
135
|
+
* @param {Function} condition - Function that returns true for matching nodes
|
|
136
|
+
* @returns {NodeProxy|null} - The found node or null
|
|
137
|
+
*/
|
|
138
|
+
find(condition) {
|
|
139
|
+
let result = null
|
|
140
|
+
for (let i = 0; i < this.nodes.length; i++) {
|
|
141
|
+
result = this.nodes[i].find(condition)
|
|
142
|
+
if (result) return result
|
|
143
|
+
}
|
|
144
|
+
return null
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Finds the path index by a custom condition
|
|
149
|
+
*
|
|
150
|
+
* @param {Function} condition - Function that returns true for matching nodes
|
|
151
|
+
* @returns {number[]} - path index of found node or empty array
|
|
152
|
+
*/
|
|
153
|
+
findPathIndex(condition) {
|
|
154
|
+
const result = this.find(condition)
|
|
155
|
+
return result?.path ?? []
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Gets a node by its path
|
|
160
|
+
*
|
|
161
|
+
* @param {number|number[]} path - Path to the node
|
|
162
|
+
* @returns {NodeProxy|null} - The node or null if not found
|
|
163
|
+
*/
|
|
164
|
+
getNodeByPath(path = []) {
|
|
165
|
+
path = Array.isArray(path) ? path : [path]
|
|
166
|
+
|
|
167
|
+
if (!path.length || !this.data) return null
|
|
168
|
+
return path.reduce((currentNodes, index, depth) => {
|
|
169
|
+
// If we've hit a dead end or invalid index, return null
|
|
170
|
+
if (currentNodes === null || index < 0 || index >= currentNodes.length) {
|
|
171
|
+
return null
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Get the node at the current index
|
|
175
|
+
const node = currentNodes[index]
|
|
176
|
+
|
|
177
|
+
// If we've reached the final depth, return the node
|
|
178
|
+
if (depth === path.length - 1) return node
|
|
179
|
+
|
|
180
|
+
// Otherwise, move to the next level (children)
|
|
181
|
+
return node.children
|
|
182
|
+
}, this.nodes)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Selects the current node
|
|
187
|
+
*
|
|
188
|
+
* @param {number|number[]} [path] - The path to the node to toggle selection
|
|
189
|
+
* @returns {boolean} - Whether the selection was successful
|
|
190
|
+
*/
|
|
191
|
+
select(path) {
|
|
192
|
+
const node = path ? this.getNodeByPath(path) : this.currentNode
|
|
193
|
+
if (!node) return false
|
|
194
|
+
|
|
195
|
+
if (!this.options.multiSelect) {
|
|
196
|
+
this.selectedNodes.forEach((node) => {
|
|
197
|
+
node.selected = false
|
|
198
|
+
})
|
|
199
|
+
this.selectedNodes.clear()
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Select the current node
|
|
203
|
+
node.selected = true
|
|
204
|
+
this.selectedNodes.set(node.id, node)
|
|
205
|
+
return true
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Toggles selection on the current node (for multi-select)
|
|
210
|
+
*
|
|
211
|
+
* @param {number|number[]} [path] - The path to the node to toggle selection
|
|
212
|
+
* @returns {boolean} - Whether the operation was successful
|
|
213
|
+
*/
|
|
214
|
+
toggleSelection(path) {
|
|
215
|
+
const node = path ? this.getNodeByPath(path) : this.currentNode
|
|
216
|
+
|
|
217
|
+
if (!node) return false
|
|
218
|
+
|
|
219
|
+
node.selected = !node.selected
|
|
220
|
+
const nodeId = node.id
|
|
221
|
+
|
|
222
|
+
if (node.selected) {
|
|
223
|
+
this.selectedNodes.set(nodeId, node)
|
|
224
|
+
} else {
|
|
225
|
+
this.selectedNodes.delete(nodeId)
|
|
226
|
+
}
|
|
227
|
+
return true
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Extends selection on the current node (for multi-select)
|
|
232
|
+
*
|
|
233
|
+
* @param {number|number[]} [path] - The path to the node to extend selection
|
|
234
|
+
* @returns {boolean} - Whether the operation was successful
|
|
235
|
+
*/
|
|
236
|
+
extendSelection(path) {
|
|
237
|
+
if (this.options.multiSelect) {
|
|
238
|
+
return this.toggleSelection(path)
|
|
239
|
+
} else {
|
|
240
|
+
return this.select(path)
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { getKeyFromPath, defaultFields, getNestedFields } from '@rokkit/core'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
*
|
|
5
|
+
* @param {Array<*>} items
|
|
6
|
+
* @param {import('@rokkit/core').FieldMapping} fields
|
|
7
|
+
* @param {Array<number>} path
|
|
8
|
+
* @returns {Array<{ key: string, value: any }>}
|
|
9
|
+
*/
|
|
10
|
+
export function flatVisibleNodes(items, fields = defaultFields, path = []) {
|
|
11
|
+
const data = []
|
|
12
|
+
items.forEach((item, index) => {
|
|
13
|
+
const itemPath = [...path, index]
|
|
14
|
+
const key = getKeyFromPath(itemPath)
|
|
15
|
+
const expanded =
|
|
16
|
+
Array.isArray(item[fields.children]) &&
|
|
17
|
+
item[fields.children].length > 0 &&
|
|
18
|
+
item[fields.expanded]
|
|
19
|
+
|
|
20
|
+
data.push({ key, value: item })
|
|
21
|
+
|
|
22
|
+
if (expanded) {
|
|
23
|
+
const childFields = getNestedFields(fields)
|
|
24
|
+
data.push(...flatVisibleNodes(item[fields.children], childFields, itemPath))
|
|
25
|
+
}
|
|
26
|
+
})
|
|
27
|
+
return data
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Derives a flat lookup table for the given items, using the index path as key
|
|
32
|
+
*
|
|
33
|
+
* @param {Array<*>} items
|
|
34
|
+
* @param {import('@rokkit/core').FieldMapping} fields
|
|
35
|
+
* @param {Array<number>} path
|
|
36
|
+
* @returns {Record<string, { depth: number, item: any }>}
|
|
37
|
+
*/
|
|
38
|
+
export function deriveLookup(items, fields = defaultFields, path = []) {
|
|
39
|
+
const lookup = {}
|
|
40
|
+
|
|
41
|
+
items.forEach((item, index) => {
|
|
42
|
+
const itemPath = [...path, index]
|
|
43
|
+
const key = getKeyFromPath(itemPath)
|
|
44
|
+
|
|
45
|
+
lookup[key] = { depth: itemPath.length - 1, value: item }
|
|
46
|
+
const hasChildren =
|
|
47
|
+
typeof item === 'object' &&
|
|
48
|
+
Array.isArray(item[fields.children]) &&
|
|
49
|
+
item[fields.children].length > 0
|
|
50
|
+
|
|
51
|
+
if (hasChildren) {
|
|
52
|
+
const childFields = getNestedFields(fields)
|
|
53
|
+
const result = deriveLookup(item[fields.children], childFields, itemPath)
|
|
54
|
+
Object.assign(lookup, result)
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
return lookup
|
|
58
|
+
}
|
package/src/index.js
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
-
export { DataWrapper } from './nested.svelte'
|
|
2
1
|
export { TableWrapper } from './tabular.svelte'
|
|
2
|
+
// export { NodeProxy } from './node-proxy.svelte'
|
|
3
|
+
// export { ListProxy } from './list-proxy.svelte'
|
|
4
|
+
// export { NestedProxy } from './nested-proxy.svelte'
|
|
5
|
+
export { Proxy } from './proxy.svelte'
|
|
3
6
|
export { vibe } from './vibe.svelte'
|
|
7
|
+
export { ListController } from './list-controller.svelte'
|
|
8
|
+
export { NestedController } from './nested-controller.svelte'
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { SvelteMap } from 'svelte/reactivity'
|
|
2
|
+
import { FieldMapper } from '@rokkit/core'
|
|
3
|
+
import { isNil, equals } from 'ramda'
|
|
4
|
+
import { getKeyFromPath } from '@rokkit/core'
|
|
5
|
+
|
|
6
|
+
export class ListController {
|
|
7
|
+
items = $state(null)
|
|
8
|
+
selected = new SvelteMap()
|
|
9
|
+
activeItem = $state(null)
|
|
10
|
+
activeIndex = -1
|
|
11
|
+
options = $state({})
|
|
12
|
+
|
|
13
|
+
mappers = $state([])
|
|
14
|
+
data = $state(null)
|
|
15
|
+
lookup = new SvelteMap()
|
|
16
|
+
|
|
17
|
+
constructor(items, value, fields, options) {
|
|
18
|
+
this.items = items
|
|
19
|
+
this.mappers.push(new FieldMapper(fields))
|
|
20
|
+
this.options = { multiSelect: false, ...options }
|
|
21
|
+
this.init(items, value)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
get isNested() {
|
|
25
|
+
return this.mappers.length > 0
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @protected
|
|
30
|
+
*/
|
|
31
|
+
init(items, value) {
|
|
32
|
+
console.log('init', value)
|
|
33
|
+
this.data = items
|
|
34
|
+
this.data.forEach((item, index) => {
|
|
35
|
+
this.lookup.set(getKeyFromPath(index), item)
|
|
36
|
+
})
|
|
37
|
+
this.moveToValue(value)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @private
|
|
42
|
+
* @param {number|number[]} path
|
|
43
|
+
*/
|
|
44
|
+
getIndexFromPath(path) {
|
|
45
|
+
return !isNil(path) && !Array.isArray(path) ? path : path[0]
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
moveToValue(value) {
|
|
49
|
+
if (!value) return false
|
|
50
|
+
|
|
51
|
+
const index = this.data.findIndex((item) => equals(item, value))
|
|
52
|
+
return this.select(index)
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* @private
|
|
56
|
+
* @param {numeric} index
|
|
57
|
+
*/
|
|
58
|
+
setActiveItem(index) {
|
|
59
|
+
this.activeItem = this.data[index]
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* @private
|
|
63
|
+
* @param {numeric} index
|
|
64
|
+
*/
|
|
65
|
+
getItemAtIndex(index) {
|
|
66
|
+
return this.data[index]
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
moveTo(path) {
|
|
70
|
+
const index = this.getIndexFromPath(path)
|
|
71
|
+
console.log('moveTo', path, index)
|
|
72
|
+
if (index >= 0 && index < this.data.length && index !== this.activeIndex) {
|
|
73
|
+
this.activeIndex = index
|
|
74
|
+
this.setActiveItem(index)
|
|
75
|
+
return true
|
|
76
|
+
}
|
|
77
|
+
return false
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
movePrev() {
|
|
81
|
+
if (this.activeIndex < 0) {
|
|
82
|
+
return this.moveTo(0)
|
|
83
|
+
} else if (this.activeIndex > 0) {
|
|
84
|
+
return this.moveTo(this.activeIndex - 1)
|
|
85
|
+
}
|
|
86
|
+
return false
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
moveNext() {
|
|
90
|
+
if (this.activeIndex < this.data.length - 1) {
|
|
91
|
+
return this.moveTo(this.activeIndex + 1)
|
|
92
|
+
}
|
|
93
|
+
return false
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
moveFirst() {
|
|
97
|
+
this.moveTo(0)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
moveLast() {
|
|
101
|
+
this.moveTo(this.data.length - 1)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* @private
|
|
106
|
+
* @param {number} index
|
|
107
|
+
*/
|
|
108
|
+
toggleSelection(path) {
|
|
109
|
+
const key = getKeyFromPath(path)
|
|
110
|
+
const index = this.getIndexFromPath(path)
|
|
111
|
+
|
|
112
|
+
if (this.selected.has(key)) {
|
|
113
|
+
this.selected.delete(key)
|
|
114
|
+
} else {
|
|
115
|
+
this.selected.add(key, this.getItemAtIndex(index))
|
|
116
|
+
}
|
|
117
|
+
return true
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
*
|
|
122
|
+
* @param {number|number[]} path
|
|
123
|
+
* @returns
|
|
124
|
+
*/
|
|
125
|
+
extendSelection(path) {
|
|
126
|
+
const index = isNil(path) ? this.activeIndex : this.getIndexFromPath(path)
|
|
127
|
+
if (index >= 0 && index < this.data.length) return false
|
|
128
|
+
|
|
129
|
+
if (this.options.multiselect) {
|
|
130
|
+
return this.toggleSelection(path)
|
|
131
|
+
} else {
|
|
132
|
+
return this.select(path)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
*
|
|
138
|
+
* @param {number|number[]} path
|
|
139
|
+
* @returns
|
|
140
|
+
*/
|
|
141
|
+
select(path) {
|
|
142
|
+
const index = isNil(path) ? this.activeIndex : this.getIndexFromPath(path)
|
|
143
|
+
if (index > -1 && index !== this.activeIndex) {
|
|
144
|
+
this.selected.clear()
|
|
145
|
+
this.selected.set(getKeyFromPath(path), this.getItemAtIndex(index))
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
this.moveTo(path)
|
|
149
|
+
return true
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
toggleExpansion() {
|
|
153
|
+
return false
|
|
154
|
+
}
|
|
155
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { FieldMapper, defaultFields } from '@rokkit/core'
|
|
2
|
+
import { equals } from 'ramda'
|
|
3
|
+
import { SvelteSet } from 'svelte/reactivity'
|
|
4
|
+
import { flatVisibleNodes } from './derive.svelte'
|
|
5
|
+
|
|
6
|
+
export class ListController {
|
|
7
|
+
items = $state(null)
|
|
8
|
+
fields = defaultFields
|
|
9
|
+
mappers = []
|
|
10
|
+
#options = $state({})
|
|
11
|
+
lookup = new Map()
|
|
12
|
+
selectedKeys = new SvelteSet()
|
|
13
|
+
focusedKey = $state(null)
|
|
14
|
+
#currentIndex = -1
|
|
15
|
+
|
|
16
|
+
selected = $derived(Array.from(this.selectedKeys).map((key) => this.lookup.get(key)))
|
|
17
|
+
focused = $derived(this.lookup.get(this.focusedKey))
|
|
18
|
+
data = $derived(flatVisibleNodes(this.items, this.fields))
|
|
19
|
+
|
|
20
|
+
constructor(items, value, fields, options) {
|
|
21
|
+
this.items = items
|
|
22
|
+
this.fields = { ...defaultFields, ...fields }
|
|
23
|
+
this.mappers.push(new FieldMapper(fields))
|
|
24
|
+
this.#options = { multiselect: false, ...options }
|
|
25
|
+
this.init(items, value)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @private
|
|
30
|
+
* @param {Array<*>} items
|
|
31
|
+
* @param {*} value
|
|
32
|
+
*/
|
|
33
|
+
init(items, value) {
|
|
34
|
+
items.forEach((item, index) => this.lookup.set(String(index), item))
|
|
35
|
+
this.moveToValue(value)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
get isNested() {
|
|
39
|
+
return this.mappers.length > 1
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
get currentKey() {
|
|
43
|
+
return this.focusedKey
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @private
|
|
48
|
+
* @param {*} value
|
|
49
|
+
* @returns
|
|
50
|
+
*/
|
|
51
|
+
findByValue(value) {
|
|
52
|
+
const index = this.data.findIndex((row) => equals(row.value, value))
|
|
53
|
+
return index < 0 ? { index } : { index, ...this.data[index] }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @private
|
|
58
|
+
* @param {*} value
|
|
59
|
+
* @returns
|
|
60
|
+
*/
|
|
61
|
+
moveToValue(value = null) {
|
|
62
|
+
const { index, key } = this.findByValue(value)
|
|
63
|
+
|
|
64
|
+
this.selectedKeys.clear()
|
|
65
|
+
if (index >= 0) {
|
|
66
|
+
this.moveToIndex(index)
|
|
67
|
+
this.selectedKeys.add(key)
|
|
68
|
+
} else {
|
|
69
|
+
this.focusedKey = null
|
|
70
|
+
this.#currentIndex = -1
|
|
71
|
+
}
|
|
72
|
+
return true
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
*
|
|
77
|
+
* @param {string} path
|
|
78
|
+
* @returns
|
|
79
|
+
*/
|
|
80
|
+
moveTo(path) {
|
|
81
|
+
const index = Number(path)
|
|
82
|
+
return this.moveToIndex(index)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* @private
|
|
87
|
+
* @param {number} index
|
|
88
|
+
*/
|
|
89
|
+
moveToIndex(index) {
|
|
90
|
+
if (index >= 0 && index < this.data.length && this.#currentIndex !== index) {
|
|
91
|
+
this.#currentIndex = index
|
|
92
|
+
this.focusedKey = this.data[index].key
|
|
93
|
+
return true
|
|
94
|
+
}
|
|
95
|
+
return false
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
movePrev() {
|
|
99
|
+
if (this.#currentIndex > 0) {
|
|
100
|
+
return this.moveToIndex(this.#currentIndex - 1)
|
|
101
|
+
} else if (this.#currentIndex < 0) {
|
|
102
|
+
return this.moveLast()
|
|
103
|
+
}
|
|
104
|
+
return false
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
moveNext() {
|
|
108
|
+
if (this.#currentIndex < this.data.length - 1) {
|
|
109
|
+
return this.moveToIndex(this.#currentIndex + 1)
|
|
110
|
+
}
|
|
111
|
+
return false
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
moveFirst() {
|
|
115
|
+
return this.moveToIndex(0)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
moveLast() {
|
|
119
|
+
return this.moveToIndex(this.data.length - 1)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Toggles the selection.
|
|
124
|
+
* @private
|
|
125
|
+
* @param {string} key
|
|
126
|
+
*/
|
|
127
|
+
toggleSelection(key) {
|
|
128
|
+
if (this.selectedKeys.has(key)) {
|
|
129
|
+
this.selectedKeys.delete(key)
|
|
130
|
+
} else {
|
|
131
|
+
this.selectedKeys.add(key)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return true
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
*
|
|
139
|
+
* @param {string} selectedKey
|
|
140
|
+
* @returns
|
|
141
|
+
*/
|
|
142
|
+
select(selectedKey) {
|
|
143
|
+
const key = selectedKey ?? this.focusedKey
|
|
144
|
+
|
|
145
|
+
if (!this.lookup.has(key)) return false
|
|
146
|
+
|
|
147
|
+
if (this.focusedKey !== key) {
|
|
148
|
+
const { index } = this.findByValue(this.lookup.get(key))
|
|
149
|
+
this.moveToIndex(index)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (!this.selectedKeys.has(key)) {
|
|
153
|
+
this.selectedKeys.clear()
|
|
154
|
+
this.selectedKeys.add(key)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return true
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
*
|
|
162
|
+
* @param {string} selectedKey
|
|
163
|
+
* @returns
|
|
164
|
+
*/
|
|
165
|
+
extendSelection(selectedKey) {
|
|
166
|
+
const key = selectedKey ?? this.focusedKey
|
|
167
|
+
|
|
168
|
+
if (!this.lookup.has(key)) return false
|
|
169
|
+
|
|
170
|
+
if (this.#options.multiselect) {
|
|
171
|
+
return this.toggleSelection(key)
|
|
172
|
+
} else {
|
|
173
|
+
return this.select(key)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|