@rokkit/states 1.0.0-next.108 → 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 +17 -17
- package/src/list-controller.svelte.js +9 -8
- package/src/nested-controller.svelte.js +39 -17
- 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-controller-dup.svelte.js +0 -155
- package/src/list-proxy.svelte.js +0 -127
- package/src/nested-proxy.svelte.js +0 -286
- 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",
|
package/src/derive.svelte.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getKeyFromPath, defaultFields, getNestedFields } from '@rokkit/core'
|
|
2
|
-
|
|
2
|
+
import { Proxy } from './proxy.svelte'
|
|
3
3
|
/**
|
|
4
4
|
*
|
|
5
5
|
* @param {Array<*>} items
|
|
@@ -28,30 +28,30 @@ export function flatVisibleNodes(items, fields = defaultFields, path = []) {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
|
-
* Derives a flat lookup table for the given items, using
|
|
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
|
|
32
33
|
*
|
|
33
|
-
* @param {Array<*>} items
|
|
34
|
-
* @param {import('@rokkit/core').FieldMapping} fields
|
|
35
|
-
* @param {Array<number>} path
|
|
36
|
-
* @returns {
|
|
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
|
|
37
38
|
*/
|
|
38
|
-
export function
|
|
39
|
-
const lookup =
|
|
39
|
+
export function deriveLookupWithProxy(items, fields = defaultFields, path = []) {
|
|
40
|
+
const lookup = new Map()
|
|
40
41
|
|
|
41
42
|
items.forEach((item, index) => {
|
|
42
43
|
const itemPath = [...path, index]
|
|
43
44
|
const key = getKeyFromPath(itemPath)
|
|
45
|
+
const proxy = new Proxy(item, fields)
|
|
44
46
|
|
|
45
|
-
lookup
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
Array.isArray(item[fields.children]) &&
|
|
49
|
-
item[fields.children].length > 0
|
|
50
|
-
|
|
51
|
-
if (hasChildren) {
|
|
47
|
+
lookup.set(key, proxy)
|
|
48
|
+
// console.log(key, proxy.value)
|
|
49
|
+
if (proxy.hasChildren) {
|
|
52
50
|
const childFields = getNestedFields(fields)
|
|
53
|
-
const
|
|
54
|
-
|
|
51
|
+
const childLookup = deriveLookupWithProxy(proxy.get('children'), childFields, itemPath)
|
|
52
|
+
for (const [childKey, childValue] of childLookup.entries()) {
|
|
53
|
+
lookup.set(childKey, childValue)
|
|
54
|
+
}
|
|
55
55
|
}
|
|
56
56
|
})
|
|
57
57
|
return lookup
|
|
@@ -1,28 +1,29 @@
|
|
|
1
1
|
import { FieldMapper, defaultFields } from '@rokkit/core'
|
|
2
2
|
import { equals } from 'ramda'
|
|
3
3
|
import { SvelteSet } from 'svelte/reactivity'
|
|
4
|
-
import { flatVisibleNodes } from './derive.svelte'
|
|
4
|
+
import { deriveLookupWithProxy, flatVisibleNodes } from './derive.svelte'
|
|
5
5
|
|
|
6
6
|
export class ListController {
|
|
7
7
|
items = $state(null)
|
|
8
8
|
fields = defaultFields
|
|
9
9
|
mappers = []
|
|
10
10
|
#options = $state({})
|
|
11
|
-
lookup = new Map()
|
|
11
|
+
// lookup = new Map()
|
|
12
12
|
selectedKeys = new SvelteSet()
|
|
13
13
|
focusedKey = $state(null)
|
|
14
14
|
#currentIndex = -1
|
|
15
15
|
|
|
16
|
-
selected = $derived(Array.from(this.selectedKeys).map((key) => this.lookup.get(key)))
|
|
17
|
-
focused = $derived(this.lookup.get(this.focusedKey))
|
|
16
|
+
selected = $derived(Array.from(this.selectedKeys).map((key) => this.lookup.get(key).value))
|
|
17
|
+
focused = $derived(this.lookup.get(this.focusedKey)?.value)
|
|
18
18
|
data = $derived(flatVisibleNodes(this.items, this.fields))
|
|
19
|
+
lookup = $derived(deriveLookupWithProxy(this.items, this.fields))
|
|
19
20
|
|
|
20
21
|
constructor(items, value, fields, options) {
|
|
21
22
|
this.items = items
|
|
22
23
|
this.fields = { ...defaultFields, ...fields }
|
|
23
24
|
this.mappers.push(new FieldMapper(fields))
|
|
24
25
|
this.#options = { multiselect: false, ...options }
|
|
25
|
-
this.init(
|
|
26
|
+
this.init(value)
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
/**
|
|
@@ -30,8 +31,8 @@ export class ListController {
|
|
|
30
31
|
* @param {Array<*>} items
|
|
31
32
|
* @param {*} value
|
|
32
33
|
*/
|
|
33
|
-
init(
|
|
34
|
-
items.forEach((item, index) => this.lookup.set(String(index), item))
|
|
34
|
+
init(value) {
|
|
35
|
+
// items.forEach((item, index) => this.lookup.set(String(index), item))
|
|
35
36
|
this.moveToValue(value)
|
|
36
37
|
}
|
|
37
38
|
|
|
@@ -145,7 +146,7 @@ export class ListController {
|
|
|
145
146
|
if (!this.lookup.has(key)) return false
|
|
146
147
|
|
|
147
148
|
if (this.focusedKey !== key) {
|
|
148
|
-
const { index } = this.findByValue(this.lookup.get(key))
|
|
149
|
+
const { index } = this.findByValue(this.lookup.get(key).value)
|
|
149
150
|
this.moveToIndex(index)
|
|
150
151
|
}
|
|
151
152
|
|
|
@@ -3,16 +3,12 @@ import { equals } from 'ramda'
|
|
|
3
3
|
import { ListController } from './list-controller.svelte'
|
|
4
4
|
|
|
5
5
|
export class NestedController extends ListController {
|
|
6
|
-
constructor(items, value, fields, options) {
|
|
7
|
-
super(items, value, fields, options)
|
|
8
|
-
}
|
|
9
|
-
|
|
10
6
|
/**
|
|
11
7
|
* @protected
|
|
12
8
|
* @param {Object} [value]
|
|
13
9
|
*/
|
|
14
|
-
init(
|
|
15
|
-
this.createLookup(items)
|
|
10
|
+
init(value) {
|
|
11
|
+
// this.createLookup(items)
|
|
16
12
|
if (value) {
|
|
17
13
|
this.ensureVisible(value)
|
|
18
14
|
this.moveToValue(value)
|
|
@@ -45,9 +41,14 @@ export class NestedController extends ListController {
|
|
|
45
41
|
})
|
|
46
42
|
}
|
|
47
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Mark parents as expanded so that item is visible
|
|
46
|
+
* @param {*} value
|
|
47
|
+
* @returns
|
|
48
|
+
*/
|
|
48
49
|
ensureVisible(value) {
|
|
49
|
-
const result = this.lookup.entries().find((entry) => equals(entry[1], value))
|
|
50
|
-
|
|
50
|
+
const result = this.lookup.entries().find((entry) => equals(entry[1].value, value))
|
|
51
|
+
// console.log(result)
|
|
51
52
|
const path = getPathFromKey(result[0])
|
|
52
53
|
|
|
53
54
|
for (let i = 1; i < path.length; i++) {
|
|
@@ -57,30 +58,51 @@ export class NestedController extends ListController {
|
|
|
57
58
|
return true
|
|
58
59
|
}
|
|
59
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Toggle expansion of item
|
|
63
|
+
* @param {*} value
|
|
64
|
+
* @returns
|
|
65
|
+
*/
|
|
60
66
|
toggleExpansion(key) {
|
|
61
67
|
if (!this.lookup.has(key)) return false
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
item
|
|
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]
|
|
65
73
|
return true
|
|
66
74
|
}
|
|
67
75
|
|
|
76
|
+
/**
|
|
77
|
+
* Expand item
|
|
78
|
+
* @param {*} value
|
|
79
|
+
* @returns
|
|
80
|
+
*/
|
|
68
81
|
expand(key) {
|
|
69
82
|
const actualKey = key ?? this.focusedKey
|
|
70
83
|
if (!this.lookup.has(actualKey)) return false
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
item
|
|
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
|
|
74
89
|
|
|
75
90
|
return true
|
|
76
91
|
}
|
|
77
92
|
|
|
93
|
+
/**
|
|
94
|
+
* Collapse item
|
|
95
|
+
* @param {*} value
|
|
96
|
+
* @returns
|
|
97
|
+
*/
|
|
78
98
|
collapse(key) {
|
|
79
99
|
const actualKey = key ?? this.focusedKey
|
|
80
100
|
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
|
|
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
|
|
84
106
|
return true
|
|
85
107
|
}
|
|
86
108
|
|
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
|
+
}
|
package/src/base-proxy.svelte.js
DELETED
|
@@ -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
|
-
}
|