@ditojs/admin 2.100.0 → 3.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 (81) hide show
  1. package/dist/dito-admin.css +1 -1
  2. package/dist/dito-admin.es.js +3555 -2719
  3. package/dist/dito-admin.umd.js +5 -6
  4. package/package.json +84 -46
  5. package/src/DitoAdmin.js +1 -65
  6. package/src/DitoComponent.js +1 -1
  7. package/src/DitoContext.js +51 -0
  8. package/src/DitoTypeComponent.js +23 -1
  9. package/src/components/DitoAffixes.vue +6 -3
  10. package/src/components/DitoButtons.vue +14 -1
  11. package/src/components/DitoContainer.vue +19 -10
  12. package/src/components/DitoCreateButton.vue +11 -2
  13. package/src/components/DitoDialog.vue +21 -2
  14. package/src/components/DitoForm.vue +152 -84
  15. package/src/components/DitoLabel.vue +3 -2
  16. package/src/components/DitoPane.vue +55 -11
  17. package/src/components/DitoPanel.vue +34 -5
  18. package/src/components/DitoPanels.vue +2 -1
  19. package/src/components/DitoRoot.vue +6 -6
  20. package/src/components/DitoRouterView.vue +27 -0
  21. package/src/components/DitoSchema.vue +278 -145
  22. package/src/components/DitoSchemaInlined.vue +1 -1
  23. package/src/components/DitoTabs.vue +16 -18
  24. package/src/components/DitoTrail.vue +5 -4
  25. package/src/components/DitoTreeItem.vue +17 -6
  26. package/src/components/DitoView.vue +21 -3
  27. package/src/components/index.js +1 -0
  28. package/src/mixins/DitoMixin.js +53 -7
  29. package/src/mixins/ItemMixin.js +3 -6
  30. package/src/mixins/NumberMixin.js +47 -31
  31. package/src/mixins/OptionsMixin.js +132 -71
  32. package/src/mixins/ResourceMixin.js +5 -2
  33. package/src/mixins/RouteMixin.js +39 -1
  34. package/src/mixins/SortableMixin.js +0 -13
  35. package/src/mixins/SourceMixin.js +155 -96
  36. package/src/mixins/TextMixin.js +1 -1
  37. package/src/mixins/TypeMixin.js +37 -22
  38. package/src/mixins/ValidationMixin.js +9 -27
  39. package/src/mixins/ValidatorMixin.js +5 -7
  40. package/src/mixins/ValueMixin.js +5 -2
  41. package/src/types/DitoTypeButton.vue +1 -1
  42. package/src/types/DitoTypeCheckbox.vue +1 -1
  43. package/src/types/DitoTypeCheckboxes.vue +1 -1
  44. package/src/types/DitoTypeCode.vue +8 -1
  45. package/src/types/DitoTypeColor.vue +20 -34
  46. package/src/types/DitoTypeComponent.vue +1 -1
  47. package/src/types/DitoTypeComputed.vue +4 -27
  48. package/src/types/DitoTypeDate.vue +16 -5
  49. package/src/types/DitoTypeLabel.vue +1 -4
  50. package/src/types/DitoTypeList.vue +9 -5
  51. package/src/types/DitoTypeMarkup.vue +25 -13
  52. package/src/types/DitoTypeMultiselect.vue +37 -6
  53. package/src/types/DitoTypeNumber.vue +1 -1
  54. package/src/types/DitoTypeObject.vue +10 -7
  55. package/src/types/DitoTypePanel.vue +20 -6
  56. package/src/types/DitoTypeProgress.vue +1 -1
  57. package/src/types/DitoTypeRadio.vue +1 -1
  58. package/src/types/DitoTypeSection.vue +32 -4
  59. package/src/types/DitoTypeSelect.vue +1 -1
  60. package/src/types/DitoTypeSlider.vue +1 -1
  61. package/src/types/DitoTypeSwitch.vue +1 -1
  62. package/src/types/DitoTypeText.vue +13 -15
  63. package/src/types/DitoTypeTextarea.vue +1 -1
  64. package/src/types/DitoTypeTreeList.vue +5 -7
  65. package/src/types/DitoTypeUpload.vue +1 -2
  66. package/src/utils/DataSchemaResolver.js +154 -0
  67. package/src/utils/FormModel.js +615 -0
  68. package/src/utils/data.js +20 -0
  69. package/src/utils/filter.js +3 -2
  70. package/src/utils/options.js +3 -0
  71. package/src/utils/request.js +70 -0
  72. package/src/utils/schema/data.js +659 -0
  73. package/src/utils/schema/lookup.js +250 -0
  74. package/src/utils/schema/setup.js +354 -0
  75. package/src/utils/schema/structure.js +172 -0
  76. package/src/utils/schema/types.js +34 -0
  77. package/src/utils/schema/validation.js +142 -0
  78. package/src/utils/units.js +70 -3
  79. package/types/index.d.ts +174 -59
  80. package/src/mixins/DataMixin.js +0 -131
  81. package/src/utils/schema.js +0 -1121
@@ -0,0 +1,659 @@
1
+ import DitoContext from '../../DitoContext.js'
2
+ import { getUid } from '../uid.js'
3
+ import { SchemaGraph } from '../SchemaGraph.js'
4
+ import { appendDataPath, getRelativeDataPath } from '../data.js'
5
+ import { isMatchingType, convertType } from '../type.js'
6
+ import {
7
+ isObject,
8
+ isArray,
9
+ isFunction,
10
+ asArray,
11
+ clone,
12
+ getValueAtDataPath
13
+ } from '@ditojs/utils'
14
+ import { getTypeOptions, getSourceType } from './types.js'
15
+ import {
16
+ someNestedSchemaComponent,
17
+ hasNestedSchemaComponents,
18
+ isPanelWithOwnData,
19
+ isNested
20
+ } from './structure.js'
21
+ import {
22
+ hasFormSchema,
23
+ hasMultipleFormSchemas,
24
+ getFormSchemas,
25
+ getItemFormSchemaFromForms
26
+ } from './lookup.js'
27
+
28
+ export function getSchemaValue(
29
+ keyOrDataPath,
30
+ { type, schema, callback = true, default: def, context } = {}
31
+ ) {
32
+ const types = type && asArray(type)
33
+ // For performance reasons, data-paths in `keyOrDataPath` can only be
34
+ // provided in in array format here:
35
+ let value = schema
36
+ ? isArray(keyOrDataPath)
37
+ ? getValueAtDataPath(schema, keyOrDataPath, () => undefined)
38
+ : schema[keyOrDataPath]
39
+ : undefined
40
+
41
+ if (value === undefined && def !== undefined) {
42
+ if (callback && isFunction(def) && !isMatchingType(types, def)) {
43
+ // Support `default()` functions for any type except `Function`:
44
+ def = def(context)
45
+ }
46
+ return def
47
+ }
48
+
49
+ if (isMatchingType(types, value)) {
50
+ return value
51
+ }
52
+ // Any schema value handled through `getSchemaValue()` can provide
53
+ // a function that's resolved when the value is evaluated:
54
+ if (callback && isFunction(value)) {
55
+ value = value(context)
56
+ }
57
+ // Now finally see if we can convert to the expect types.
58
+ if (types && value != null && !isMatchingType(types, value)) {
59
+ for (const type of types) {
60
+ const converted = convertType(type, value)
61
+ if (converted !== value) {
62
+ return converted
63
+ }
64
+ }
65
+ }
66
+ return value
67
+ }
68
+
69
+ export function shouldRenderSchema(schema, context) {
70
+ return (
71
+ getSchemaValue('if', {
72
+ type: Boolean,
73
+ schema,
74
+ context,
75
+ default: true
76
+ }) && (
77
+ !hasNestedSchemaComponents(schema) ||
78
+ // The components of sources, e.g. lists with inlined components, are
79
+ // rendered for each item, with the item's own context:
80
+ !!getSourceType(schema) ||
81
+ // Evaluate the components with their own contexts, like when rendered:
82
+ someNestedSchemaComponent(schema, (component, name) =>
83
+ shouldRenderSchema(
84
+ component,
85
+ context.createChildContext(component, name)
86
+ )
87
+ )
88
+ )
89
+ )
90
+ }
91
+
92
+ function getContext(context) {
93
+ return isFunction(context) ? context() : context
94
+ }
95
+
96
+ export function getDefaultValue(schema, context) {
97
+ // Support default values both on schema and on component level.
98
+ // NOTE: At the time of creation, components may not be instantiated, (e.g. if
99
+ // entries are created through nested forms, the parent form isn't mounted) so
100
+ // we can't use `dataPath` to get to components, and the `defaultValue` from
101
+ // there. That's why `defaultValue` is defined statically in the components:
102
+ const defaultValue =
103
+ schema.default !== undefined
104
+ ? schema.default
105
+ : getTypeOptions(schema)?.defaultValue
106
+ return isFunction(defaultValue)
107
+ ? defaultValue(getContext(context))
108
+ : clone(defaultValue)
109
+ }
110
+
111
+ export function shouldExcludeValue(schema, context) {
112
+ const excludeValue =
113
+ schema.exclude !== undefined
114
+ ? schema.exclude
115
+ : getTypeOptions(schema)?.excludeValue
116
+ return isFunction(excludeValue)
117
+ ? excludeValue(getContext(context))
118
+ : !!excludeValue
119
+ }
120
+
121
+ export function shouldIgnoreMissingValue(schema, context) {
122
+ return !!getTypeOptions(schema)?.ignoreMissingValue?.(getContext(context))
123
+ }
124
+
125
+ // Returns whether the value of the component described by `schema` and `name`
126
+ // is missing in `data`: if there's no value, or if the value is `null` and
127
+ // the type treats that as missing, e.g. nested sections.
128
+ export function isMissingValue(schema, data, name, context) {
129
+ // Only read the value if needed, so that callers that are tracked, e.g. the
130
+ // walk of the form model's data, don't depend on all values.
131
+ return (
132
+ !(name in data) ||
133
+ !!getTypeOptions(schema)?.treatNullAsMissing?.(getContext(context)) &&
134
+ data[name] === null
135
+ )
136
+ }
137
+
138
+ /**
139
+ * Returns whether the value of the component described by `schema` is
140
+ * resolved from its data schema, `schema.data` or `schema.dataPath`, as for
141
+ * the types with the `valueFromDataSchema` option, e.g. `computed`. See
142
+ * `FormModel`.
143
+ */
144
+ export function hasValueFromDataSchema(schema) {
145
+ return (
146
+ !!getTypeOptions(schema)?.valueFromDataSchema &&
147
+ !!(schema.data || schema.dataPath)
148
+ )
149
+ }
150
+
151
+ export function getMultipleValue(schema) {
152
+ return schema.multiple ?? !!getTypeOptions(schema)?.defaultMultiple
153
+ }
154
+
155
+ /**
156
+ * Returns whether the value of the component described by `schema` and `name`
157
+ * is missing in `data` and needs its default, see `initializeData()`.
158
+ * `shouldSetDefaultsOfComponentsWithCompute` decides for components with
159
+ * `compute()`, which may derive their missing values instead.
160
+ */
161
+ export function shouldSetDefaultValue(schema, data, name, context, {
162
+ shouldSetDefaultsOfComponentsWithCompute = true
163
+ } = {}) {
164
+ return (
165
+ (shouldSetDefaultsOfComponentsWithCompute || !schema.compute) &&
166
+ isMissingValue(schema, data, name, context) &&
167
+ !shouldIgnoreMissingValue(schema, context)
168
+ )
169
+ }
170
+
171
+ /**
172
+ * Initializes `data` for the components of `schema`: Sets missing values to
173
+ * their defaults, so they can be correctly watched for changes, and numbers
174
+ * the items of lists by their order key, see `updateOrder()`, so that their
175
+ * order is stored even if it never changes.
176
+ */
177
+ export function initializeData(schema, data = {}, component, {
178
+ dataPath = null,
179
+ rootData = data,
180
+ shouldProcess,
181
+ // Whether to also set the defaults of components with `compute()`. New data
182
+ // starts with all defaults, which `compute()` can rely on. In data that was
183
+ // loaded, `compute()` may derive missing values instead, and they fall back
184
+ // to their defaults only if it doesn't return a value, see `computeValue()`.
185
+ shouldSetDefaultsOfComponentsWithCompute = true
186
+ } = {}) {
187
+ const options = { component, rootData }
188
+
189
+ const before = ({ schema, data, name, dataPath }) => {
190
+ const context = () =>
191
+ new DitoContext(component, {
192
+ schema,
193
+ name,
194
+ data,
195
+ dataPath,
196
+ rootData
197
+ })
198
+ const shouldSetDefault = shouldSetDefaultValue(
199
+ schema,
200
+ data,
201
+ name,
202
+ context,
203
+ { shouldSetDefaultsOfComponentsWithCompute }
204
+ )
205
+ if (shouldSetDefault) {
206
+ data[name] = getDefaultValue(schema, context)
207
+ }
208
+ if (hasItemsNumberedByOrderKey(schema) && isArray(data[name])) {
209
+ updateOrder(schema, data[name])
210
+ }
211
+ }
212
+
213
+ return processSchemaData(schema, data, {
214
+ dataPath,
215
+ before,
216
+ shouldProcess,
217
+ options
218
+ })
219
+ }
220
+
221
+ /**
222
+ * Returns the value of the component described by `schema` and `name` in
223
+ * `data`: the result of `schema.compute()` if it returns a value, else the
224
+ * value in `data`, or its default if it's missing. Never writes into `data`:
225
+ * Computed values, including the defaults of components with `compute()`, are
226
+ * written by `FormModel`, and other defaults when the data is set up, see
227
+ * `initializeData()`, or when they go missing later, see `FormModel`. If
228
+ * provided, `getOptions()` returns the options for `context.options`, called
229
+ * only when they're read.
230
+ */
231
+ export function computeValue(schema, data, name, dataPath, {
232
+ component = null,
233
+ rootData = component?.rootData,
234
+ getOptions = null
235
+ } = {}) {
236
+ const context = () =>
237
+ // Pass a function, so that the `options` getter isn't evaluated when the
238
+ // context is created.
239
+ new DitoContext(component, () => {
240
+ const properties = {
241
+ schema,
242
+ // Override value to prevent endless recursion through calling the
243
+ // getter for `this.value` in `DitoContext`. Read it only when it's
244
+ // used, so that `compute()` only depends on its own value if it reads
245
+ // it, and writing the computed value doesn't call it again.
246
+ get value() {
247
+ return data[name]
248
+ },
249
+ name,
250
+ data,
251
+ dataPath,
252
+ rootData
253
+ }
254
+ if (getOptions) {
255
+ Object.defineProperty(properties, 'options', {
256
+ get: getOptions,
257
+ enumerable: true
258
+ })
259
+ }
260
+ return properties
261
+ })
262
+ const { compute } = schema
263
+ // Like the form model, only compute values of components that are shown
264
+ // through their `if`, as `compute()` may rely on the same conditions, e.g.
265
+ // `if: ({ item }) => item.preview` with `compute: ({ item }) =>
266
+ // item.preview.name`.
267
+ if (
268
+ compute &&
269
+ getSchemaValue('if', {
270
+ type: Boolean,
271
+ schema,
272
+ context: getContext(context),
273
+ default: true
274
+ })
275
+ ) {
276
+ const value = compute(getContext(context))
277
+ if (value !== undefined) {
278
+ return value
279
+ }
280
+ }
281
+ return getValueOrDefault(schema, data, name, context)
282
+ }
283
+
284
+ // Returns the value in `data`, or its default if it's missing. `context` can be
285
+ // a function that creates the context, called only when it's needed.
286
+ function getValueOrDefault(schema, data, name, context) {
287
+ const shouldUseDefault = (
288
+ isMissingValue(schema, data, name, context) &&
289
+ !shouldIgnoreMissingValue(schema, context)
290
+ )
291
+ return shouldUseDefault ? getDefaultValue(schema, context) : data[name]
292
+ }
293
+
294
+ function cloneItem(sourceSchema, item, options) {
295
+ if (options.schemaOnly) {
296
+ const copy = {}
297
+ const { idKey = 'id', orderKey } = sourceSchema
298
+ const id = item[idKey]
299
+ if (id !== undefined) {
300
+ copy[idKey] = id
301
+ }
302
+ // Copy over type in case there are multiple forms to choose from.
303
+ if (hasMultipleFormSchemas(sourceSchema)) {
304
+ copy.type = item.type
305
+ }
306
+ if (orderKey) {
307
+ copy[orderKey] = item[orderKey]
308
+ }
309
+ return copy
310
+ } else {
311
+ return { ...item }
312
+ }
313
+ }
314
+
315
+ export function processData(schema, sourceSchema, data, dataPath, {
316
+ component,
317
+ rootData,
318
+ schemaOnly, // whether to only include data covered by the schema, or all data
319
+ target,
320
+ // Whether to call the schema callbacks `compute()` and `process()`. Data that
321
+ // is compared while it's edited, e.g. by `FormModel.isDirty`, isn't validated
322
+ // yet, which the callbacks may rely on, and `FormModel` writes the computed
323
+ // values into the data. Without them, the values in the data are used, or
324
+ // their defaults if they're missing, the types still process them through
325
+ // `processValue()`, and the excluded values of components with `process()`
326
+ // are kept, as `process()` may store them elsewhere through `processedItem`.
327
+ shouldCallComputeAndProcess = true
328
+ } = {}) {
329
+ const options = { component, rootData, schemaOnly, target }
330
+ const processedData = cloneItem(sourceSchema, data, options)
331
+ const graph = new SchemaGraph()
332
+
333
+ const before = ({ schema, data, name, dataPath, processedData }) => {
334
+ let value = shouldCallComputeAndProcess
335
+ ? computeValue(schema, data, name, dataPath, options)
336
+ : getValueOrDefault(
337
+ schema,
338
+ data,
339
+ name,
340
+ () =>
341
+ new DitoContext(component, {
342
+ schema,
343
+ name,
344
+ data,
345
+ dataPath,
346
+ rootData: options.rootData
347
+ })
348
+ )
349
+ // The schema expects the `wrapPrimitives` transformations to be present on
350
+ // the data that it is applied on, so warp before and unwrap after.
351
+ if (isArray(value)) {
352
+ const { wrapPrimitives } = schema
353
+ if (wrapPrimitives) {
354
+ value = value.map(entry => ({
355
+ [wrapPrimitives]: entry
356
+ }))
357
+ } else {
358
+ // Always shallow-clone array values:
359
+ value = [...value]
360
+ }
361
+ }
362
+ processedData[name] = value
363
+ }
364
+
365
+ const after = ({ schema, data, name, dataPath, processedData }) => {
366
+ const { wrapPrimitives, process } = schema
367
+ let value = processedData[name]
368
+
369
+ // NOTE: We don't cache this context, since `value` is changing.
370
+ const context = () =>
371
+ new DitoContext(component, {
372
+ schema,
373
+ value,
374
+ name,
375
+ data,
376
+ dataPath,
377
+ rootData: options.rootData,
378
+ // Pass the already processed data to `process()`, so it can be modified
379
+ // through `processedItem` from there.
380
+ processedData
381
+ })
382
+
383
+ // First unwrap the wrapped primitives again, to bring the data back into
384
+ // its native form. See `before()` for more details.
385
+ if (wrapPrimitives && isArray(value)) {
386
+ value = value.map(object => object[wrapPrimitives])
387
+ }
388
+
389
+ // Each component type can provide its own static `processValue()` method
390
+ // to convert the data for storage.
391
+ const processValue = getTypeOptions(schema)?.processValue
392
+ if (processValue) {
393
+ value = processValue(getContext(context), graph)
394
+ }
395
+
396
+ // Handle the user's `process()` callback next, if one is provided, so that
397
+ // it can modify data in `processedData` even if it provides `exclude: true`
398
+ if (process && shouldCallComputeAndProcess) {
399
+ value = process(getContext(context))
400
+ }
401
+
402
+ // Without calling `process()`, keep the excluded values that it may store
403
+ // elsewhere, see `shouldCallComputeAndProcess`:
404
+ const shouldKeepExcludedValue = !!process && !shouldCallComputeAndProcess
405
+ if (!shouldKeepExcludedValue && shouldExcludeValue(schema, context)) {
406
+ delete processedData[name]
407
+ } else {
408
+ processedData[name] = value
409
+ }
410
+ }
411
+
412
+ processSchemaData(schema, data, {
413
+ dataPath,
414
+ processedData,
415
+ before,
416
+ after,
417
+ options
418
+ })
419
+
420
+ return graph.process(sourceSchema, processedData, options)
421
+ }
422
+
423
+ /**
424
+ * Walks `data` along the components of `schema`, including nested forms of
425
+ * list and object items, calling `before()` and `after()` for each nested
426
+ * component, e.g. to process the data into `processedData`. `shouldProcess()`
427
+ * is called for all components, tabs and panels, and the ones for which it
428
+ * returns `false` are skipped along with their content. The entries passed to
429
+ * them contain the `componentPath` of the component, tab or panel, continuing
430
+ * `componentPath` like `DitoMixin.componentPath` does.
431
+ *
432
+ * The primitive values of lists with `wrapPrimitives` are walked wrapped in
433
+ * objects under the `wrapPrimitives` key, like the admin edits them, see
434
+ * `SourceMixin`. `wrappedPrimitiveName` is that key while walking such an
435
+ * object, and its component keeps the data path of the item, where the admin
436
+ * maps errors of the value to. Values written into the objects don't reach
437
+ * the data, e.g. defaults, as the primitive values themselves are the data.
438
+ */
439
+ export function processSchemaData(schema, data, {
440
+ dataPath = null,
441
+ componentPath = '',
442
+ processedData = null,
443
+ wrappedPrimitiveName = null,
444
+ before = null,
445
+ after = null,
446
+ shouldProcess = () => true,
447
+ options
448
+ }) {
449
+ const walkOptions = { before, after, shouldProcess, options }
450
+ const getDataPath = (dataPath, token) =>
451
+ dataPath != null
452
+ ? appendDataPath(dataPath, token)
453
+ : null
454
+
455
+ const processComponents = (components, parentComponentPath) => {
456
+ if (components) {
457
+ for (const [name, componentSchema] of Object.entries(components)) {
458
+ const isNestedComponent = isNested(componentSchema)
459
+ const isWrappedPrimitive = name === wrappedPrimitiveName
460
+ const componentDataPath =
461
+ isNestedComponent && !isWrappedPrimitive
462
+ ? getDataPath(dataPath, name)
463
+ : dataPath
464
+ const entry = {
465
+ schema: componentSchema,
466
+ data,
467
+ name,
468
+ dataPath: componentDataPath,
469
+ componentPath: appendDataPath(parentComponentPath, name),
470
+ processedData
471
+ }
472
+ if (!shouldProcess(entry)) {
473
+ continue
474
+ }
475
+ if (isPanelWithOwnData(componentSchema)) {
476
+ // The components of panels with their own data don't edit `data`.
477
+ continue
478
+ }
479
+ if (!isNestedComponent) {
480
+ // Recursively process data on unnested components.
481
+ processSchemaData(componentSchema, data, {
482
+ ...walkOptions,
483
+ dataPath,
484
+ componentPath: entry.componentPath,
485
+ processedData
486
+ })
487
+ } else {
488
+ const { wrapPrimitives } = componentSchema
489
+ const processItem = (item, index = null) => {
490
+ // Data that isn't processed holds the primitive values themselves,
491
+ // processed data holds them wrapped already, see `processData()`.
492
+ if (wrapPrimitives && !isObject(item)) {
493
+ item = { [wrapPrimitives]: item }
494
+ }
495
+ const itemDataPath =
496
+ index !== null
497
+ ? getDataPath(componentDataPath, index)
498
+ : componentDataPath
499
+ const itemComponentPath =
500
+ index !== null
501
+ ? appendDataPath(entry.componentPath, index)
502
+ : entry.componentPath
503
+ const context = new DitoContext(options.component, {
504
+ schema: componentSchema,
505
+ data,
506
+ dataPath: componentDataPath,
507
+ name,
508
+ rootData: options.rootData
509
+ })
510
+ const getForms = (
511
+ getTypeOptions(componentSchema)?.getFormSchemasForProcessing ||
512
+ getFormSchemas
513
+ )
514
+ const forms = getForms(componentSchema, context)
515
+ const form = getItemFormSchemaFromForms(forms, item)
516
+ if (form) {
517
+ const processedItem = processedData
518
+ ? cloneItem(componentSchema, item, options)
519
+ : null
520
+ return processSchemaData(form, item, {
521
+ ...walkOptions,
522
+ dataPath: itemDataPath,
523
+ componentPath: itemComponentPath,
524
+ processedData: processedItem,
525
+ wrappedPrimitiveName: wrapPrimitives ?? null
526
+ })
527
+ } else {
528
+ // Items without a matching form, e.g. of an unknown type, are
529
+ // passed through as shallow clones.
530
+ return { ...item }
531
+ }
532
+ }
533
+
534
+ before?.(entry)
535
+
536
+ // Only read the values of sources with forms, so that callers that
537
+ // are tracked, e.g. the walk of the form model's data, don't depend
538
+ // on all values.
539
+ if (hasFormSchema(componentSchema)) {
540
+ let value = processedData ? processedData[name] : data[name]
541
+ if (value != null) {
542
+ // Recursively process data on nested form items.
543
+ if (isArray(value)) {
544
+ // Optimization: No need to collect values if not cloning!
545
+ value = processedData
546
+ ? value.map(processItem)
547
+ : value.forEach(processItem)
548
+ } else {
549
+ value = processItem(value)
550
+ }
551
+ if (processedData) {
552
+ processedData[name] = value
553
+ }
554
+ }
555
+ }
556
+
557
+ after?.(entry)
558
+ // The panels of nested components display the data that contains
559
+ // the components, like the ones of unnested components, see
560
+ // `DitoContainer.panelEntries`.
561
+ processTabOrPanelSchemas(componentSchema.panels, entry.componentPath)
562
+ }
563
+ }
564
+ }
565
+ }
566
+
567
+ const processTabOrPanelSchemas = (schemas, parentComponentPath) => {
568
+ // Tabs and panels add their names to component paths, not to data paths.
569
+ for (const [name, tabOrPanelSchema] of Object.entries(schemas || {})) {
570
+ const entry = {
571
+ schema: tabOrPanelSchema,
572
+ data,
573
+ name: null,
574
+ dataPath,
575
+ componentPath: appendDataPath(parentComponentPath, name),
576
+ processedData
577
+ }
578
+ // The components of panels with their own data don't edit `data`.
579
+ if (shouldProcess(entry) && !isPanelWithOwnData(tabOrPanelSchema)) {
580
+ processComponents(tabOrPanelSchema.components, entry.componentPath)
581
+ }
582
+ }
583
+ }
584
+
585
+ processComponents(schema.components, componentPath)
586
+ processTabOrPanelSchemas(schema.tabs, componentPath)
587
+ processTabOrPanelSchemas(schema.panels, componentPath)
588
+
589
+ return processedData || data
590
+ }
591
+
592
+ /**
593
+ * Returns the component path of the value at `dataPath` in `data`, by walking
594
+ * `data` along the components of `schema` to the deepest component that
595
+ * contains the value, e.g. `main/chapters/1` for `chapters/1`, with the list
596
+ * `chapters` in the tab `main`. The part of `dataPath` inside that component,
597
+ * e.g. item indices, is appended to its component path. Of several components
598
+ * that display the same data, the first in schema order is used.
599
+ */
600
+ export function getComponentPathByDataPath(schema, data, dataPath, {
601
+ componentPath = '',
602
+ component = null,
603
+ rootData = data
604
+ } = {}) {
605
+ let deepestEntry = { dataPath: '', componentPath }
606
+ if (data) {
607
+ processSchemaData(schema, data, {
608
+ dataPath: '',
609
+ componentPath,
610
+ // Only walk the components that contain the value at `dataPath`:
611
+ shouldProcess: entry => (
612
+ getRelativeDataPath(dataPath, entry.dataPath) !== null
613
+ ),
614
+ before: entry => {
615
+ if (entry.dataPath.length > deepestEntry.dataPath.length) {
616
+ deepestEntry = entry
617
+ }
618
+ },
619
+ options: { component, rootData }
620
+ })
621
+ }
622
+ const relativeDataPath = getRelativeDataPath(dataPath, deepestEntry.dataPath)
623
+ return relativeDataPath
624
+ ? appendDataPath(deepestEntry.componentPath, relativeDataPath)
625
+ : deepestEntry.componentPath
626
+ }
627
+
628
+ export function getItemId(sourceSchema, item) {
629
+ const id = item[sourceSchema.idKey || 'id']
630
+ return id != null ? String(id) : undefined
631
+ }
632
+
633
+ // Returns whether the items of the list described by `schema` are numbered by
634
+ // its `orderKey`, see `updateOrder()`. Lists of primitives have no items that
635
+ // could hold an order key.
636
+ export function hasItemsNumberedByOrderKey(schema) {
637
+ return !!schema.orderKey && !schema.wrapPrimitives
638
+ }
639
+
640
+ export function updateOrder(sourceSchema, list, paginationRange) {
641
+ const { orderKey } = sourceSchema
642
+ if (orderKey) {
643
+ // Reorder the changed entries by their order key, taking pagination
644
+ // offsets into account:
645
+ const offset = paginationRange?.[0] || 0
646
+ for (let i = 0; i < list.length; i++) {
647
+ list[i][orderKey] = i + offset
648
+ }
649
+ }
650
+ return list
651
+ }
652
+
653
+ export function getItemUid(sourceSchema, item) {
654
+ // Try to use the item id as the uid, falling back on auto-generated ids, but
655
+ // either way, pass through `getUid()` so that the ids are associated with the
656
+ // item through a weak map, as the ids can be filtered out in `processData()`
657
+ // while the components that use the uids as key are still visible.
658
+ return getUid(item, item => getItemId(sourceSchema, item))
659
+ }