@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.
- package/LICENSE +21 -0
- package/package.json +70 -0
- package/src/compile/index.js +136 -0
- package/src/compile/serialize.js +76 -0
- package/src/compile/skeleton-node.js +162 -0
- package/src/compile/skeleton-tree.js +28 -0
- package/src/compile/types.ts +50 -0
- package/src/compile/utils/resolve-refs.js +69 -0
- package/src/i18n/en.js +10 -0
- package/src/i18n/fr.js +10 -0
- package/src/i18n/index.js +8 -0
- package/src/i18n/types.ts +14 -0
- package/src/index.js +3 -0
- package/src/state/index.js +381 -0
- package/src/state/state-node.js +317 -0
- package/src/state/state-tree.js +59 -0
- package/src/state/types.ts +89 -0
- package/src/state/utils/display.js +132 -0
- package/src/state/utils/immutable.js +11 -0
- package/types/compile/index.d.ts +14 -0
- package/types/compile/index.d.ts.map +1 -0
- package/types/compile/serialize.d.ts +6 -0
- package/types/compile/serialize.d.ts.map +1 -0
- package/types/compile/skeleton-node.d.ts +14 -0
- package/types/compile/skeleton-node.d.ts.map +1 -0
- package/types/compile/skeleton-tree.d.ts +12 -0
- package/types/compile/skeleton-tree.d.ts.map +1 -0
- package/types/compile/types.d.ts +43 -0
- package/types/compile/types.d.ts.map +1 -0
- package/types/compile/utils/resolve-refs.d.ts +9 -0
- package/types/compile/utils/resolve-refs.d.ts.map +1 -0
- package/types/i18n/en.d.ts +3 -0
- package/types/i18n/en.d.ts.map +1 -0
- package/types/i18n/fr.d.ts +3 -0
- package/types/i18n/fr.d.ts.map +1 -0
- package/types/i18n/index.d.ts +3 -0
- package/types/i18n/index.d.ts.map +1 -0
- package/types/i18n/types.d.ts +13 -0
- package/types/i18n/types.d.ts.map +1 -0
- package/types/index.d.ts +4 -0
- package/types/index.d.ts.map +1 -0
- package/types/state/index.d.ts +185 -0
- package/types/state/index.d.ts.map +1 -0
- package/types/state/state-node.d.ts +31 -0
- package/types/state/state-node.d.ts.map +1 -0
- package/types/state/state-tree.d.ts +13 -0
- package/types/state/state-tree.d.ts.map +1 -0
- package/types/state/types.d.ts +119 -0
- package/types/state/types.d.ts.map +1 -0
- package/types/state/utils/display.d.ts +51 -0
- package/types/state/utils/display.d.ts.map +1 -0
- package/types/state/utils/immutable.d.ts +8 -0
- 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
|
+
)
|