@json-layout/core 0.1.0

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.
Files changed (53) hide show
  1. package/LICENSE +21 -0
  2. package/package.json +70 -0
  3. package/src/compile/index.js +136 -0
  4. package/src/compile/serialize.js +76 -0
  5. package/src/compile/skeleton-node.js +162 -0
  6. package/src/compile/skeleton-tree.js +28 -0
  7. package/src/compile/types.ts +50 -0
  8. package/src/compile/utils/resolve-refs.js +69 -0
  9. package/src/i18n/en.js +10 -0
  10. package/src/i18n/fr.js +10 -0
  11. package/src/i18n/index.js +8 -0
  12. package/src/i18n/types.ts +14 -0
  13. package/src/index.js +3 -0
  14. package/src/state/index.js +381 -0
  15. package/src/state/state-node.js +317 -0
  16. package/src/state/state-tree.js +59 -0
  17. package/src/state/types.ts +89 -0
  18. package/src/state/utils/display.js +132 -0
  19. package/src/state/utils/immutable.js +11 -0
  20. package/types/compile/index.d.ts +14 -0
  21. package/types/compile/index.d.ts.map +1 -0
  22. package/types/compile/serialize.d.ts +6 -0
  23. package/types/compile/serialize.d.ts.map +1 -0
  24. package/types/compile/skeleton-node.d.ts +14 -0
  25. package/types/compile/skeleton-node.d.ts.map +1 -0
  26. package/types/compile/skeleton-tree.d.ts +12 -0
  27. package/types/compile/skeleton-tree.d.ts.map +1 -0
  28. package/types/compile/types.d.ts +43 -0
  29. package/types/compile/types.d.ts.map +1 -0
  30. package/types/compile/utils/resolve-refs.d.ts +9 -0
  31. package/types/compile/utils/resolve-refs.d.ts.map +1 -0
  32. package/types/i18n/en.d.ts +3 -0
  33. package/types/i18n/en.d.ts.map +1 -0
  34. package/types/i18n/fr.d.ts +3 -0
  35. package/types/i18n/fr.d.ts.map +1 -0
  36. package/types/i18n/index.d.ts +3 -0
  37. package/types/i18n/index.d.ts.map +1 -0
  38. package/types/i18n/types.d.ts +13 -0
  39. package/types/i18n/types.d.ts.map +1 -0
  40. package/types/index.d.ts +4 -0
  41. package/types/index.d.ts.map +1 -0
  42. package/types/state/index.d.ts +185 -0
  43. package/types/state/index.d.ts.map +1 -0
  44. package/types/state/state-node.d.ts +31 -0
  45. package/types/state/state-node.d.ts.map +1 -0
  46. package/types/state/state-tree.d.ts +13 -0
  47. package/types/state/state-tree.d.ts.map +1 -0
  48. package/types/state/types.d.ts +119 -0
  49. package/types/state/types.d.ts.map +1 -0
  50. package/types/state/utils/display.d.ts +51 -0
  51. package/types/state/utils/display.d.ts.map +1 -0
  52. package/types/state/utils/immutable.d.ts +8 -0
  53. package/types/state/utils/immutable.d.ts.map +1 -0
@@ -0,0 +1,381 @@
1
+ // eslint-disable-next-line import/no-named-default
2
+ import mittModule from 'mitt'
3
+ import debug from 'debug'
4
+ import { evalExpression, producePatchedData } from './state-node.js'
5
+ import { createStateTree } from './state-tree.js'
6
+ import { Display } from './utils/display.js'
7
+ import { isGetItemsExpression, isGetItemsFetch } from '@json-layout/vocabulary'
8
+
9
+ export { Display } from './utils/display.js'
10
+
11
+ /**
12
+ * @typedef {import('./types.js').StateNode} StateNode
13
+ * @typedef {import('./types.js').StateTree} StateTree
14
+ * @typedef {import('./types.js').StatefulLayoutOptions} StatefulLayoutOptions
15
+ * @typedef {import('./types.js').StatefulLayoutEvents} StatefulLayoutEvents
16
+ * @typedef {import('./types.js').CreateStateTreeContext} CreateStateTreeContext
17
+ * @typedef {import('./types.js').TextFieldNode} TextFieldNode
18
+ * @typedef {import('./types.js').TextareaNode} TextareaNode
19
+ * @typedef {import('./types.js').NumberFieldNode} NumberFieldNode
20
+ * @typedef {import('./types.js').SliderNode} SliderNode
21
+ * @typedef {import('./types.js').SectionNode} SectionNode
22
+ * @typedef {import('./types.js').SelectNode} SelectNode
23
+ * @typedef {import('./types.js').CheckboxNode} CheckboxNode
24
+ * @typedef {import('./types.js').SwitchNode} SwitchNode
25
+ * @typedef {import('./types.js').ColorPickerNode} ColorPickerNode
26
+ * @typedef {import('./types.js').DatePickerNode} DatePickerNode
27
+ * @typedef {import('./types.js').DateTimePickerNode} DateTimePickerNode
28
+ * @typedef {import('./types.js').TimePickerNode} TimePickerNode
29
+ * @typedef {import('./types.js').ExpansionPanelsNode} ExpansionPanelsNode
30
+ * @typedef {import('./types.js').TabsNode} TabsNode
31
+ * @typedef {import('./types.js').VerticalTabsNode} VerticalTabsNode
32
+ * @typedef {import('./types.js').OneOfSelectNode} OneOfSelectNode
33
+ * @typedef {import('./types.js').ListNode} ListNode
34
+ */
35
+
36
+ /** @type {(node: StateNode | undefined) => node is SectionNode} */
37
+ export const isSection = (node) => !!node && node.layout.comp === 'section'
38
+
39
+ /** @type {(node: StateNode | undefined) => node is SelectNode} */
40
+ export const isSelect = (node) => !!node && node.layout.comp === 'select'
41
+
42
+ const logDataBinding = debug('jl:data-binding')
43
+
44
+ // ugly fix of modules whose default export were wrongly declared
45
+ // @ts-ignore
46
+ const mitt = /** @type {typeof mittModule.default} */ (mittModule)
47
+
48
+ /**
49
+ * @param {Partial<StatefulLayoutOptions>} partialOptions
50
+ * @param {import('../index.js').CompiledLayout} compiledLayout
51
+ * @returns {StatefulLayoutOptions}
52
+ */
53
+ function fillOptions (partialOptions, compiledLayout) {
54
+ const messages = { ...compiledLayout.messages }
55
+ if (partialOptions.messages) Object.assign(messages, partialOptions.messages)
56
+ return {
57
+ context: {},
58
+ width: 1000,
59
+ readOnly: false,
60
+ summary: false,
61
+ titleDepth: 2,
62
+ validateOn: 'input',
63
+ initialValidation: 'withData',
64
+ ...partialOptions,
65
+ messages
66
+ }
67
+ }
68
+
69
+ export class StatefulLayout {
70
+ /**
71
+ * @readonly
72
+ * @type {import('mitt').Emitter<StatefulLayoutEvents>}
73
+ */
74
+ events
75
+
76
+ /**
77
+ * @private
78
+ * @readonly
79
+ * @type {import('../index.js').CompiledLayout}
80
+ */
81
+ _compiledLayout
82
+ get compiledLayout () { return this._compiledLayout }
83
+
84
+ /**
85
+ * @private
86
+ * @type {StateTree}
87
+ */
88
+ // @ts-ignore
89
+ _stateTree
90
+ get stateTree () { return this._stateTree }
91
+
92
+ /**
93
+ * @readonly
94
+ * @type {import('../index.js').SkeletonTree}
95
+ */
96
+ skeletonTree
97
+
98
+ /**
99
+ * @private
100
+ * @type {Display}
101
+ */
102
+ // @ts-ignore
103
+ _display
104
+ get display () { return this._display }
105
+
106
+ /**
107
+ * @private
108
+ * @type {import('./types.js').ValidationState}
109
+ */
110
+ // @ts-ignore
111
+ _validationState
112
+ /**
113
+ * @returns {import('./types.js').ValidationState}
114
+ */
115
+ get validationState () {
116
+ return this._validationState
117
+ }
118
+
119
+ /**
120
+ * @private
121
+ * @param {Partial<import('./types.js').ValidationState>} validationState
122
+ */
123
+ set validationState (validationState) {
124
+ this._validationState = {
125
+ initialized: validationState.initialized ?? this._validationState.initialized ?? false,
126
+ validatedForm: validationState.validatedForm ?? this._validationState.validatedForm ?? false,
127
+ validatedChildren: validationState.validatedChildren ?? this._validationState.validatedChildren ?? []
128
+ }
129
+ this.updateState()
130
+ }
131
+
132
+ /**
133
+ * @private
134
+ * @type {StatefulLayoutOptions}
135
+ */
136
+ // @ts-ignore
137
+ _options
138
+ /**
139
+ * @returns {StatefulLayoutOptions}
140
+ */
141
+ get options () { return this._options }
142
+ /**
143
+ * @param {Partial<StatefulLayoutOptions>} options
144
+ */
145
+ set options (options) {
146
+ this.prepareOptions(options)
147
+ this.updateState()
148
+ }
149
+
150
+ /**
151
+ * @private
152
+ * @type {unknown}
153
+ */
154
+ _data
155
+ get data () { return this._data }
156
+ set data (data) {
157
+ logDataBinding('apply main data setter', data)
158
+ this._data = data
159
+ this.updateState()
160
+ }
161
+
162
+ /**
163
+ * @private
164
+ * @type {CreateStateTreeContext}
165
+ */
166
+ // @ts-ignore
167
+ _lastCreateStateTreeContext
168
+
169
+ /**
170
+ * @param {import("../index.js").CompiledLayout} compiledLayout
171
+ * @param {import("../index.js").SkeletonTree} skeletonTree
172
+ * @param {Partial<StatefulLayoutOptions>} options
173
+ * @param {unknown} data
174
+ */
175
+ constructor (compiledLayout, skeletonTree, options, data = {}) {
176
+ this._compiledLayout = compiledLayout
177
+ this.skeletonTree = skeletonTree
178
+ /** @type {import('mitt').Emitter<StatefulLayoutEvents>} */
179
+ this.events = mitt()
180
+ this.prepareOptions(options)
181
+ this._data = data
182
+ this.initValidationState()
183
+ this.activeItems = {}
184
+ this.updateState()
185
+ }
186
+
187
+ /**
188
+ * @private
189
+ * @param {Partial<StatefulLayoutOptions>} options
190
+ */
191
+ prepareOptions (options) {
192
+ this._options = fillOptions(options, this.compiledLayout)
193
+ this._display = this._display && this._display.width === this._options.width ? this._display : new Display(this._options.width)
194
+ }
195
+
196
+ /**
197
+ * @private
198
+ */
199
+ initValidationState () {
200
+ const initialValidation = this.options.initialValidation === 'always'
201
+ this._validationState = {
202
+ initialized: initialValidation,
203
+ validatedForm: initialValidation,
204
+ validatedChildren: []
205
+ }
206
+ }
207
+
208
+ /**
209
+ * @private
210
+ */
211
+ updateState () {
212
+ this.createStateTree()
213
+ if (this._data !== this._stateTree.root.data) {
214
+ logDataBinding('hydrating state tree changed the data, do it again', this._data, this._stateTree.root.data)
215
+ // this is necessary because a first hydration can add default values and change validity, etc
216
+ this._data = this._stateTree.root.data
217
+ this.createStateTree()
218
+ }
219
+ logDataBinding('emit update event', this._data, this._stateTree)
220
+ this.events.emit('update', this)
221
+ }
222
+
223
+ /**
224
+ * @private
225
+ */
226
+ createStateTree () {
227
+ /** @type {CreateStateTreeContext} */
228
+ const createStateTreeContext = { nodes: [], activeItems: this.activeItems }
229
+ this._stateTree = createStateTree(
230
+ createStateTreeContext,
231
+ this._options,
232
+ this._compiledLayout,
233
+ this.skeletonTree,
234
+ this._display,
235
+ this._data,
236
+ this._validationState,
237
+ this._stateTree
238
+ )
239
+ this._lastCreateStateTreeContext = createStateTreeContext
240
+ if (!this.validationState.initialized) {
241
+ this.validationState = { initialized: true, validatedChildren: createStateTreeContext.nodes.filter(n => n.validated).map(n => n.fullKey) }
242
+ }
243
+ }
244
+
245
+ validate () {
246
+ this.validationState = { validatedForm: true }
247
+ }
248
+
249
+ resetValidation () {
250
+ this.initValidationState()
251
+ this.updateState()
252
+ }
253
+
254
+ /**
255
+ * @returns {boolean}
256
+ */
257
+ get valid () {
258
+ return this.stateTree.valid
259
+ }
260
+
261
+ /**
262
+ * @returns {boolean}
263
+ */
264
+ get hasHiddenError () {
265
+ return this._lastCreateStateTreeContext.nodes.findIndex(node => node.error && !node.validated) !== -1
266
+ }
267
+
268
+ /**
269
+ * @param {StateNode} node
270
+ * @param {unknown} data
271
+ * @param {number} [activateKey]
272
+ */
273
+ input (node, data, activateKey) {
274
+ logDataBinding('received input event from node', node, data)
275
+ if (node.options.validateOn === 'input' && !this.validationState.validatedChildren.includes(node.fullKey)) {
276
+ this.validationState = { validatedChildren: this.validationState.validatedChildren.concat([node.fullKey]) }
277
+ }
278
+ if (activateKey !== undefined) {
279
+ this.activeItems[node.fullKey] = activateKey
280
+ }
281
+ if (node.parentFullKey === null) {
282
+ this.data = data
283
+ this.events.emit('input', this.data)
284
+ return
285
+ }
286
+ const parentNode = this._lastCreateStateTreeContext.nodes.find(p => p.fullKey === node.parentFullKey)
287
+ if (!parentNode) throw new Error(`parent with key "${node.parentFullKey}" not found`)
288
+ const newParentValue = producePatchedData(parentNode.data ?? {}, node, data)
289
+ this.input(parentNode, newParentValue)
290
+ }
291
+
292
+ /**
293
+ * @param {StateNode} node
294
+ */
295
+ blur (node) {
296
+ logDataBinding('received blur event from node', node)
297
+ if (
298
+ (node.options.validateOn === 'input' || node.options.validateOn === 'blur') &&
299
+ !this.validationState.validatedChildren.includes(node.fullKey)
300
+ ) {
301
+ this.validationState = { validatedChildren: this.validationState.validatedChildren.concat([node.fullKey]) }
302
+ }
303
+ }
304
+
305
+ /**
306
+ * @param {StateNode} node
307
+ * @returns {Promise<import('@json-layout/vocabulary').SelectItems>}
308
+ */
309
+ async getSelectItems (node) {
310
+ if (!isSelect(node)) throw new Error('node is not a select component')
311
+ if (node.layout.items) return node.layout.items
312
+
313
+ /** @type {(expression: import('@json-layout/vocabulary').Expression, data: any) => any} */
314
+ const evalSelectExpression = (expression, data) => {
315
+ return evalExpression(this.compiledLayout.expressions, expression, data, node.options, new Display(node.options.width))
316
+ }
317
+
318
+ let rawItems
319
+ if (node.layout.getItems && isGetItemsExpression(node.layout.getItems)) {
320
+ rawItems = evalSelectExpression(node.layout.getItems, null)
321
+ if (!Array.isArray(rawItems)) throw new Error('getItems expression didn\'t return an array')
322
+ }
323
+ if (node.layout.getItems && isGetItemsFetch(node.layout.getItems)) {
324
+ const url = evalSelectExpression(node.layout.getItems.url, null)
325
+ rawItems = await (await fetch(url)).json()
326
+ }
327
+
328
+ if (rawItems) {
329
+ if (node.layout.getItems?.itemsResults) {
330
+ rawItems = evalSelectExpression(node.layout.getItems.itemsResults, rawItems)
331
+ }
332
+ return rawItems.map((/** @type {any} */ rawItem) => {
333
+ if (typeof rawItem === 'object') {
334
+ /** @type {Partial<import('@json-layout/vocabulary').SelectItem>} */
335
+ const item = {}
336
+ item.value = node.layout.getItems?.itemValue ? evalSelectExpression(node.layout.getItems.itemValue, rawItem) : (node.layout.getItems?.returnObjects ? rawItem : rawItem.value)
337
+ item.key = node.layout.getItems?.itemKey ? evalSelectExpression(node.layout.getItems.itemKey, rawItem) : rawItem.key
338
+ item.title = node.layout.getItems?.itemTitle ? evalSelectExpression(node.layout.getItems.itemTitle, rawItem) : rawItem.title
339
+ item.value = item.value ?? item.key
340
+ item.key = item.key ?? item.value + ''
341
+ item.title = item.title ?? item.key
342
+ return item
343
+ } else {
344
+ /** @type {Partial<import('@json-layout/vocabulary').SelectItem>} */
345
+ const item = {}
346
+ item.value = node.layout.getItems?.itemValue ? evalSelectExpression(node.layout.getItems.itemValue, rawItem) : rawItem
347
+ item.key = node.layout.getItems?.itemKey ? evalSelectExpression(node.layout.getItems.itemKey, rawItem) : item.value
348
+ item.title = node.layout.getItems?.itemTitle ? evalSelectExpression(node.layout.getItems.itemTitle, rawItem) : item.value
349
+ return item
350
+ }
351
+ })
352
+ }
353
+ throw new Error('node is missing items or getItems parameters')
354
+ }
355
+
356
+ /**
357
+ * @type {Record<string, number>}
358
+ */
359
+ activeItems
360
+
361
+ /**
362
+ * @param {StateNode} node
363
+ * @param {number} key
364
+ */
365
+ activateItem (node, key) {
366
+ this.activeItems[node.fullKey] = key
367
+ if (node.key === '$oneOf') {
368
+ this.input(node, node.skeleton.childrenTrees?.[key].root.defaultData)
369
+ } else {
370
+ this.updateState()
371
+ }
372
+ }
373
+
374
+ /**
375
+ * @param {StateNode} node
376
+ */
377
+ deactivateItem (node) {
378
+ delete this.activeItems[node.fullKey]
379
+ this.updateState()
380
+ }
381
+ }
@@ -0,0 +1,317 @@
1
+ import { isSwitchStruct, childIsCompObject, isCompositeLayout } from '@json-layout/vocabulary'
2
+ import { produce } from 'immer'
3
+ import { getChildDisplay } from './utils/display.js'
4
+ import { shallowCompareArrays } from './utils/immutable.js'
5
+
6
+ /**
7
+ * @param {unknown} data
8
+ * @returns {boolean}
9
+ */
10
+ const isDataEmpty = (data) => {
11
+ if (data === '' || data === undefined) return true
12
+ if (Array.isArray(data) && !data.length) return true
13
+ if (typeof data === 'object' && !Array.isArray(data) && !!data && Object.values(data).findIndex(prop => !isDataEmpty(prop)) === -1) return true
14
+ return false
15
+ }
16
+
17
+ // use Immer for efficient updating with immutability and no-op detection
18
+ /** @type {(draft: import('./types.js').StateNode, key: string | number, fullKey: string, parentFullKey: string | null, dataPath: string, parentDataPath: string | null, skeleton: import('../index.js').SkeletonNode, layout: import('@json-layout/vocabulary').CompObject, cols: number, data: unknown, error: string | undefined, validated: boolean, options: import('./types.js').StatefulLayoutOptions, children: import('../index.js').StateNode[] | undefined) => import('../index.js').StateNode} */
19
+ const produceStateNode = produce((draft, key, fullKey, parentFullKey, dataPath, parentDataPath, skeleton, layout, cols, data, error, validated, options, children) => {
20
+ data = children && layout.comp !== 'list' ? produceStateNodeData(/** @type {Record<string, unknown>} */(data ?? {}), dataPath, children) : data
21
+ // empty data is removed and replaced by the default data
22
+ if (isDataEmpty(data) && skeleton.defaultData === undefined) data = undefined
23
+ data = skeleton.const ?? data ?? skeleton.defaultData
24
+
25
+ draft.messages = layout.messages ? produceStateNodeMessages(draft.messages || {}, layout.messages, options) : options.messages
26
+
27
+ draft.key = key
28
+ draft.fullKey = fullKey
29
+ draft.parentFullKey = parentFullKey
30
+ draft.dataPath = dataPath
31
+ draft.parentDataPath = parentDataPath
32
+ draft.skeleton = skeleton
33
+ draft.layout = layout
34
+ draft.options = options
35
+ draft.cols = cols
36
+ draft.data = data
37
+ draft.error = error
38
+ draft.childError = children && (children.findIndex(c => c.error || c.childError) !== -1)
39
+ draft.validated = validated
40
+ draft.children = children
41
+ })
42
+
43
+ /** @type {(draft: import('../i18n/types.js').LocaleMessages, layoutMessages: Partial<import('../i18n/types.js').LocaleMessages>, options: import('./types.js').StatefulLayoutOptions) => import('../i18n/types.js').LocaleMessages} */
44
+ const produceStateNodeMessages = produce((draft, layoutMessages, options) => {
45
+ Object.assign(draft, options.messages, layoutMessages)
46
+ })
47
+
48
+ /** @type {(draft: Record<string, unknown>, parentDataPath: string, children: import('../index.js').StateNode[]) => Record<string, unknown>} */
49
+ const produceStateNodeData = produce((draft, parentDataPath, children) => {
50
+ for (const child of children) {
51
+ if (parentDataPath === child.dataPath) {
52
+ if (child.data === undefined) continue
53
+ Object.assign(draft, child.data)
54
+ } else {
55
+ // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
56
+ if (child.data === undefined) delete draft[child.key]
57
+ else draft[child.key] = child.data
58
+ }
59
+ }
60
+ })
61
+
62
+ /** @type {(draft: import('./types.js').StatefulLayoutOptions, parentNodeOptions: import('./types.js').StatefulLayoutOptions, nodeOptions: import('@json-layout/vocabulary').StateNodeOptions | undefined, width: number) => import('./types.js').StatefulLayoutOptions} */
63
+ const produceNodeOptions = produce((draft, parentNodeOptions, nodeOptions = {}, width) => {
64
+ for (const key in parentNodeOptions) {
65
+ draft[key] = parentNodeOptions[key]
66
+ }
67
+ for (const key in nodeOptions) {
68
+ draft[key] = nodeOptions[key]
69
+ }
70
+ for (const key in draft) {
71
+ if (!(key in parentNodeOptions) && !(key in nodeOptions)) {
72
+ // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
73
+ delete draft[key]
74
+ }
75
+ }
76
+ draft.width = width
77
+ })
78
+
79
+ /** @type {(draft: import('./types.js').StatefulLayoutOptions) => import('./types.js').StatefulLayoutOptions} */
80
+ const produceReadonlyArrayItemOptions = produce((draft) => {
81
+ draft.readOnly = true
82
+ draft.summary = true
83
+ })
84
+
85
+ /** @type {(draft: import('./types.js').StatefulLayoutOptions, section: import('@json-layout/vocabulary').CompositeCompObject) => import('./types.js').StatefulLayoutOptions} */
86
+ const produceCompositeChildrenOptions = produce((draft, section) => {
87
+ // eslint-disable-next-line @typescript-eslint/restrict-plus-operands
88
+ if (section.title && draft.titleDepth < 6) draft.titleDepth += 1
89
+ })
90
+
91
+ /**
92
+ * @param {import('ajv').ErrorObject} error
93
+ * @param {import('../index.js').SkeletonNode} skeleton
94
+ * @param {string} dataPath
95
+ * @param {string | null} parentDataPath
96
+ * @returns {boolean}
97
+ */
98
+ const matchError = (error, skeleton, dataPath, parentDataPath) => {
99
+ const originalError = error.params?.errors?.[0] ?? error
100
+ if (parentDataPath === originalError.instancePath && originalError.params?.missingProperty === skeleton.key) return true
101
+ if (originalError.instancePath === dataPath && originalError.schemaPath === skeleton.pointer) return true
102
+ return false
103
+ }
104
+
105
+ /**
106
+ * @param {import('ajv').ErrorObject} error
107
+ * @param {import('../index.js').SkeletonNode} skeleton
108
+ * @param {string} dataPath
109
+ * @returns {boolean}
110
+ */
111
+ const matchChildError = (error, skeleton, dataPath) => {
112
+ if (error.instancePath.startsWith(dataPath) && !(typeof skeleton.key === 'string' && skeleton.key.startsWith('$allOf'))) return true
113
+ return false
114
+ }
115
+
116
+ /**
117
+ * @param {import('../index.js').CompiledExpression[]} expressions
118
+ * @param {import('@json-layout/vocabulary').Expression} expression
119
+ * @param {any} data
120
+ * @param {import('./types.js').StatefulLayoutOptions} options
121
+ * @param {import('./utils/display.js').Display} display
122
+ * @returns {any}
123
+ */
124
+ export function evalExpression (expressions, expression, data, options, display) {
125
+ if (expression.ref === undefined) throw new Error('expression was not compiled : ' + JSON.stringify(expression))
126
+ const compiledExpression = expressions[expression.ref]
127
+ // console.log(expression.expr, context, mode, display)
128
+ return compiledExpression(data, options, display)
129
+ }
130
+
131
+ /**
132
+ * @param {import('@json-layout/vocabulary').NormalizedLayout} normalizedLayout
133
+ * @param {import('./types.js').StatefulLayoutOptions} options
134
+ * @param {import('../index.js').CompiledLayout} compiledLayout
135
+ * @param {import('./utils/display.js').Display} display
136
+ * @param {unknown} data
137
+ * @returns {import('@json-layout/vocabulary').CompObject}
138
+ */
139
+ const getCompObject = (normalizedLayout, options, compiledLayout, display, data) => {
140
+ if (isSwitchStruct(normalizedLayout)) {
141
+ for (const compObject of normalizedLayout.switch) {
142
+ if (!compObject.if || !!evalExpression(compiledLayout.expressions, compObject.if, data, options, display)) {
143
+ return compObject
144
+ }
145
+ }
146
+ } else {
147
+ return normalizedLayout
148
+ }
149
+ throw new Error('no layout matched for node')
150
+ }
151
+
152
+ /**
153
+ *
154
+ * @param {import('./types.js').CreateStateTreeContext} context
155
+ * @param {import('./types.js').StatefulLayoutOptions} parentOptions
156
+ * @param {import('../index.js').CompiledLayout} compiledLayout
157
+ * @param {string | number} key
158
+ * @param {string} fullKey
159
+ * @param {string | null} parentFullKey
160
+ * @param {string} dataPath
161
+ * @param {string | null} parentDataPath
162
+ * @param {import('../index.js').SkeletonNode} skeleton
163
+ * @param {import('@json-layout/vocabulary').Child | null} childDefinition
164
+ * @param {import('./utils/display.js').Display} parentDisplay
165
+ * @param {unknown} data
166
+ * @param {import('./types.js').ValidationState} validationState
167
+ * @param {import('./types.js').StateNode} [reusedNode]
168
+ * @returns {import('./types.js').StateNode}
169
+ */
170
+ export function createStateNode (
171
+ context,
172
+ parentOptions,
173
+ compiledLayout,
174
+ key,
175
+ fullKey,
176
+ parentFullKey,
177
+ dataPath,
178
+ parentDataPath,
179
+ skeleton,
180
+ childDefinition,
181
+ parentDisplay,
182
+ data,
183
+ validationState,
184
+ reusedNode
185
+ ) {
186
+ const normalizedLayout = childDefinition && childIsCompObject(childDefinition)
187
+ ? childDefinition
188
+ : compiledLayout.normalizedLayouts[skeleton.pointer]
189
+ const layout = getCompObject(normalizedLayout, parentOptions, compiledLayout, parentDisplay, data)
190
+ const [display, cols] = getChildDisplay(parentDisplay, childDefinition?.cols ?? layout.cols)
191
+
192
+ const options = produceNodeOptions(
193
+ reusedNode?.options ?? /** @type {import('./types.js').StatefulLayoutOptions} */({}),
194
+ parentOptions,
195
+ layout.options, display.width
196
+ )
197
+
198
+ /** @type {import('./types.js').StateNode[] | undefined} */
199
+ let children
200
+ if (isCompositeLayout(layout)) {
201
+ // TODO: make this type casting safe using prior validation
202
+ const objectData = /** @type {Record<string, unknown>} */(data ?? {})
203
+ const childrenOptions = produceCompositeChildrenOptions(options, layout)
204
+ children = layout.children.map((child, i) => {
205
+ const childSkeleton = skeleton.children?.find(c => c.key === child.key) ?? skeleton
206
+ const isSameData = typeof child.key === 'string' && child.key.startsWith('$')
207
+ return createStateNode(
208
+ context,
209
+ childrenOptions,
210
+ compiledLayout,
211
+ child.key,
212
+ `${fullKey}/${child.key}`,
213
+ fullKey,
214
+ isSameData ? dataPath : `${dataPath}/${child.key}`,
215
+ dataPath,
216
+ childSkeleton,
217
+ child,
218
+ display,
219
+ isSameData ? objectData : objectData[child.key],
220
+ validationState,
221
+ reusedNode?.children?.[i]
222
+ )
223
+ })
224
+ }
225
+
226
+ if (key === '$oneOf' && skeleton.childrenTrees) {
227
+ // find the oneOf child that was either previously selected, if none were selected select the child that is valid with current data
228
+ /** @type {number} */
229
+ const activeChildTreeIndex = fullKey in context.activeItems ? context.activeItems[fullKey] : skeleton.childrenTrees?.findIndex((childTree) => compiledLayout.validates[childTree.root.pointer](data))
230
+ if (activeChildTreeIndex !== -1) {
231
+ context.activeItems[fullKey] = activeChildTreeIndex
232
+ const activeChildTree = skeleton.childrenTrees[activeChildTreeIndex]
233
+ children = [
234
+ createStateNode(
235
+ context,
236
+ options,
237
+ compiledLayout,
238
+ activeChildTreeIndex,
239
+ `${fullKey}/${activeChildTreeIndex}`,
240
+ fullKey,
241
+ dataPath,
242
+ dataPath,
243
+ activeChildTree.root,
244
+ null,
245
+ display,
246
+ data,
247
+ validationState,
248
+ reusedNode?.children?.[0]
249
+ )
250
+ ]
251
+ }
252
+ }
253
+
254
+ if (layout.comp === 'list') {
255
+ const arrayData = /** @type {unknown[]} */(data ?? [])
256
+ const childSkeleton = /** @type {import('../index.js').SkeletonNode} */(skeleton?.childrenTrees?.[0]?.root)
257
+ const listItemOptions = layout.listEditMode === 'inline' ? options : produceReadonlyArrayItemOptions(options)
258
+ children = arrayData.map((itemData, i) => {
259
+ const childFullKey = `${fullKey}/${i}`
260
+ return createStateNode(
261
+ context,
262
+ (layout.listEditMode === 'inline-single' && context.activeItems[fullKey] === i) ? options : listItemOptions,
263
+ compiledLayout,
264
+ i,
265
+ childFullKey,
266
+ fullKey,
267
+ `${dataPath}/${i}`,
268
+ dataPath,
269
+ childSkeleton,
270
+ null,
271
+ display,
272
+ itemData,
273
+ validationState,
274
+ reusedNode?.children?.[0]
275
+ )
276
+ })
277
+ }
278
+
279
+ const error = context.errors?.find(error => matchError(error, skeleton, dataPath, parentDataPath)) ?? context.errors?.find(error => matchChildError(error, skeleton, dataPath))
280
+ // capture errors so that they are not repeated in parent nodes
281
+ if (layout.comp !== 'none') {
282
+ if (error) context.errors = context.errors?.filter(error => !matchError(error, skeleton, dataPath, parentDataPath) && !matchChildError(error, skeleton, dataPath))
283
+ }
284
+ const validated = validationState.validatedForm ||
285
+ validationState.validatedChildren.includes(fullKey) ||
286
+ (validationState.initialized === false && options.initialValidation === 'always') ||
287
+ (validationState.initialized === false && options.initialValidation === 'withData' && !isDataEmpty(data))
288
+
289
+ const node = produceStateNode(
290
+ reusedNode ?? /** @type {import('./types.js').StateNode} */({}),
291
+ key,
292
+ fullKey,
293
+ parentFullKey,
294
+ dataPath,
295
+ parentDataPath,
296
+ skeleton,
297
+ layout,
298
+ cols,
299
+ data,
300
+ error?.message,
301
+ validated,
302
+ options,
303
+ children && shallowCompareArrays(reusedNode?.children, children)
304
+ )
305
+ context.nodes.push(node)
306
+ return node
307
+ }
308
+
309
+ /** @type {(draft: any, node: import('./types.js').StateNode, data: unknown) => any} */
310
+ export const producePatchedData = produce((draft, node, data) => {
311
+ if (node.dataPath === node.parentDataPath) {
312
+ Object.assign(draft, data)
313
+ } else {
314
+ draft[node.key] = data
315
+ }
316
+ }
317
+ )