@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.
@@ -1,243 +0,0 @@
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
- }
@@ -1,127 +0,0 @@
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
- }
@@ -1,286 +0,0 @@
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
- }