@rokkit/states 1.0.0-next.107 → 1.0.0-next.109
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/derive.svelte.js +58 -0
- package/src/index.js +5 -4
- package/src/list-controller.svelte.js +177 -0
- package/src/nested-controller.svelte.js +122 -0
- package/src/proxy.svelte.js +32 -7
- package/src/traversal.svelte.js +137 -0
- package/src/base-proxy.svelte.js +0 -243
- package/src/list-proxy.svelte.js +0 -127
- package/src/nested-proxy.svelte.js +0 -286
- package/src/nested.svelte.js +0 -267
- package/src/node-proxy.svelte.js +0 -293
- package/src/node.js +0 -64
- package/src/tree-filter.svelte.js +0 -33
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.109",
|
|
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.109",
|
|
35
35
|
"d3-array": "^3.2.4",
|
|
36
36
|
"d3-collection": "^1.0.7",
|
|
37
37
|
"ramda": "^0.30.1",
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { getKeyFromPath, defaultFields, getNestedFields } from '@rokkit/core'
|
|
2
|
+
import { Proxy } from './proxy.svelte'
|
|
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 index paths as keys
|
|
32
|
+
* Each value is a Proxy instance for convenient manipulation
|
|
33
|
+
*
|
|
34
|
+
* @param {Array<*>} items - Source items array
|
|
35
|
+
* @param {import('@rokkit/core').FieldMapping} fields - Field mappings configuration
|
|
36
|
+
* @param {Array<number>} path - Current path in the tree
|
|
37
|
+
* @returns {Map<string, Proxy>} - Map of path keys to Proxy instances
|
|
38
|
+
*/
|
|
39
|
+
export function deriveLookupWithProxy(items, fields = defaultFields, path = []) {
|
|
40
|
+
const lookup = new Map()
|
|
41
|
+
|
|
42
|
+
items.forEach((item, index) => {
|
|
43
|
+
const itemPath = [...path, index]
|
|
44
|
+
const key = getKeyFromPath(itemPath)
|
|
45
|
+
const proxy = new Proxy(item, fields)
|
|
46
|
+
|
|
47
|
+
lookup.set(key, proxy)
|
|
48
|
+
// console.log(key, proxy.value)
|
|
49
|
+
if (proxy.hasChildren) {
|
|
50
|
+
const childFields = getNestedFields(fields)
|
|
51
|
+
const childLookup = deriveLookupWithProxy(proxy.get('children'), childFields, itemPath)
|
|
52
|
+
for (const [childKey, childValue] of childLookup.entries()) {
|
|
53
|
+
lookup.set(childKey, childValue)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
return lookup
|
|
58
|
+
}
|
package/src/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
export { DataWrapper } from './nested.svelte'
|
|
2
1
|
export { TableWrapper } from './tabular.svelte'
|
|
3
|
-
export { NodeProxy } from './node-proxy.svelte'
|
|
4
|
-
export { ListProxy } from './list-proxy.svelte'
|
|
5
|
-
export { NestedProxy } from './nested-proxy.svelte'
|
|
2
|
+
// export { NodeProxy } from './node-proxy.svelte'
|
|
3
|
+
// export { ListProxy } from './list-proxy.svelte'
|
|
4
|
+
// export { NestedProxy } from './nested-proxy.svelte'
|
|
6
5
|
export { Proxy } from './proxy.svelte'
|
|
7
6
|
export { vibe } from './vibe.svelte'
|
|
7
|
+
export { ListController } from './list-controller.svelte'
|
|
8
|
+
export { NestedController } from './nested-controller.svelte'
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { FieldMapper, defaultFields } from '@rokkit/core'
|
|
2
|
+
import { equals } from 'ramda'
|
|
3
|
+
import { SvelteSet } from 'svelte/reactivity'
|
|
4
|
+
import { deriveLookupWithProxy, 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).value))
|
|
17
|
+
focused = $derived(this.lookup.get(this.focusedKey)?.value)
|
|
18
|
+
data = $derived(flatVisibleNodes(this.items, this.fields))
|
|
19
|
+
lookup = $derived(deriveLookupWithProxy(this.items, this.fields))
|
|
20
|
+
|
|
21
|
+
constructor(items, value, fields, options) {
|
|
22
|
+
this.items = items
|
|
23
|
+
this.fields = { ...defaultFields, ...fields }
|
|
24
|
+
this.mappers.push(new FieldMapper(fields))
|
|
25
|
+
this.#options = { multiselect: false, ...options }
|
|
26
|
+
this.init(value)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @private
|
|
31
|
+
* @param {Array<*>} items
|
|
32
|
+
* @param {*} value
|
|
33
|
+
*/
|
|
34
|
+
init(value) {
|
|
35
|
+
// items.forEach((item, index) => this.lookup.set(String(index), item))
|
|
36
|
+
this.moveToValue(value)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
get isNested() {
|
|
40
|
+
return this.mappers.length > 1
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
get currentKey() {
|
|
44
|
+
return this.focusedKey
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @private
|
|
49
|
+
* @param {*} value
|
|
50
|
+
* @returns
|
|
51
|
+
*/
|
|
52
|
+
findByValue(value) {
|
|
53
|
+
const index = this.data.findIndex((row) => equals(row.value, value))
|
|
54
|
+
return index < 0 ? { index } : { index, ...this.data[index] }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* @private
|
|
59
|
+
* @param {*} value
|
|
60
|
+
* @returns
|
|
61
|
+
*/
|
|
62
|
+
moveToValue(value = null) {
|
|
63
|
+
const { index, key } = this.findByValue(value)
|
|
64
|
+
|
|
65
|
+
this.selectedKeys.clear()
|
|
66
|
+
if (index >= 0) {
|
|
67
|
+
this.moveToIndex(index)
|
|
68
|
+
this.selectedKeys.add(key)
|
|
69
|
+
} else {
|
|
70
|
+
this.focusedKey = null
|
|
71
|
+
this.#currentIndex = -1
|
|
72
|
+
}
|
|
73
|
+
return true
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
*
|
|
78
|
+
* @param {string} path
|
|
79
|
+
* @returns
|
|
80
|
+
*/
|
|
81
|
+
moveTo(path) {
|
|
82
|
+
const index = Number(path)
|
|
83
|
+
return this.moveToIndex(index)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* @private
|
|
88
|
+
* @param {number} index
|
|
89
|
+
*/
|
|
90
|
+
moveToIndex(index) {
|
|
91
|
+
if (index >= 0 && index < this.data.length && this.#currentIndex !== index) {
|
|
92
|
+
this.#currentIndex = index
|
|
93
|
+
this.focusedKey = this.data[index].key
|
|
94
|
+
return true
|
|
95
|
+
}
|
|
96
|
+
return false
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
movePrev() {
|
|
100
|
+
if (this.#currentIndex > 0) {
|
|
101
|
+
return this.moveToIndex(this.#currentIndex - 1)
|
|
102
|
+
} else if (this.#currentIndex < 0) {
|
|
103
|
+
return this.moveLast()
|
|
104
|
+
}
|
|
105
|
+
return false
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
moveNext() {
|
|
109
|
+
if (this.#currentIndex < this.data.length - 1) {
|
|
110
|
+
return this.moveToIndex(this.#currentIndex + 1)
|
|
111
|
+
}
|
|
112
|
+
return false
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
moveFirst() {
|
|
116
|
+
return this.moveToIndex(0)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
moveLast() {
|
|
120
|
+
return this.moveToIndex(this.data.length - 1)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Toggles the selection.
|
|
125
|
+
* @private
|
|
126
|
+
* @param {string} key
|
|
127
|
+
*/
|
|
128
|
+
toggleSelection(key) {
|
|
129
|
+
if (this.selectedKeys.has(key)) {
|
|
130
|
+
this.selectedKeys.delete(key)
|
|
131
|
+
} else {
|
|
132
|
+
this.selectedKeys.add(key)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return true
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
*
|
|
140
|
+
* @param {string} selectedKey
|
|
141
|
+
* @returns
|
|
142
|
+
*/
|
|
143
|
+
select(selectedKey) {
|
|
144
|
+
const key = selectedKey ?? this.focusedKey
|
|
145
|
+
|
|
146
|
+
if (!this.lookup.has(key)) return false
|
|
147
|
+
|
|
148
|
+
if (this.focusedKey !== key) {
|
|
149
|
+
const { index } = this.findByValue(this.lookup.get(key).value)
|
|
150
|
+
this.moveToIndex(index)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (!this.selectedKeys.has(key)) {
|
|
154
|
+
this.selectedKeys.clear()
|
|
155
|
+
this.selectedKeys.add(key)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return true
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
*
|
|
163
|
+
* @param {string} selectedKey
|
|
164
|
+
* @returns
|
|
165
|
+
*/
|
|
166
|
+
extendSelection(selectedKey) {
|
|
167
|
+
const key = selectedKey ?? this.focusedKey
|
|
168
|
+
|
|
169
|
+
if (!this.lookup.has(key)) return false
|
|
170
|
+
|
|
171
|
+
if (this.#options.multiselect) {
|
|
172
|
+
return this.toggleSelection(key)
|
|
173
|
+
} else {
|
|
174
|
+
return this.select(key)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { getKeyFromPath, getPathFromKey, getNestedFields } from '@rokkit/core'
|
|
2
|
+
import { equals } from 'ramda'
|
|
3
|
+
import { ListController } from './list-controller.svelte'
|
|
4
|
+
|
|
5
|
+
export class NestedController extends ListController {
|
|
6
|
+
/**
|
|
7
|
+
* @protected
|
|
8
|
+
* @param {Object} [value]
|
|
9
|
+
*/
|
|
10
|
+
init(value) {
|
|
11
|
+
// this.createLookup(items)
|
|
12
|
+
if (value) {
|
|
13
|
+
this.ensureVisible(value)
|
|
14
|
+
this.moveToValue(value)
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* @private
|
|
19
|
+
* @param {Object[]} items
|
|
20
|
+
* @param {number[]} [path]=[]
|
|
21
|
+
*/
|
|
22
|
+
createLookup(items, path = []) {
|
|
23
|
+
const depth = path.length
|
|
24
|
+
if (depth >= this.mappers.length) {
|
|
25
|
+
this.mappers.push(this.mappers[depth - 1].getChildMapper())
|
|
26
|
+
}
|
|
27
|
+
const fm = this.mappers[depth]
|
|
28
|
+
|
|
29
|
+
items.forEach((item, index) => {
|
|
30
|
+
const itemPath = [...path, index]
|
|
31
|
+
const key = getKeyFromPath(itemPath)
|
|
32
|
+
|
|
33
|
+
this.lookup.set(key, item)
|
|
34
|
+
if (fm.get('selected', item)) {
|
|
35
|
+
this.selectedKeys.add(key)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (fm.hasChildren(item)) {
|
|
39
|
+
this.createLookup(fm.get('children', item), itemPath)
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Mark parents as expanded so that item is visible
|
|
46
|
+
* @param {*} value
|
|
47
|
+
* @returns
|
|
48
|
+
*/
|
|
49
|
+
ensureVisible(value) {
|
|
50
|
+
const result = this.lookup.entries().find((entry) => equals(entry[1].value, value))
|
|
51
|
+
// console.log(result)
|
|
52
|
+
const path = getPathFromKey(result[0])
|
|
53
|
+
|
|
54
|
+
for (let i = 1; i < path.length; i++) {
|
|
55
|
+
const nodeKey = getKeyFromPath(path.slice(0, i))
|
|
56
|
+
this.expand(nodeKey)
|
|
57
|
+
}
|
|
58
|
+
return true
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Toggle expansion of item
|
|
63
|
+
* @param {*} value
|
|
64
|
+
* @returns
|
|
65
|
+
*/
|
|
66
|
+
toggleExpansion(key) {
|
|
67
|
+
if (!this.lookup.has(key)) return false
|
|
68
|
+
const proxy = this.lookup.get(key)
|
|
69
|
+
proxy.expanded = !proxy.expanded
|
|
70
|
+
// const item = this.lookup.get(key)
|
|
71
|
+
// const fields = this.fieldsFor(key)
|
|
72
|
+
// item[fields.expanded] = !item[this.fields.expanded]
|
|
73
|
+
return true
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Expand item
|
|
78
|
+
* @param {*} value
|
|
79
|
+
* @returns
|
|
80
|
+
*/
|
|
81
|
+
expand(key) {
|
|
82
|
+
const actualKey = key ?? this.focusedKey
|
|
83
|
+
if (!this.lookup.has(actualKey)) return false
|
|
84
|
+
const proxy = this.lookup.get(actualKey)
|
|
85
|
+
proxy.expanded = true
|
|
86
|
+
// const item = this.lookup.get(actualKey)
|
|
87
|
+
// const fields = this.fieldsFor(actualKey)
|
|
88
|
+
// item[fields.expanded] = true
|
|
89
|
+
|
|
90
|
+
return true
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Collapse item
|
|
95
|
+
* @param {*} value
|
|
96
|
+
* @returns
|
|
97
|
+
*/
|
|
98
|
+
collapse(key) {
|
|
99
|
+
const actualKey = key ?? this.focusedKey
|
|
100
|
+
if (!this.lookup.has(actualKey)) return false
|
|
101
|
+
// const item = this.lookup.get(actualKey)
|
|
102
|
+
// const fields = this.fieldsFor(actualKey)
|
|
103
|
+
// item[fields.expanded] = false
|
|
104
|
+
const proxy = this.lookup.get(actualKey)
|
|
105
|
+
proxy.expanded = false
|
|
106
|
+
return true
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
*
|
|
111
|
+
* @param {*} key
|
|
112
|
+
* @returns
|
|
113
|
+
*/
|
|
114
|
+
fieldsFor(key) {
|
|
115
|
+
const path = getPathFromKey(key)
|
|
116
|
+
let fields = this.fields
|
|
117
|
+
for (let i = 1; i < path.length; i++) {
|
|
118
|
+
fields = getNestedFields(fields)
|
|
119
|
+
}
|
|
120
|
+
return fields
|
|
121
|
+
}
|
|
122
|
+
}
|
package/src/proxy.svelte.js
CHANGED
|
@@ -2,12 +2,14 @@ import { defaultFields } from '@rokkit/core'
|
|
|
2
2
|
import { isNil, has } from 'ramda'
|
|
3
3
|
|
|
4
4
|
export class Proxy {
|
|
5
|
-
#
|
|
6
|
-
#
|
|
5
|
+
#original = null
|
|
6
|
+
#value = null
|
|
7
|
+
#fields = null
|
|
7
8
|
|
|
8
9
|
constructor(value, fields) {
|
|
9
|
-
this.#value = typeof value === 'object' ? value : { text: value }
|
|
10
10
|
this.fields = fields
|
|
11
|
+
this.#original = value
|
|
12
|
+
this.#value = typeof value === 'object' ? value : { [this.fields.text]: value }
|
|
11
13
|
}
|
|
12
14
|
|
|
13
15
|
get fields() {
|
|
@@ -18,7 +20,7 @@ export class Proxy {
|
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
get value() {
|
|
21
|
-
return this.#value
|
|
23
|
+
return typeof this.#original === 'object' ? this.#value : this.#original
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
set value(value) {
|
|
@@ -34,6 +36,7 @@ export class Proxy {
|
|
|
34
36
|
})
|
|
35
37
|
} else {
|
|
36
38
|
this.#value.text = value
|
|
39
|
+
this.#original = value
|
|
37
40
|
}
|
|
38
41
|
}
|
|
39
42
|
|
|
@@ -41,10 +44,11 @@ export class Proxy {
|
|
|
41
44
|
* Gets a mapped attribute from the original item
|
|
42
45
|
*
|
|
43
46
|
* @param {string} fieldName - Name of the field to get
|
|
47
|
+
* @param {any} defaultValue - Default value to return if not found
|
|
44
48
|
* @returns {any|null} - The attribute value or null if not found
|
|
45
49
|
*/
|
|
46
|
-
get(fieldName) {
|
|
47
|
-
return this.has(fieldName) ? this
|
|
50
|
+
get(fieldName, defaultValue = null) {
|
|
51
|
+
return this.has(fieldName) ? this.#value[this.fields[fieldName]] : defaultValue
|
|
48
52
|
}
|
|
49
53
|
|
|
50
54
|
/**
|
|
@@ -54,6 +58,27 @@ export class Proxy {
|
|
|
54
58
|
*/
|
|
55
59
|
has(fieldName) {
|
|
56
60
|
const mappedField = this.fields[fieldName]
|
|
57
|
-
return !isNil(mappedField) && has(mappedField, this
|
|
61
|
+
return !isNil(mappedField) && has(mappedField, this.#value)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Identifies if the item has children
|
|
66
|
+
*/
|
|
67
|
+
get hasChildren() {
|
|
68
|
+
return (
|
|
69
|
+
typeof this.#original === 'object' &&
|
|
70
|
+
Array.isArray(this.#value[this.fields.children]) &&
|
|
71
|
+
this.#value[this.fields.children].length > 0
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
get expanded() {
|
|
76
|
+
return this.has('expanded') ? this.#value[this.fields.expanded] : false
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
set expanded(value) {
|
|
80
|
+
if (typeof this.#original === 'object') {
|
|
81
|
+
this.#value[this.fields.expanded] = Boolean(value)
|
|
82
|
+
}
|
|
58
83
|
}
|
|
59
84
|
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Handles navigation through a flattened data structure
|
|
3
|
+
*/
|
|
4
|
+
export class Traversal {
|
|
5
|
+
#dataProvider
|
|
6
|
+
#currentKey = $state(null)
|
|
7
|
+
#currentIndex = $derived(this.getCurrentIndex())
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {object} dataProvider - Data provider component
|
|
11
|
+
*/
|
|
12
|
+
constructor(dataProvider) {
|
|
13
|
+
this.#dataProvider = dataProvider
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Gets the current focused key
|
|
18
|
+
* @returns {string|null} The current key or null if none selected
|
|
19
|
+
*/
|
|
20
|
+
get currentKey() {
|
|
21
|
+
return this.#currentKey
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Gets the current focused index
|
|
26
|
+
* @returns {number} The current index or -1 if none selected
|
|
27
|
+
*/
|
|
28
|
+
get currentIndex() {
|
|
29
|
+
return this.#currentIndex
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Gets the currently focused item
|
|
34
|
+
* @returns {object|null} The focused item or null
|
|
35
|
+
*/
|
|
36
|
+
get focused() {
|
|
37
|
+
return this.#currentKey ? this.#dataProvider.getItemByKey(this.#currentKey) : null
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Calculates the current index based on the current key
|
|
42
|
+
* @private
|
|
43
|
+
* @returns {number} The current index or -1 if not found
|
|
44
|
+
*/
|
|
45
|
+
getCurrentIndex() {
|
|
46
|
+
if (!this.#currentKey) return -1
|
|
47
|
+
|
|
48
|
+
const index = this.#dataProvider.getIndexForKey(this.#currentKey)
|
|
49
|
+
return index !== undefined ? index : -1
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Focuses on an item by its key
|
|
54
|
+
* @param {string} key - Key of the item to focus
|
|
55
|
+
* @returns {boolean} True if focus changed, false otherwise
|
|
56
|
+
*/
|
|
57
|
+
moveToKey(key) {
|
|
58
|
+
if (!key || !this.#dataProvider.lookup.has(key)) return false
|
|
59
|
+
if (this.#currentKey === key) return false
|
|
60
|
+
|
|
61
|
+
this.#currentKey = key
|
|
62
|
+
return true
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Focuses on an item by its index in the flattened list
|
|
67
|
+
* @param {number} index - Index of the item to focus
|
|
68
|
+
* @returns {boolean} True if focus changed, false otherwise
|
|
69
|
+
*/
|
|
70
|
+
moveToIndex(index) {
|
|
71
|
+
const nodes = this.#dataProvider.nodes
|
|
72
|
+
|
|
73
|
+
if (index < 0 || index >= nodes.length) return false
|
|
74
|
+
if (this.#currentIndex === index) return false
|
|
75
|
+
|
|
76
|
+
this.#currentKey = nodes[index].key
|
|
77
|
+
return true
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Focuses on an item by finding its value in the data
|
|
82
|
+
* @param {*} value - Value to find and focus
|
|
83
|
+
* @returns {boolean} True if found and focused, false otherwise
|
|
84
|
+
*/
|
|
85
|
+
moveToValue(value) {
|
|
86
|
+
if (!value) {
|
|
87
|
+
this.#currentKey = null
|
|
88
|
+
return true
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const key = this.#dataProvider.getKeyForValue(value)
|
|
92
|
+
if (!key) return false
|
|
93
|
+
|
|
94
|
+
return this.moveToKey(key)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Moves focus to the previous visible item
|
|
99
|
+
* @returns {boolean} True if moved, false if at the beginning
|
|
100
|
+
*/
|
|
101
|
+
movePrev() {
|
|
102
|
+
if (this.#currentIndex > 0) {
|
|
103
|
+
return this.moveToIndex(this.#currentIndex - 1)
|
|
104
|
+
} else if (this.#currentIndex < 0 && this.#dataProvider.nodes.length > 0) {
|
|
105
|
+
return this.moveLast() // If not focused, go to last item
|
|
106
|
+
}
|
|
107
|
+
return false
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Moves focus to the next visible item
|
|
112
|
+
* @returns {boolean} True if moved, false if at the end
|
|
113
|
+
*/
|
|
114
|
+
moveNext() {
|
|
115
|
+
if (this.#currentIndex < this.#dataProvider.nodes.length - 1) {
|
|
116
|
+
return this.moveToIndex(this.#currentIndex + 1)
|
|
117
|
+
}
|
|
118
|
+
return false
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Moves focus to the first item
|
|
123
|
+
* @returns {boolean} True if moved, false otherwise
|
|
124
|
+
*/
|
|
125
|
+
moveFirst() {
|
|
126
|
+
return this.#dataProvider.nodes.length > 0 ? this.moveToIndex(0) : false
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Moves focus to the last item
|
|
131
|
+
* @returns {boolean} True if moved, false otherwise
|
|
132
|
+
*/
|
|
133
|
+
moveLast() {
|
|
134
|
+
const lastIndex = this.#dataProvider.nodes.length - 1
|
|
135
|
+
return lastIndex >= 0 ? this.moveToIndex(lastIndex) : false
|
|
136
|
+
}
|
|
137
|
+
}
|