@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
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { BaseProxy } from './base-proxy.svelte.js'
|
|
2
|
+
import { NodeProxy } from './node-proxy.svelte.js'
|
|
3
|
+
import { equals } from 'ramda'
|
|
4
|
+
/**
|
|
5
|
+
* Manages a flat list of nodes with selection and focus capabilities
|
|
6
|
+
*/
|
|
7
|
+
export class ListProxy extends BaseProxy {
|
|
8
|
+
/**
|
|
9
|
+
* Creates a new ListProxy
|
|
10
|
+
*
|
|
11
|
+
* @param {Object[]} data - Original data array
|
|
12
|
+
* @param {Object} value - Active value in the list
|
|
13
|
+
* @param {Object} fields - Field mappings
|
|
14
|
+
* @param {Object} options - Configuration options
|
|
15
|
+
*/
|
|
16
|
+
constructor(data, value, fields = {}, options = {}) {
|
|
17
|
+
super(data, value, fields, options)
|
|
18
|
+
this.moveToValue(value)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Updates the proxy with new data
|
|
23
|
+
*
|
|
24
|
+
* @param {any[]} data - New data to use
|
|
25
|
+
* @returns {ListProxy} - This proxy for method chaining
|
|
26
|
+
*/
|
|
27
|
+
update(data) {
|
|
28
|
+
this.data = data || null
|
|
29
|
+
|
|
30
|
+
// Create node proxies for all items
|
|
31
|
+
if (data && Array.isArray(data)) {
|
|
32
|
+
this.nodes = data.map((item, index) => new NodeProxy(item, [index], this.fields))
|
|
33
|
+
} else {
|
|
34
|
+
this.nodes = []
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
this.reset()
|
|
38
|
+
return this
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Moves focus to the specified target (index or item)
|
|
43
|
+
*
|
|
44
|
+
* @param {number|object} target - Index of the node or the node item to focus
|
|
45
|
+
* @returns {boolean} - Whether the move was successful
|
|
46
|
+
*/
|
|
47
|
+
moveTo(target) {
|
|
48
|
+
const index = Array.isArray(target) ? target[0] : target
|
|
49
|
+
|
|
50
|
+
// Validate index
|
|
51
|
+
if (index < 0 || index >= this.nodes.length) {
|
|
52
|
+
return false
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Update focus
|
|
56
|
+
if (this.currentNode) this.currentNode.focused = false
|
|
57
|
+
|
|
58
|
+
this.currentNode = this.nodes[index]
|
|
59
|
+
this.currentNode.focused = true
|
|
60
|
+
|
|
61
|
+
return true
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Moves focus to the next node
|
|
66
|
+
*
|
|
67
|
+
* @returns {boolean} - Whether the move was successful
|
|
68
|
+
*/
|
|
69
|
+
moveNext() {
|
|
70
|
+
if (!this.nodes.length) return false
|
|
71
|
+
|
|
72
|
+
let nextIndex = 0
|
|
73
|
+
|
|
74
|
+
if (this.currentNode) {
|
|
75
|
+
const currentIndex = this.nodes.indexOf(this.currentNode)
|
|
76
|
+
nextIndex = currentIndex + 1
|
|
77
|
+
|
|
78
|
+
// If at end of list, stay at current position
|
|
79
|
+
if (nextIndex >= this.nodes.length) {
|
|
80
|
+
return false
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return this.moveTo(nextIndex)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Moves focus to the previous node
|
|
89
|
+
*
|
|
90
|
+
* @returns {boolean} - Whether the move was successful
|
|
91
|
+
*/
|
|
92
|
+
movePrev() {
|
|
93
|
+
if (!this.nodes.length) return false
|
|
94
|
+
|
|
95
|
+
if (!this.currentNode) {
|
|
96
|
+
return this.moveTo(this.nodes.length - 1)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const currentIndex = this.nodes.indexOf(this.currentNode)
|
|
100
|
+
const prevIndex = currentIndex - 1
|
|
101
|
+
|
|
102
|
+
// If at start of list, stay at current position
|
|
103
|
+
if (prevIndex < 0) {
|
|
104
|
+
return false
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return this.moveTo(prevIndex)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Finds a node by value and makes it the current & active node
|
|
112
|
+
*
|
|
113
|
+
* @param {any} value
|
|
114
|
+
* @returns
|
|
115
|
+
*/
|
|
116
|
+
moveToValue(value) {
|
|
117
|
+
if (!value || equals(this.currentNode?.value, value)) return false
|
|
118
|
+
|
|
119
|
+
const path = this.findPathIndex((node) => equals(node.value, value))
|
|
120
|
+
if (path.length > 0) {
|
|
121
|
+
this.moveTo(path)
|
|
122
|
+
this.select()
|
|
123
|
+
return true
|
|
124
|
+
}
|
|
125
|
+
return false
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
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
|
+
constructor(items, value, fields, options) {
|
|
7
|
+
super(items, value, fields, options)
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @protected
|
|
12
|
+
* @param {Object} [value]
|
|
13
|
+
*/
|
|
14
|
+
init(items, value) {
|
|
15
|
+
this.createLookup(items)
|
|
16
|
+
if (value) {
|
|
17
|
+
this.ensureVisible(value)
|
|
18
|
+
this.moveToValue(value)
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* @private
|
|
23
|
+
* @param {Object[]} items
|
|
24
|
+
* @param {number[]} [path]=[]
|
|
25
|
+
*/
|
|
26
|
+
createLookup(items, path = []) {
|
|
27
|
+
const depth = path.length
|
|
28
|
+
if (depth >= this.mappers.length) {
|
|
29
|
+
this.mappers.push(this.mappers[depth - 1].getChildMapper())
|
|
30
|
+
}
|
|
31
|
+
const fm = this.mappers[depth]
|
|
32
|
+
|
|
33
|
+
items.forEach((item, index) => {
|
|
34
|
+
const itemPath = [...path, index]
|
|
35
|
+
const key = getKeyFromPath(itemPath)
|
|
36
|
+
|
|
37
|
+
this.lookup.set(key, item)
|
|
38
|
+
if (fm.get('selected', item)) {
|
|
39
|
+
this.selectedKeys.add(key)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (fm.hasChildren(item)) {
|
|
43
|
+
this.createLookup(fm.get('children', item), itemPath)
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
ensureVisible(value) {
|
|
49
|
+
const result = this.lookup.entries().find((entry) => equals(entry[1], value))
|
|
50
|
+
if (!Array.isArray(result)) return false
|
|
51
|
+
const path = getPathFromKey(result[0])
|
|
52
|
+
|
|
53
|
+
for (let i = 1; i < path.length; i++) {
|
|
54
|
+
const nodeKey = getKeyFromPath(path.slice(0, i))
|
|
55
|
+
this.expand(nodeKey)
|
|
56
|
+
}
|
|
57
|
+
return true
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
toggleExpansion(key) {
|
|
61
|
+
if (!this.lookup.has(key)) return false
|
|
62
|
+
const item = this.lookup.get(key)
|
|
63
|
+
const fields = this.fieldsFor(key)
|
|
64
|
+
item[fields.expanded] = !item[this.fields.expanded]
|
|
65
|
+
return true
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
expand(key) {
|
|
69
|
+
const actualKey = key ?? this.focusedKey
|
|
70
|
+
if (!this.lookup.has(actualKey)) return false
|
|
71
|
+
const item = this.lookup.get(actualKey)
|
|
72
|
+
const fields = this.fieldsFor(actualKey)
|
|
73
|
+
item[fields.expanded] = true
|
|
74
|
+
|
|
75
|
+
return true
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
collapse(key) {
|
|
79
|
+
const actualKey = key ?? this.focusedKey
|
|
80
|
+
if (!this.lookup.has(actualKey)) return false
|
|
81
|
+
const item = this.lookup.get(actualKey)
|
|
82
|
+
const fields = this.fieldsFor(actualKey)
|
|
83
|
+
item[fields.expanded] = false
|
|
84
|
+
return true
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
*
|
|
89
|
+
* @param {*} key
|
|
90
|
+
* @returns
|
|
91
|
+
*/
|
|
92
|
+
fieldsFor(key) {
|
|
93
|
+
const path = getPathFromKey(key)
|
|
94
|
+
let fields = this.fields
|
|
95
|
+
for (let i = 1; i < path.length; i++) {
|
|
96
|
+
fields = getNestedFields(fields)
|
|
97
|
+
}
|
|
98
|
+
return fields
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { BaseProxy } from './base-proxy.svelte.js'
|
|
2
|
+
import { NodeProxy } from './node-proxy.svelte.js'
|
|
3
|
+
import { equals } from 'ramda'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Manages a hierarchical tree of nodes with selection, focus and expansion capabilities
|
|
7
|
+
*/
|
|
8
|
+
export class NestedProxy extends BaseProxy {
|
|
9
|
+
/**
|
|
10
|
+
* Creates a new NestedProxy
|
|
11
|
+
*
|
|
12
|
+
* @param {any[]} data - Original hierarchical data array
|
|
13
|
+
* @param {any} value - Initial value for the proxy
|
|
14
|
+
* @param {Object} fields - Field mappings
|
|
15
|
+
* @param {Object} options - Configuration options
|
|
16
|
+
*/
|
|
17
|
+
constructor(data, value, fields = {}, options = {}) {
|
|
18
|
+
// Default options for tree structures
|
|
19
|
+
const defaultTreeOptions = {
|
|
20
|
+
expandedByDefault: false
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
super(data, value, fields, { ...defaultTreeOptions, ...options })
|
|
24
|
+
this.moveToValue(value)
|
|
25
|
+
// this._refreshFlatNodes()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Refreshes the flatNodes
|
|
30
|
+
* @private
|
|
31
|
+
*/
|
|
32
|
+
_refreshFlatNodes(nodes = null) {
|
|
33
|
+
if (!nodes) {
|
|
34
|
+
this.visibleNodes = []
|
|
35
|
+
this._refreshFlatNodes(this.nodes)
|
|
36
|
+
} else {
|
|
37
|
+
nodes.forEach((node) => {
|
|
38
|
+
this.visibleNodes.push(node)
|
|
39
|
+
if (node.hasChildren() && node.expanded) {
|
|
40
|
+
this._refreshFlatNodes(node.children)
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Processes hierarchical data into node proxies
|
|
48
|
+
*
|
|
49
|
+
* @private
|
|
50
|
+
* @param {any[]} items - Items to process
|
|
51
|
+
*/
|
|
52
|
+
processNodes(items) {
|
|
53
|
+
if (!items || !Array.isArray(items)) return
|
|
54
|
+
|
|
55
|
+
this.nodes = []
|
|
56
|
+
items.forEach((item, index) => {
|
|
57
|
+
const node = new NodeProxy(item, [index], this.fields)
|
|
58
|
+
this.nodes.push(node)
|
|
59
|
+
})
|
|
60
|
+
this._refreshFlatNodes()
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Updates the proxy with new data
|
|
65
|
+
*
|
|
66
|
+
* @param {any[]} data - New hierarchical data to use
|
|
67
|
+
* @returns {NestedProxy} - This proxy for method chaining
|
|
68
|
+
*/
|
|
69
|
+
update(data) {
|
|
70
|
+
this.data = data || null
|
|
71
|
+
this.reset()
|
|
72
|
+
|
|
73
|
+
if (!data) {
|
|
74
|
+
this.visibleNodes = []
|
|
75
|
+
return this
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Create node tree and flatten visible nodes
|
|
79
|
+
this.processNodes(data)
|
|
80
|
+
return this
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Moves focus to the specified target (index, path)
|
|
85
|
+
*
|
|
86
|
+
* @param {number|number[]} target - index or path to move to
|
|
87
|
+
* @returns {boolean} - Whether the move was successful
|
|
88
|
+
*/
|
|
89
|
+
moveTo(target) {
|
|
90
|
+
const targetNode = Array.isArray(target)
|
|
91
|
+
? this.find((node) => equals(node.path, target))
|
|
92
|
+
: this.visibleNodes[target]
|
|
93
|
+
|
|
94
|
+
if (!targetNode) return false
|
|
95
|
+
|
|
96
|
+
// Update focus
|
|
97
|
+
if (this.currentNode) {
|
|
98
|
+
this.currentNode.focused = false
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
this.currentNode = targetNode
|
|
102
|
+
this.currentNode.focused = true
|
|
103
|
+
|
|
104
|
+
return true
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Moves focus to the next visible node
|
|
109
|
+
*
|
|
110
|
+
* @returns {boolean} - Whether the move was successful
|
|
111
|
+
*/
|
|
112
|
+
moveNext() {
|
|
113
|
+
if (!this.visibleNodes.length) return false
|
|
114
|
+
|
|
115
|
+
let nextIndex = 0
|
|
116
|
+
|
|
117
|
+
if (this.currentNode) {
|
|
118
|
+
const currentIndex = this.visibleNodes.indexOf(this.currentNode)
|
|
119
|
+
nextIndex = currentIndex + 1
|
|
120
|
+
|
|
121
|
+
// If at end of list, stay at current position
|
|
122
|
+
if (nextIndex >= this.visibleNodes.length) {
|
|
123
|
+
return false
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return this.moveTo(nextIndex)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Moves focus to the previous visible node
|
|
132
|
+
*
|
|
133
|
+
* @returns {boolean} - Whether the move was successful
|
|
134
|
+
*/
|
|
135
|
+
movePrev() {
|
|
136
|
+
if (!this.visibleNodes.length) return false
|
|
137
|
+
|
|
138
|
+
if (!this.currentNode) {
|
|
139
|
+
return this.moveTo(this.visibleNodes.length - 1)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const currentIndex = this.visibleNodes.indexOf(this.currentNode)
|
|
143
|
+
const prevIndex = currentIndex - 1
|
|
144
|
+
|
|
145
|
+
// If at start of list, stay at current position
|
|
146
|
+
if (prevIndex < 0) {
|
|
147
|
+
return false
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return this.moveTo(prevIndex)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Expand the current node
|
|
155
|
+
*
|
|
156
|
+
* @param {number[]} [path] - The path to the node to expand
|
|
157
|
+
* @returns {boolean} - Whether the node was expanded
|
|
158
|
+
*/
|
|
159
|
+
expand(path) {
|
|
160
|
+
const node = path ? this.getNodeByPath(path) : this.currentNode
|
|
161
|
+
if (!node || !node.hasChildren() || node.expanded) {
|
|
162
|
+
return false
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
node.expanded = true
|
|
166
|
+
this._refreshFlatNodes()
|
|
167
|
+
return true
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Collapse the current node
|
|
172
|
+
*
|
|
173
|
+
* @param {number[]} [path] - The path to the node to collapse
|
|
174
|
+
* @returns {boolean} - Whether the node was collapsed
|
|
175
|
+
*/
|
|
176
|
+
collapse(path) {
|
|
177
|
+
const node = path ? this.getNodeByPath(path) : this.currentNode
|
|
178
|
+
if (!node || !node.hasChildren() || !node.expanded) return false
|
|
179
|
+
|
|
180
|
+
node.expanded = false
|
|
181
|
+
this._refreshFlatNodes()
|
|
182
|
+
return true
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Toggle expanded/collapsed state of current node
|
|
187
|
+
*
|
|
188
|
+
* @param {number[]} [path] - The path to the node to toggle expansion
|
|
189
|
+
* @returns {boolean} - Whether the state changed
|
|
190
|
+
*/
|
|
191
|
+
toggleExpansion(path) {
|
|
192
|
+
const node = path ? this.getNodeByPath(path) : this.currentNode
|
|
193
|
+
|
|
194
|
+
if (!node || !node.hasChildren()) {
|
|
195
|
+
return false
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
node.expanded = !node.expanded
|
|
199
|
+
this._refreshFlatNodes()
|
|
200
|
+
return true
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Expands all nodes
|
|
205
|
+
*
|
|
206
|
+
* @returns {NestedProxy} - This proxy for method chaining
|
|
207
|
+
*/
|
|
208
|
+
expandAll() {
|
|
209
|
+
this.visibleNodes.forEach((node) => {
|
|
210
|
+
node.expandAll()
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
this._refreshFlatNodes()
|
|
214
|
+
return this
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Collapses all nodes
|
|
219
|
+
*
|
|
220
|
+
* @returns {NestedProxy} - This proxy for method chaining
|
|
221
|
+
*/
|
|
222
|
+
collapseAll() {
|
|
223
|
+
this.visibleNodes.forEach((node) => {
|
|
224
|
+
node.collapseAll()
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
this._refreshFlatNodes()
|
|
228
|
+
return this
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Resets selection and expansion state
|
|
233
|
+
*
|
|
234
|
+
* @returns {NestedProxy} - This proxy for method chaining
|
|
235
|
+
*/
|
|
236
|
+
reset() {
|
|
237
|
+
// Clear focus
|
|
238
|
+
if (this.currentNode) {
|
|
239
|
+
this.currentNode.focused = false
|
|
240
|
+
this.currentNode = null
|
|
241
|
+
}
|
|
242
|
+
if (this.options.expandedByDefault) this.expandAll()
|
|
243
|
+
this.nodes.forEach((node) => node.resetStates())
|
|
244
|
+
this.selectedNodes.clear()
|
|
245
|
+
this._refreshFlatNodes()
|
|
246
|
+
|
|
247
|
+
return this
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Ensures a node is visible by expanding its ancestors
|
|
252
|
+
*
|
|
253
|
+
* @param {NodeProxy} node - Node to make visible
|
|
254
|
+
* @returns {boolean} - Whether the node is now visible
|
|
255
|
+
*/
|
|
256
|
+
ensureVisible(node) {
|
|
257
|
+
if (!node || !node.path || node.path.length <= 1) return true
|
|
258
|
+
|
|
259
|
+
for (let i = 1; i < node.path.length; i++) {
|
|
260
|
+
const parentNode = this.getNodeByPath(node.path.slice(0, i))
|
|
261
|
+
parentNode.expanded = true
|
|
262
|
+
}
|
|
263
|
+
this._refreshFlatNodes()
|
|
264
|
+
return true
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Finds a node by value and makes it the current & active node
|
|
269
|
+
*
|
|
270
|
+
* @param {any} value
|
|
271
|
+
* @returns
|
|
272
|
+
*/
|
|
273
|
+
moveToValue(value) {
|
|
274
|
+
if (!value || equals(this.currentNode?.value, value)) return false
|
|
275
|
+
|
|
276
|
+
const targetNode = this.find((node) => equals(node.value, value))
|
|
277
|
+
|
|
278
|
+
if (targetNode) {
|
|
279
|
+
this.ensureVisible(targetNode)
|
|
280
|
+
this.moveTo(targetNode.path)
|
|
281
|
+
this.select()
|
|
282
|
+
return true
|
|
283
|
+
}
|
|
284
|
+
return false
|
|
285
|
+
}
|
|
286
|
+
}
|