@rokkit/forms 1.0.0-next.122

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 (84) hide show
  1. package/dist/src/forms-old/input/types.d.ts +7 -0
  2. package/dist/src/forms-old/lib/form.d.ts +95 -0
  3. package/dist/src/forms-old/lib/index.d.ts +1 -0
  4. package/dist/src/index.d.ts +5 -0
  5. package/dist/src/input/index.d.ts +18 -0
  6. package/dist/src/lib/builder.svelte.d.ts +140 -0
  7. package/dist/src/lib/deprecated/nested.d.ts +48 -0
  8. package/dist/src/lib/deprecated/nested.spec.d.ts +1 -0
  9. package/dist/src/lib/deprecated/validator.d.ts +30 -0
  10. package/dist/src/lib/deprecated/validator.spec.d.ts +1 -0
  11. package/dist/src/lib/fields.d.ts +16 -0
  12. package/dist/src/lib/fields.spec.d.ts +1 -0
  13. package/dist/src/lib/index.d.ts +7 -0
  14. package/dist/src/lib/layout.d.ts +7 -0
  15. package/dist/src/lib/schema.d.ts +7 -0
  16. package/dist/src/lib/validation.d.ts +41 -0
  17. package/dist/src/types.d.ts +5 -0
  18. package/package.json +38 -0
  19. package/src/DataEditor.svelte +30 -0
  20. package/src/FieldLayout.svelte +48 -0
  21. package/src/FormRenderer.svelte +118 -0
  22. package/src/Input.svelte +75 -0
  23. package/src/InputField.svelte +55 -0
  24. package/src/ListEditor.svelte +44 -0
  25. package/src/NestedEditor.svelte +85 -0
  26. package/src/forms-old/CheckBox.svelte +56 -0
  27. package/src/forms-old/DataEditor.svelte +30 -0
  28. package/src/forms-old/FieldLayout.svelte +48 -0
  29. package/src/forms-old/Form.svelte +17 -0
  30. package/src/forms-old/Icon.svelte +76 -0
  31. package/src/forms-old/Item.svelte +25 -0
  32. package/src/forms-old/ListEditor.svelte +44 -0
  33. package/src/forms-old/Tabs.svelte +57 -0
  34. package/src/forms-old/Wrapper.svelte +12 -0
  35. package/src/forms-old/input/Input.svelte +17 -0
  36. package/src/forms-old/input/InputField.svelte +70 -0
  37. package/src/forms-old/input/InputSelect.svelte +23 -0
  38. package/src/forms-old/input/InputSwitch.svelte +19 -0
  39. package/src/forms-old/input/types.js +29 -0
  40. package/src/forms-old/lib/form.js +72 -0
  41. package/src/forms-old/lib/index.js +12 -0
  42. package/src/forms-old/mocks/CustomField.svelte +7 -0
  43. package/src/forms-old/mocks/CustomWrapper.svelte +8 -0
  44. package/src/forms-old/mocks/Register.svelte +25 -0
  45. package/src/index.js +7 -0
  46. package/src/inp/Input.svelte +17 -0
  47. package/src/inp/InputField.svelte +69 -0
  48. package/src/inp/InputSelect.svelte +23 -0
  49. package/src/inp/InputSwitch.svelte +19 -0
  50. package/src/input/InputCheckbox.svelte +74 -0
  51. package/src/input/InputColor.svelte +42 -0
  52. package/src/input/InputDate.svelte +54 -0
  53. package/src/input/InputDateTime.svelte +54 -0
  54. package/src/input/InputEmail.svelte +63 -0
  55. package/src/input/InputFile.svelte +45 -0
  56. package/src/input/InputMonth.svelte +54 -0
  57. package/src/input/InputNumber.svelte +57 -0
  58. package/src/input/InputPassword.svelte +60 -0
  59. package/src/input/InputRadio.svelte +60 -0
  60. package/src/input/InputRange.svelte +51 -0
  61. package/src/input/InputSelect.svelte +71 -0
  62. package/src/input/InputSwitch.svelte +29 -0
  63. package/src/input/InputTel.svelte +60 -0
  64. package/src/input/InputText.svelte +60 -0
  65. package/src/input/InputTextArea.svelte +59 -0
  66. package/src/input/InputTime.svelte +54 -0
  67. package/src/input/InputUrl.svelte +60 -0
  68. package/src/input/InputWeek.svelte +54 -0
  69. package/src/input/index.js +23 -0
  70. package/src/lib/Input.svelte +291 -0
  71. package/src/lib/builder.svelte.js +359 -0
  72. package/src/lib/deprecated/Form.svelte +17 -0
  73. package/src/lib/deprecated/FormRenderer.svelte +121 -0
  74. package/src/lib/deprecated/nested.js +192 -0
  75. package/src/lib/deprecated/nested.spec.js +512 -0
  76. package/src/lib/deprecated/validator.js +137 -0
  77. package/src/lib/deprecated/validator.spec.js +348 -0
  78. package/src/lib/fields.js +119 -0
  79. package/src/lib/fields.spec.js +250 -0
  80. package/src/lib/index.js +7 -0
  81. package/src/lib/layout.js +63 -0
  82. package/src/lib/schema.js +32 -0
  83. package/src/lib/validation.js +273 -0
  84. package/src/types.js +29 -0
@@ -0,0 +1,359 @@
1
+ import { deriveSchemaFromValue } from './schema.js'
2
+ import { deriveLayoutFromValue } from './layout.js'
3
+ import { getSchemaWithLayout } from './fields.js'
4
+ import { omit } from 'ramda'
5
+
6
+ /**
7
+ * @typedef {Object} FormElement
8
+ * @property {string} scope - JSON Pointer path (e.g., '#/email', '#/user/name')
9
+ * @property {string} type - Input type (text, number, range, checkbox, select, etc.)
10
+ * @property {any} value - Current value from data
11
+ * @property {boolean} override - Whether to use custom child snippet (from layout)
12
+ * @property {Object} props - Merged properties from schema + layout + validation
13
+ * @property {string} [props.label] - Display label (from layout)
14
+ * @property {string} [props.description] - Help text (from layout)
15
+ * @property {string} [props.placeholder] - Placeholder text (from layout)
16
+ * @property {boolean} [props.required] - Required flag (from schema)
17
+ * @property {number} [props.min] - Minimum value (from schema)
18
+ * @property {number} [props.max] - Maximum value (from schema)
19
+ * @property {Object} [props.message] - Validation message object
20
+ * @property {string} [props.message.state] - Message state: 'error', 'warning', 'info', 'success'
21
+ * @property {string} [props.message.text] - Message text content
22
+ */
23
+
24
+ /**
25
+ * FormBuilder class for dynamically generating forms from data structures
26
+ */
27
+ export class FormBuilder {
28
+ /** @type {Object} */
29
+ #data = $state({})
30
+
31
+ /** @type {Object} */
32
+ #schema = $state({})
33
+
34
+ /** @type {Object} */
35
+ #layout = $state({})
36
+
37
+ /** @type {Object} */
38
+ #validation = $state({})
39
+
40
+ /** @type {FormElement[]} */
41
+ elements = $derived(this.#buildElements())
42
+ combined = $derived(getSchemaWithLayout(this.schema, this.layout))
43
+ /**
44
+ * Get the current data
45
+ * @returns {Object} Current data object
46
+ */
47
+ get data() {
48
+ return this.#data
49
+ }
50
+
51
+ /**
52
+ * Set the data
53
+ * @param {Object} value - New data object
54
+ */
55
+ set data(value) {
56
+ this.#data = value
57
+ }
58
+
59
+ /**
60
+ * Get the current schema
61
+ * @returns {Object} Current schema object
62
+ */
63
+ get schema() {
64
+ return this.#schema
65
+ }
66
+
67
+ /**
68
+ * Set the schema
69
+ * @param {Object} value - New schema object
70
+ */
71
+ set schema(value) {
72
+ this.#schema = value ?? deriveSchemaFromValue(this.#data)
73
+ }
74
+
75
+ /**
76
+ * Get the current layout
77
+ * @returns {Object} Current layout object
78
+ */
79
+ get layout() {
80
+ return this.#layout
81
+ }
82
+
83
+ /**
84
+ * Set the layout
85
+ * @param {Object} value - New layout object
86
+ */
87
+ set layout(value) {
88
+ this.#layout = value ?? deriveLayoutFromValue(this.#data)
89
+ }
90
+
91
+ /**
92
+ * Get the current validation state
93
+ * @returns {Object} Current validation object
94
+ */
95
+ get validation() {
96
+ return this.#validation
97
+ }
98
+
99
+ /**
100
+ * Set validation messages for fields
101
+ * @param {Object} value - Validation object with field paths as keys
102
+ */
103
+ set validation(value) {
104
+ this.#validation = value
105
+ }
106
+
107
+ /**
108
+ * Create a new FormBuilder instance
109
+ * @param {Object} [data={}] - Initial data object
110
+ * @param {Object|null} [schema=null] - Optional schema override
111
+ * @param {Object|null} [layout=null] - Optional layout override
112
+ */
113
+ constructor(data = {}, schema = null, layout = null) {
114
+ this.#data = data
115
+ this.schema = schema
116
+ this.layout = layout
117
+ }
118
+
119
+ /**
120
+ * Update a specific field value
121
+ * @param {string} path - Field path (e.g., 'count', 'settings/distance')
122
+ * @param {any} value - New value
123
+ */
124
+ updateField(path, value) {
125
+ // Simple path handling for now - can be enhanced for nested objects
126
+ const keys = path.split('/')
127
+ if (keys.length === 1) {
128
+ this.#data = { ...this.#data, [keys[0]]: value }
129
+ } else {
130
+ // Handle nested paths if needed
131
+ const updatedData = { ...this.#data }
132
+ let current = updatedData
133
+ for (let i = 0; i < keys.length - 1; i++) {
134
+ current[keys[i]] = { ...current[keys[i]] }
135
+ current = current[keys[i]]
136
+ }
137
+ current[keys[keys.length - 1]] = value
138
+ this.#data = updatedData
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Get a field value by path
144
+ * @param {string} path - Field path
145
+ * @returns {any} Field value
146
+ */
147
+ getValue(path) {
148
+ if (!path) return undefined
149
+
150
+ const keys = path.split('/')
151
+ let current = this.#data
152
+ for (const key of keys) {
153
+ if (current && typeof current === 'object') {
154
+ current = current[key]
155
+ } else {
156
+ return undefined
157
+ }
158
+ }
159
+ return current
160
+ }
161
+
162
+ /**
163
+ * Build form elements from schema and layout using getSchemaWithLayout
164
+ * @private
165
+ * @returns {FormElement[]} Array of form elements
166
+ */
167
+ #buildElements() {
168
+ // if (!this.#schema || !this.#layout || !this.#layout.elements) {
169
+ // return []
170
+ // }
171
+
172
+ try {
173
+ // Use getSchemaWithLayout to combine schema and layout
174
+ // const combined = getSchemaWithLayout(this.#schema, this.#layout)
175
+ // Convert combined elements to FormElement format
176
+ return this.combined.elements
177
+ .map((element) => this.#convertToFormElement(element))
178
+ .filter((element) => element !== null)
179
+ } catch (error) {
180
+ // If getSchemaWithLayout fails, fall back to basic element creation
181
+ console.warn('Failed to build elements:', error)
182
+ return this.#buildBasicElements()
183
+ }
184
+ }
185
+
186
+ /**
187
+ * Build basic form elements when getSchemaWithLayout fails
188
+ * @private
189
+ * @returns {FormElement[]} Array of form elements
190
+ */
191
+ #buildBasicElements() {
192
+ const elements = []
193
+
194
+ if (this.#layout.elements) {
195
+ for (const layoutElement of this.#layout.elements) {
196
+ const formElement = this.#buildBasicElement(layoutElement)
197
+ if (formElement) {
198
+ elements.push(formElement)
199
+ }
200
+ }
201
+ }
202
+
203
+ return elements
204
+ }
205
+
206
+ /**
207
+ * Build a basic form element from layout only
208
+ * @private
209
+ * @param {Object} layoutElement - Layout element definition
210
+ * @returns {FormElement|null} Form element or null
211
+ */
212
+ #buildBasicElement(layoutElement) {
213
+ const { scope, label, override = false, ...layoutProps } = layoutElement
214
+
215
+ if (!scope) return null
216
+
217
+ // Extract field name from scope (remove leading '#/')
218
+ const fieldPath = scope.replace(/^#\//, '')
219
+ const value = this.getValue(fieldPath)
220
+
221
+ // Default type is text when no schema is available
222
+ const type = 'text'
223
+
224
+ // Basic props
225
+ const props = {
226
+ label: label || fieldPath,
227
+ ...layoutProps,
228
+ message: this.#validation[fieldPath] || null,
229
+ type
230
+ }
231
+
232
+ return {
233
+ scope,
234
+ // type,
235
+ value,
236
+ override,
237
+ props
238
+ }
239
+ }
240
+
241
+ /**
242
+ * Convert a combined schema/layout element to FormElement format
243
+ * @private
244
+ * @param {Object} element - Combined element from getSchemaWithLayout
245
+ * @param {string} parentPath - Parent path for nested elements
246
+ * @returns {FormElement} Form element
247
+ */
248
+ #convertToFormElement(element, parentPath = '') {
249
+ const { key, props } = element
250
+
251
+ // Skip elements without a key
252
+ if (!key) {
253
+ return null
254
+ }
255
+
256
+ // Create scope in JSON Pointer format
257
+ const fieldPath = parentPath ? `${parentPath}/${key}` : key
258
+ const scope = `#/${fieldPath}`
259
+ const value = this.getValue(fieldPath)
260
+
261
+ // Handle nested elements (arrays and objects)
262
+ if (element.elements) {
263
+ // This is a nested structure, process children
264
+ const nestedElements = element.elements.map((child) =>
265
+ this.#convertToFormElement(child, fieldPath)
266
+ )
267
+
268
+ return {
269
+ scope,
270
+ type: 'group',
271
+ value,
272
+ override: element.override || false,
273
+ props: {
274
+ ...props,
275
+ elements: nestedElements,
276
+ message: this.#validation[fieldPath] || null
277
+ }
278
+ }
279
+ }
280
+
281
+ // Determine input type based on schema type
282
+ let type = 'text'
283
+ if (props.type) {
284
+ switch (props.type) {
285
+ case 'number':
286
+ case 'integer':
287
+ type = props.min !== undefined && props.max !== undefined ? 'range' : 'number'
288
+ break
289
+ case 'boolean':
290
+ type = 'checkbox'
291
+ break
292
+ case 'string':
293
+ if (props.enum) {
294
+ type = 'select'
295
+ // Map enum values to options format expected by select inputs
296
+ if (Array.isArray(props.enum)) {
297
+ props.options = props.enum //.map((val) => ({ value: val, label: val }))
298
+ }
299
+ } else {
300
+ type = 'text'
301
+ }
302
+ break
303
+ case 'array':
304
+ type = 'array'
305
+ break
306
+ }
307
+ }
308
+
309
+ // Add validation message if exists
310
+ const validationMessage = this.#validation[fieldPath] || null
311
+
312
+ // Compose final props
313
+ const finalProps = {
314
+ ...props,
315
+ type,
316
+ message: validationMessage
317
+ }
318
+
319
+ return {
320
+ scope,
321
+ // type,
322
+ value,
323
+ override: element.override || false,
324
+ props: finalProps
325
+ }
326
+ }
327
+
328
+ /**
329
+ * Set validation message for a specific field
330
+ * @param {string} fieldPath - Field path (without '#/' prefix)
331
+ * @param {Object|null} message - Validation message object or null to clear
332
+ * @param {string} message.state - Message state: 'error', 'warning', 'info', 'success'
333
+ * @param {string} message.text - Message text content
334
+ */
335
+ setFieldValidation(fieldPath, message) {
336
+ if (message) {
337
+ this.#validation = { ...this.#validation, [fieldPath]: message }
338
+ } else {
339
+ this.#validation = omit([fieldPath], this.#validation)
340
+ }
341
+ }
342
+
343
+ /**
344
+ * Clear all validation messages
345
+ */
346
+ clearValidation() {
347
+ this.#validation = {}
348
+ }
349
+
350
+ /**
351
+ * Reset form to initial state
352
+ */
353
+ reset() {
354
+ this.#data = {}
355
+ // this.#schema = {}
356
+ // this.#layout = {}
357
+ this.#validation = {}
358
+ }
359
+ }
@@ -0,0 +1,17 @@
1
+ <script>
2
+ import DataEditor from './DataEditor.svelte'
3
+
4
+ export let value
5
+ export let schema = null
6
+ export let layout = null
7
+ export let using = {}
8
+ </script>
9
+
10
+ <form on:submit>
11
+ <DataEditor bind:value {schema} {layout} {using} />
12
+ <span>
13
+ <slot>
14
+ <button type="submit">Submit</button>
15
+ </slot>
16
+ </span>
17
+ </form>
@@ -0,0 +1,121 @@
1
+ <script>
2
+ // Props using Svelte 5 runes
3
+ let { elements = [], onUpdate = null } = $props()
4
+
5
+ /**
6
+ * Handle field value changes
7
+ * @param {string} scope - Field scope/path
8
+ * @param {any} value - New value
9
+ */
10
+ function handleChange(scope, value) {
11
+ onUpdate?.(scope, value)
12
+ }
13
+
14
+ /**
15
+ * Handle range input changes (convert to number)
16
+ * @param {string} scope - Field scope/path
17
+ * @param {Event} event - Input event
18
+ */
19
+ function handleRangeChange(scope, event) {
20
+ const value = Number(event.target.value)
21
+ handleChange(scope, value)
22
+ }
23
+
24
+ /**
25
+ * Handle number input changes (convert to number)
26
+ * @param {string} scope - Field scope/path
27
+ * @param {Event} event - Input event
28
+ */
29
+ function handleNumberChange(scope, event) {
30
+ const value = Number(event.target.value)
31
+ handleChange(scope, value)
32
+ }
33
+
34
+ /**
35
+ * Handle checkbox changes
36
+ * @param {string} scope - Field scope/path
37
+ * @param {Event} event - Input event
38
+ */
39
+ function handleCheckboxChange(scope, event) {
40
+ const value = event.target.checked
41
+ handleChange(scope, value)
42
+ }
43
+
44
+ /**
45
+ * Handle text input changes
46
+ * @param {string} scope - Field scope/path
47
+ * @param {Event} event - Input event
48
+ */
49
+ function handleTextChange(scope, event) {
50
+ const value = event.target.value
51
+ handleChange(scope, value)
52
+ }
53
+ </script>
54
+
55
+ <div class="space-y-4">
56
+ {#each elements as element (element.scope)}
57
+ <div class="form-element">
58
+ {#if element.type === 'range'}
59
+ <label class="block text-sm font-medium text-neutral-700 dark:text-neutral-300">
60
+ {element.label}: {element.value}
61
+ <input
62
+ type="range"
63
+ value={element.value}
64
+ min={element.constraints?.min}
65
+ max={element.constraints?.max}
66
+ step={element.constraints?.step}
67
+ oninput={(e) => handleRangeChange(element.scope, e)}
68
+ class="mt-1 h-2 w-full cursor-pointer appearance-none rounded-lg bg-neutral-200 dark:bg-neutral-700"
69
+ />
70
+ </label>
71
+ {:else if element.type === 'number'}
72
+ <label class="block text-sm font-medium text-neutral-700 dark:text-neutral-300">
73
+ {element.label}
74
+ <input
75
+ type="number"
76
+ value={element.value}
77
+ min={element.constraints?.min}
78
+ max={element.constraints?.max}
79
+ step={element.constraints?.step}
80
+ oninput={(e) => handleNumberChange(element.scope, e)}
81
+ class="focus:border-primary-500 focus:ring-primary-500 mt-1 block w-full rounded-md border border-neutral-300 px-3 py-2 shadow-sm focus:outline-none dark:border-neutral-600 dark:bg-neutral-700 dark:text-white"
82
+ />
83
+ </label>
84
+ {:else if element.type === 'checkbox'}
85
+ <label class="flex items-center text-sm font-medium text-neutral-700 dark:text-neutral-300">
86
+ <input
87
+ type="checkbox"
88
+ checked={element.value}
89
+ onchange={(e) => handleCheckboxChange(element.scope, e)}
90
+ class="text-primary-600 focus:ring-primary-500 mr-2 h-4 w-4 rounded border-neutral-300 dark:border-neutral-600"
91
+ />
92
+ {element.label}
93
+ </label>
94
+ {:else if element.type === 'select'}
95
+ <label class="block text-sm font-medium text-neutral-700 dark:text-neutral-300">
96
+ {element.label}
97
+ <select
98
+ value={element.value}
99
+ onchange={(e) => handleTextChange(element.scope, e)}
100
+ class="focus:border-primary-500 focus:ring-primary-500 mt-1 block w-full rounded-md border border-neutral-300 px-3 py-2 shadow-sm focus:outline-none dark:border-neutral-600 dark:bg-neutral-700 dark:text-white"
101
+ >
102
+ {#each element.constraints?.options || [] as option, index (index)}
103
+ <option value={option}>{option}</option>
104
+ {/each}
105
+ </select>
106
+ </label>
107
+ {:else}
108
+ <!-- Default text input -->
109
+ <label class="block text-sm font-medium text-neutral-700 dark:text-neutral-300">
110
+ {element.label}
111
+ <input
112
+ type="text"
113
+ value={element.value || ''}
114
+ oninput={(e) => handleTextChange(element.scope, e)}
115
+ class="focus:border-primary-500 focus:ring-primary-500 mt-1 block w-full rounded-md border border-neutral-300 px-3 py-2 shadow-sm focus:outline-none dark:border-neutral-600 dark:bg-neutral-700 dark:text-white"
116
+ />
117
+ </label>
118
+ {/if}
119
+ </div>
120
+ {/each}
121
+ </div>
@@ -0,0 +1,192 @@
1
+ import { omit, pick } from 'ramda'
2
+ import { isObject } from '@rokkit/core'
3
+ import { typeOf } from '@rokkit/data'
4
+ import { deriveSchemaFromValue } from '../schema'
5
+ import { deriveLayoutFromValue } from '../layout'
6
+
7
+ /**
8
+ * Flattens an object into a flat object
9
+ *
10
+ * @param {Object} input - The object to flatten
11
+ * @param {String} scope - The scope of the object
12
+ */
13
+ export function flattenObject(input, scope = '#') {
14
+ // eslint-disable-next-line no-use-before-define
15
+ return flattenAttributes(input, scope).reduce(
16
+ // eslint-disable-next-line no-use-before-define
17
+ (acc, item) => ({ ...acc, ...flattenElement(item) }),
18
+ {
19
+ [scope]: {
20
+ type: 'object',
21
+ value: input,
22
+ scope,
23
+ key: scope.split('/').slice(-1)[0]
24
+ }
25
+ }
26
+ )
27
+ }
28
+ /**
29
+ * Flattens an object into an array of key-value pairs
30
+ *
31
+ * @param {Object} input - The object to flatten
32
+ * @param {String} scope - The scope of the object
33
+ */
34
+ export function flattenAttributes(input, scope = '#') {
35
+ return Object.entries(input).map(([key, value]) => ({
36
+ key,
37
+ value,
38
+ type: typeOf(value),
39
+ scope: [scope, key].join('/')
40
+ }))
41
+ }
42
+
43
+ /**
44
+ * Derives a nested schema from an object
45
+ *
46
+ * @param {Object} input - The object to derive the schema from
47
+ * @param {String} scope - The scope of the object
48
+ * @returns {Object} The derived schema
49
+ */
50
+ export function deriveNestedSchema(input, scope = '#') {
51
+ const elements = flattenAttributes(input)
52
+ const atoms = elements.filter(({ type }) => !['object', 'array'].includes(type))
53
+
54
+ const schema = {
55
+ type: 'object'
56
+ }
57
+
58
+ if (atoms.length > 0) {
59
+ schema.properties = atoms.reduce(
60
+ (acc, { key, type, value }) => ({
61
+ ...acc,
62
+ [key]: {
63
+ type,
64
+ default: value
65
+ }
66
+ }),
67
+ {}
68
+ )
69
+ schema.layout = {
70
+ type: 'vertical',
71
+ elements: atoms.map((el) => ({ label: el.key, scope: el.scope }))
72
+ }
73
+ }
74
+
75
+ if (atoms.length < elements.length) {
76
+ // eslint-disable-next-line no-use-before-define
77
+ schema.children = deriveSchemaForChildren(elements, scope)
78
+ }
79
+
80
+ if (scope !== '#') return schema
81
+ return schema.properties ? [schema] : schema.children
82
+ }
83
+
84
+ /**
85
+ * Derives the children of an object
86
+ *
87
+ * @param {Array} elements - The elements to derive children from
88
+ * @param {String} scope - The scope of the object
89
+ * @returns {Array} The derived children
90
+ */
91
+ function deriveSchemaForChildren(elements, scope) {
92
+ return [
93
+ ...elements
94
+ .filter(({ type }) => type === 'object')
95
+ .map((item) => ({
96
+ ...omit(['value', 'scope'], item),
97
+ scope: [scope, item.key].join('/'),
98
+ ...deriveNestedSchema(item.value, [scope, item.key].join('/'))
99
+ })),
100
+ ...elements
101
+ .filter(({ type }) => type === 'array')
102
+ .map((item) => ({
103
+ ...omit(['value'], item),
104
+ default: [],
105
+ scope: [scope, item.key].join('/'),
106
+ items: deriveSchemaFromValue(item.value.length ? item.value[0] : null),
107
+ layout: deriveLayoutFromValue(item.value.length ? item.value[0] : null)
108
+ }))
109
+ ]
110
+ }
111
+
112
+ /**
113
+ * Flattens an element into a flat object
114
+ *
115
+ * @param {Object} element - The element to flatten
116
+ */
117
+ export function flattenElement(element) {
118
+ if (element.type === 'object') {
119
+ return flattenObject(element.value, element.scope)
120
+ } else if (element.type === 'array') {
121
+ return element.value
122
+ .map((item, index) => ({
123
+ value: item,
124
+ scope: [element.scope, `[${index}]`].join('/'),
125
+ key: `[${index}]`,
126
+ type: typeOf(item)
127
+ }))
128
+ .reduce((acc, item) => ({ ...acc, ...flattenElement(item) }), {
129
+ [element.scope]: pick(['key', 'type', 'scope', 'value'], element)
130
+ })
131
+ }
132
+ return { [element.scope]: element }
133
+ }
134
+
135
+ /**
136
+ * Generates an index array referencing the input data
137
+ *
138
+ * @param {Object} data - The flat object to index
139
+ * @param {String} key - The key to use as index
140
+ */
141
+ export function generateIndex(data, key = 'scope') {
142
+ const index = data
143
+ .map((item) => ({
144
+ ...item,
145
+ _path: item[key],
146
+ _isParent: false,
147
+ _isExpanded: true,
148
+ _levels: []
149
+ }))
150
+ .sort((a, b) => a[key].localeCompare(b[key]))
151
+ .filter((item) => item[key] !== '#')
152
+
153
+ let levels = [0]
154
+ let current = 0
155
+
156
+ index.forEach((item, row) => {
157
+ const path = item._path.split('/').slice(1)
158
+ item._depth = path.length - 1
159
+ if (row === 0) {
160
+ item._levels = [0]
161
+ } else if (path.length > levels.length) {
162
+ index[row - 1]._isParent = true
163
+ item._levels = [...levels, 0]
164
+ } else {
165
+ current = levels[path.length - 1] + 1
166
+ item._levels = [...levels.slice(0, path.length - 1), current]
167
+ }
168
+ levels = item._levels
169
+ })
170
+ return index
171
+ }
172
+
173
+ /**
174
+ * Generates a tree table from the input data
175
+ *
176
+ * @param {Object} data - The data to generate the tree table from
177
+ * @param {String} key - The key to use as index
178
+ * @param {Boolean} ellipsis - Whether to truncate the value
179
+ */
180
+ export function generateTreeTable(data, key = 'scope', ellipsis = false) {
181
+ let result = []
182
+ if (Array.isArray(data)) result = generateIndex(data, key)
183
+ if (isObject(data)) result = generateIndex(Object.values(flattenObject(data)), key)
184
+
185
+ if (ellipsis) {
186
+ result = result.map((item) => ({
187
+ ...omit(['value'], item),
188
+ value: ['array', 'object'].includes(item.type) ? '...' : item.value
189
+ }))
190
+ }
191
+ return result
192
+ }