@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.
@@ -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
+ }