@rokkit/states 1.0.0-next.125 → 1.0.0-next.128
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 +5 -6
- package/src/constants.js +1 -0
- package/src/derive.svelte.js +30 -18
- package/src/index.js +7 -3
- package/src/lazy-wrapper.svelte.js +119 -0
- package/src/list-controller.svelte.js +128 -16
- package/src/media.svelte.js +24 -0
- package/src/messages.svelte.js +71 -0
- package/src/proxy-item.svelte.js +320 -0
- package/src/proxy-tree.svelte.js +158 -0
- package/src/table-controller.svelte.js +199 -0
- package/src/vibe.svelte.js +36 -5
- package/src/wrapper.svelte.js +231 -0
- package/src/nested-controller.svelte.js +0 -71
- package/src/proxy.svelte.js +0 -126
package/src/vibe.svelte.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/** @typedef {'light' | 'dark'} ThemeMode */
|
|
2
2
|
/** @typedef {'cozy' | 'compact' | 'comfortable'} Density */
|
|
3
|
+
/** @typedef {'ltr' | 'rtl'} Direction */
|
|
3
4
|
|
|
4
|
-
import { defaultColors,
|
|
5
|
-
import { DEFAULT_STYLES, VALID_DENSITIES, VALID_MODES } from './constants'
|
|
5
|
+
import { defaultColors, DEFAULT_THEME_MAPPING, themeRules, detectDirection } from '@rokkit/core'
|
|
6
|
+
import { DEFAULT_STYLES, VALID_DENSITIES, VALID_MODES, VALID_DIRECTIONS } from './constants'
|
|
6
7
|
import { has } from 'ramda'
|
|
7
8
|
|
|
8
9
|
/**
|
|
@@ -25,7 +26,8 @@ class Vibe {
|
|
|
25
26
|
#style = $state('rokkit')
|
|
26
27
|
#colors = $state(defaultColors)
|
|
27
28
|
#density = $state('comfortable')
|
|
28
|
-
#
|
|
29
|
+
#direction = $state(detectDirection())
|
|
30
|
+
#colorMap = $state(DEFAULT_THEME_MAPPING)
|
|
29
31
|
#palette = $derived.by(() => themeRules(this.#colorMap, this.#colors))
|
|
30
32
|
|
|
31
33
|
/**
|
|
@@ -61,7 +63,7 @@ class Vibe {
|
|
|
61
63
|
if (missing.length > 0) {
|
|
62
64
|
throw new Error(`Did you forget to define "${missing.join(', ')}"?`)
|
|
63
65
|
}
|
|
64
|
-
this.#colorMap = { ...
|
|
66
|
+
this.#colorMap = { ...DEFAULT_THEME_MAPPING, ...value }
|
|
65
67
|
}
|
|
66
68
|
}
|
|
67
69
|
|
|
@@ -105,6 +107,21 @@ class Vibe {
|
|
|
105
107
|
}
|
|
106
108
|
}
|
|
107
109
|
|
|
110
|
+
get direction() {
|
|
111
|
+
return this.#direction
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
set direction(value) {
|
|
115
|
+
if (isAllowedValue(value, VALID_DIRECTIONS, this.#direction)) {
|
|
116
|
+
this.#direction = value
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** @returns {boolean} */
|
|
121
|
+
get isRTL() {
|
|
122
|
+
return this.#direction === 'rtl'
|
|
123
|
+
}
|
|
124
|
+
|
|
108
125
|
get palette() {
|
|
109
126
|
return this.#palette
|
|
110
127
|
}
|
|
@@ -133,7 +150,12 @@ class Vibe {
|
|
|
133
150
|
if (!key) throw new Error('Key is required')
|
|
134
151
|
|
|
135
152
|
try {
|
|
136
|
-
const config = {
|
|
153
|
+
const config = {
|
|
154
|
+
style: this.#style,
|
|
155
|
+
mode: this.#mode,
|
|
156
|
+
density: this.#density,
|
|
157
|
+
direction: this.#direction
|
|
158
|
+
}
|
|
137
159
|
localStorage.setItem(key, JSON.stringify(config))
|
|
138
160
|
} catch (e) {
|
|
139
161
|
// eslint-disable-next-line no-console
|
|
@@ -149,6 +171,15 @@ class Vibe {
|
|
|
149
171
|
this.style = value.style
|
|
150
172
|
this.mode = value.mode
|
|
151
173
|
this.density = value.density
|
|
174
|
+
this.direction = value.direction
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Re-detect direction from document
|
|
179
|
+
* Useful when lang attribute changes dynamically
|
|
180
|
+
*/
|
|
181
|
+
detectDirection() {
|
|
182
|
+
this.#direction = detectDirection()
|
|
152
183
|
}
|
|
153
184
|
}
|
|
154
185
|
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wrapper
|
|
3
|
+
*
|
|
4
|
+
* Navigation controller for persistent list/tree/sidebar components.
|
|
5
|
+
* Accepts a ProxyTree instance for reactive data (flatView, lookup),
|
|
6
|
+
* and provides full navigation, expansion, selection, and typeahead logic.
|
|
7
|
+
*
|
|
8
|
+
* ProxyTree owns the data layer: items -> proxies -> flatView + lookup.
|
|
9
|
+
* Wrapper owns the navigation layer: focusedKey, movement, selection callbacks.
|
|
10
|
+
*
|
|
11
|
+
* Designed for any persistent (always-visible) component:
|
|
12
|
+
* - Sidebar navigation (links, collapsible groups)
|
|
13
|
+
* - List / Tree components
|
|
14
|
+
* - Any option list rendered inline
|
|
15
|
+
*
|
|
16
|
+
* Dropdown variants (Select, Menu) extend this class and override cancel() / blur()
|
|
17
|
+
* to close the dropdown and return focus to the trigger.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
export class Wrapper {
|
|
21
|
+
// ─── Data ──────────────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
#proxyTree
|
|
24
|
+
|
|
25
|
+
// flatView: re-derives from proxyTree's flatView, which itself re-derives
|
|
26
|
+
// when any proxy.expanded or proxy.children changes.
|
|
27
|
+
flatView = $derived(this.#proxyTree.flatView)
|
|
28
|
+
|
|
29
|
+
// Navigable items: exclude separators, spacers, and disabled items.
|
|
30
|
+
// This is the subset that keyboard navigation moves through.
|
|
31
|
+
#navigable = $derived(
|
|
32
|
+
this.flatView.filter(
|
|
33
|
+
(n) => n.type !== 'separator' && n.type !== 'spacer' && !n.proxy.disabled
|
|
34
|
+
)
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
// ─── State ──────────────────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
#focusedKey = $state(null)
|
|
40
|
+
|
|
41
|
+
// ─── Callbacks ──────────────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
#onselect
|
|
44
|
+
#onchange
|
|
45
|
+
#selectedValue = $state(undefined)
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @param {import('./proxy-tree.svelte.js').ProxyTree} proxyTree
|
|
49
|
+
* @param {{ onselect?: Function, onchange?: Function }} [options]
|
|
50
|
+
*/
|
|
51
|
+
constructor(proxyTree, options = {}) {
|
|
52
|
+
this.#proxyTree = proxyTree
|
|
53
|
+
this.#onselect = options.onselect
|
|
54
|
+
this.#onchange = options.onchange
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ─── IWrapper: state read by Navigator ─────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
get focusedKey() { return this.#focusedKey }
|
|
60
|
+
|
|
61
|
+
// ─── IWrapper: movement (path passed through but ignored) ──────────────────
|
|
62
|
+
|
|
63
|
+
/** Move focus to the next navigable item; clamp at end. */
|
|
64
|
+
next(_path) {
|
|
65
|
+
const nav = this.#navigable
|
|
66
|
+
if (!nav.length) return
|
|
67
|
+
const idx = nav.findIndex((n) => n.key === this.#focusedKey)
|
|
68
|
+
if (idx < nav.length - 1) this.#focusedKey = nav[idx + 1].key
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Move focus to the previous navigable item; clamp at start. */
|
|
72
|
+
prev(_path) {
|
|
73
|
+
const nav = this.#navigable
|
|
74
|
+
if (!nav.length) return
|
|
75
|
+
const idx = nav.findIndex((n) => n.key === this.#focusedKey)
|
|
76
|
+
if (idx > 0) this.#focusedKey = nav[idx - 1].key
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Move focus to the first navigable item. */
|
|
80
|
+
first(_path) {
|
|
81
|
+
const nav = this.#navigable
|
|
82
|
+
if (nav.length) this.#focusedKey = nav[0].key
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Move focus to the last navigable item. */
|
|
86
|
+
last(_path) {
|
|
87
|
+
const nav = this.#navigable
|
|
88
|
+
if (nav.length) this.#focusedKey = nav[nav.length - 1].key
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Expand focused group, or move focus into it if already open.
|
|
93
|
+
* No-op on leaf items.
|
|
94
|
+
*/
|
|
95
|
+
expand(_path) {
|
|
96
|
+
if (!this.#focusedKey) return
|
|
97
|
+
const node = this.flatView.find((n) => n.key === this.#focusedKey)
|
|
98
|
+
if (!node || !node.hasChildren) return
|
|
99
|
+
if (!node.proxy.expanded) {
|
|
100
|
+
node.proxy.expanded = true
|
|
101
|
+
} else {
|
|
102
|
+
// Already open — advance focus to first visible child
|
|
103
|
+
this.next(null)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Collapse focused group, or move focus to parent if already collapsed / leaf.
|
|
109
|
+
* At root level with no parent: no-op.
|
|
110
|
+
*/
|
|
111
|
+
collapse(_path) {
|
|
112
|
+
if (!this.#focusedKey) return
|
|
113
|
+
const node = this.flatView.find((n) => n.key === this.#focusedKey)
|
|
114
|
+
if (!node) return
|
|
115
|
+
if (node.hasChildren && node.proxy.expanded) {
|
|
116
|
+
node.proxy.expanded = false
|
|
117
|
+
} else {
|
|
118
|
+
// Move to parent: strip the last segment from the key
|
|
119
|
+
const parts = this.#focusedKey.split('-')
|
|
120
|
+
if (parts.length > 1) {
|
|
121
|
+
parts.pop()
|
|
122
|
+
this.#focusedKey = parts.join('-')
|
|
123
|
+
}
|
|
124
|
+
// At root level (no '-'): no-op — already at root
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ─── IWrapper: selection actions ───────────────────────────────────────────
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Select item at path (or focusedKey when path is null).
|
|
132
|
+
* Groups toggle expanded. Leaves fire onchange (value differs) and onselect callbacks.
|
|
133
|
+
*/
|
|
134
|
+
select(path) {
|
|
135
|
+
const key = path ?? this.#focusedKey
|
|
136
|
+
if (!key) return
|
|
137
|
+
this.#focusedKey = key
|
|
138
|
+
const proxy = this.#proxyTree.lookup.get(key)
|
|
139
|
+
if (!proxy) return
|
|
140
|
+
if (proxy.hasChildren) {
|
|
141
|
+
proxy.expanded = !proxy.expanded
|
|
142
|
+
} else {
|
|
143
|
+
if (proxy.value !== this.#selectedValue) {
|
|
144
|
+
this.#selectedValue = proxy.value
|
|
145
|
+
this.#onchange?.(proxy.value, proxy)
|
|
146
|
+
}
|
|
147
|
+
this.#onselect?.(proxy.value, proxy)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Toggle expansion of group at path — called by Navigator for accordion-trigger clicks.
|
|
153
|
+
* Unlike select(), this only applies to groups and never fires onselect.
|
|
154
|
+
*/
|
|
155
|
+
toggle(path) {
|
|
156
|
+
const key = path ?? this.#focusedKey
|
|
157
|
+
if (!key) return
|
|
158
|
+
const proxy = this.#proxyTree.lookup.get(key)
|
|
159
|
+
if (proxy?.hasChildren) proxy.expanded = !proxy.expanded
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Sync focused state to path — called by Navigator on focusin and typeahead match.
|
|
164
|
+
*/
|
|
165
|
+
moveTo(path) {
|
|
166
|
+
if (path !== null) this.#focusedKey = path
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Sync focused key to the item matching this semantic value.
|
|
171
|
+
* Used by controlled components (Toggle, Select) to keep navigation
|
|
172
|
+
* in sync when the bound value changes externally.
|
|
173
|
+
*
|
|
174
|
+
* @param {unknown} v
|
|
175
|
+
*/
|
|
176
|
+
moveToValue(v) {
|
|
177
|
+
if (v === undefined || v === null) return
|
|
178
|
+
for (const [key, proxy] of this.#proxyTree.lookup) {
|
|
179
|
+
if (proxy.value === v) {
|
|
180
|
+
this.#focusedKey = key
|
|
181
|
+
this.#selectedValue = v
|
|
182
|
+
return
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Persistent list: no dropdown to close. Override in dropdown wrappers. */
|
|
188
|
+
cancel(_path) {}
|
|
189
|
+
|
|
190
|
+
/** Persistent list: no-op. Override in dropdown wrappers to close + restore trigger focus. */
|
|
191
|
+
blur() {}
|
|
192
|
+
|
|
193
|
+
/** Multiselect toggle — not yet implemented. */
|
|
194
|
+
extend(_path) {}
|
|
195
|
+
|
|
196
|
+
/** Multiselect range — not yet implemented. */
|
|
197
|
+
range(_path) {}
|
|
198
|
+
|
|
199
|
+
// ─── IWrapper: typeahead ───────────────────────────────────────────────────
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Return the key of the first navigable item whose text starts with query
|
|
203
|
+
* (case-insensitive). Wraps around. startAfterKey enables cycling.
|
|
204
|
+
* Returns null if no match.
|
|
205
|
+
*
|
|
206
|
+
* @param {string} query
|
|
207
|
+
* @param {string|null} [startAfterKey]
|
|
208
|
+
* @returns {string|null}
|
|
209
|
+
*/
|
|
210
|
+
findByText(query, startAfterKey = null) {
|
|
211
|
+
const nav = this.#navigable
|
|
212
|
+
if (!nav.length) return null
|
|
213
|
+
const q = query.toLowerCase()
|
|
214
|
+
const startIdx = startAfterKey
|
|
215
|
+
? nav.findIndex((n) => n.key === startAfterKey) + 1
|
|
216
|
+
: 0
|
|
217
|
+
for (let i = 0; i < nav.length; i++) {
|
|
218
|
+
const node = nav[(startIdx + i) % nav.length]
|
|
219
|
+
if (node.proxy.label.toLowerCase().startsWith(q)) return node.key
|
|
220
|
+
}
|
|
221
|
+
return null
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ─── Helpers for the component ─────────────────────────────────────────────
|
|
225
|
+
|
|
226
|
+
/** @returns {Map<string, import('./proxy-item.svelte.js').ProxyItem>} */
|
|
227
|
+
get lookup() { return this.#proxyTree.lookup }
|
|
228
|
+
|
|
229
|
+
/** @returns {import('./proxy-tree.svelte.js').ProxyTree} */
|
|
230
|
+
get proxyTree() { return this.#proxyTree }
|
|
231
|
+
}
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import { getKeyFromPath, getPathFromKey } 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
|
-
if (value) {
|
|
12
|
-
this.ensureVisible(value)
|
|
13
|
-
this.moveToValue(value)
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Mark parents as expanded so that item is visible
|
|
19
|
-
* @param {*} value
|
|
20
|
-
* @returns
|
|
21
|
-
*/
|
|
22
|
-
ensureVisible(value) {
|
|
23
|
-
const result = this.lookup.entries().find((entry) => equals(entry[1].value, value))
|
|
24
|
-
const path = getPathFromKey(result[0])
|
|
25
|
-
|
|
26
|
-
for (let i = 1; i < path.length; i++) {
|
|
27
|
-
const nodeKey = getKeyFromPath(path.slice(0, i))
|
|
28
|
-
this.expand(nodeKey)
|
|
29
|
-
}
|
|
30
|
-
return true
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Toggle expansion of item
|
|
35
|
-
* @param {*} value
|
|
36
|
-
* @returns
|
|
37
|
-
*/
|
|
38
|
-
toggleExpansion(key) {
|
|
39
|
-
if (!this.lookup.has(key)) return false
|
|
40
|
-
const proxy = this.lookup.get(key)
|
|
41
|
-
proxy.expanded = !proxy.expanded
|
|
42
|
-
return true
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Expand item
|
|
47
|
-
* @param {*} value
|
|
48
|
-
* @returns
|
|
49
|
-
*/
|
|
50
|
-
expand(key) {
|
|
51
|
-
const actualKey = key ?? this.focusedKey
|
|
52
|
-
if (!this.lookup.has(actualKey)) return false
|
|
53
|
-
const proxy = this.lookup.get(actualKey)
|
|
54
|
-
proxy.expanded = true
|
|
55
|
-
|
|
56
|
-
return true
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Collapse item
|
|
61
|
-
* @param {*} value
|
|
62
|
-
* @returns
|
|
63
|
-
*/
|
|
64
|
-
collapse(key) {
|
|
65
|
-
const actualKey = key ?? this.focusedKey
|
|
66
|
-
if (!this.lookup.has(actualKey)) return false
|
|
67
|
-
const proxy = this.lookup.get(actualKey)
|
|
68
|
-
proxy.expanded = false
|
|
69
|
-
return true
|
|
70
|
-
}
|
|
71
|
-
}
|
package/src/proxy.svelte.js
DELETED
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
import { defaultFields, id, toString, getNestedFields } from '@rokkit/core'
|
|
2
|
-
import { isNil, has } from 'ramda'
|
|
3
|
-
|
|
4
|
-
export class Proxy {
|
|
5
|
-
#original = null
|
|
6
|
-
#value = $state(null)
|
|
7
|
-
#fields = defaultFields
|
|
8
|
-
#id = null
|
|
9
|
-
|
|
10
|
-
#children = $derived(this.#processChildren())
|
|
11
|
-
|
|
12
|
-
constructor(value, fields) {
|
|
13
|
-
this.fields = fields
|
|
14
|
-
this.#original = value
|
|
15
|
-
this.#value = typeof value === 'object' ? value : { [this.fields.text]: value }
|
|
16
|
-
this.id = typeof value === 'object' ? (this.get('id') ?? id()) : value
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
#processChildren() {
|
|
20
|
-
if (isNil(this.#value)) return []
|
|
21
|
-
|
|
22
|
-
const children = this.#value[this.fields.children] ?? []
|
|
23
|
-
if (Array.isArray(children)) {
|
|
24
|
-
const fields = getNestedFields(this.fields)
|
|
25
|
-
return children.map((child) => new Proxy(child, fields))
|
|
26
|
-
}
|
|
27
|
-
return []
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
get id() {
|
|
31
|
-
return this.#id
|
|
32
|
-
}
|
|
33
|
-
set id(new_id) {
|
|
34
|
-
this.#id = typeof new_id === 'string' ? new_id : toString(new_id)
|
|
35
|
-
}
|
|
36
|
-
get children() {
|
|
37
|
-
return this.#children
|
|
38
|
-
}
|
|
39
|
-
get fields() {
|
|
40
|
-
return this.#fields
|
|
41
|
-
}
|
|
42
|
-
set fields(value) {
|
|
43
|
-
this.#fields = { ...defaultFields, ...value }
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
get value() {
|
|
47
|
-
return typeof this.#original === 'object' ? this.#value : this.#original
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
set value(value) {
|
|
51
|
-
if (typeof value === 'object') {
|
|
52
|
-
const removedKeys = Object.keys(this.#value).filter(
|
|
53
|
-
(key) => !Object.keys(value).includes(key)
|
|
54
|
-
)
|
|
55
|
-
Object.entries(value).forEach(([k, v]) => {
|
|
56
|
-
this.#value[k] = v
|
|
57
|
-
})
|
|
58
|
-
removedKeys.forEach((key) => {
|
|
59
|
-
delete this.#value[key]
|
|
60
|
-
})
|
|
61
|
-
} else {
|
|
62
|
-
this.#value = { [this.fields.text]: value }
|
|
63
|
-
this.#original = value
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Gets a mapped attribute from the original item
|
|
69
|
-
*
|
|
70
|
-
* @param {string} fieldName - Name of the field to get
|
|
71
|
-
* @param {any} [defaultValue] - Default value to return if not found
|
|
72
|
-
* @returns {any|undefined} - The attribute value or null if not found
|
|
73
|
-
*/
|
|
74
|
-
get(fieldName, defaultValue) {
|
|
75
|
-
return this.has(fieldName) ? this.#value[this.fields[fieldName]] : defaultValue
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Checks if a mapped attribute exists in the original item
|
|
80
|
-
* @param {string} fieldName - Name of the field to check
|
|
81
|
-
* @returns boolean
|
|
82
|
-
*/
|
|
83
|
-
has(fieldName) {
|
|
84
|
-
const mappedField = this.fields[fieldName]
|
|
85
|
-
return !isNil(mappedField) && has(mappedField, this.#value)
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Gets the appropriate snippet for rendering this item:
|
|
90
|
-
* - Uses the 'snippet' field from the current item to find the snippet key
|
|
91
|
-
* - Finds a matching snippet in the provided collection using this key
|
|
92
|
-
* - Falls back to the defaultSnippet if:
|
|
93
|
-
* - No snippet key is configured for this item
|
|
94
|
-
* - The configured snippet key doesn't exist in the snippets collection
|
|
95
|
-
* @param {Object} snippets
|
|
96
|
-
* @param {import('svelte').Snippet|undefined} [defaultSnippet]
|
|
97
|
-
* @returns {import('svelte').Snippet|undefined}
|
|
98
|
-
*/
|
|
99
|
-
getSnippet(snippets, defaultSnippet) {
|
|
100
|
-
const snippetKey = this.get('snippet')
|
|
101
|
-
const snippet = has(snippetKey, snippets) ? snippets[snippetKey] : undefined
|
|
102
|
-
return snippet ?? defaultSnippet
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Identifies if the item has children
|
|
107
|
-
*/
|
|
108
|
-
get hasChildren() {
|
|
109
|
-
return (
|
|
110
|
-
typeof this.#original === 'object' &&
|
|
111
|
-
has(this.fields.children, this.#value) &&
|
|
112
|
-
Array.isArray(this.#value[this.fields.children]) &&
|
|
113
|
-
this.#value[this.fields.children].length > 0
|
|
114
|
-
)
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
get expanded() {
|
|
118
|
-
return this.has('expanded') ? this.#value[this.fields.expanded] : false
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
set expanded(value) {
|
|
122
|
-
if (typeof this.#original === 'object') {
|
|
123
|
-
this.#value[this.fields.expanded] = Boolean(value)
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
}
|