@json-layout/vocabulary 0.1.0 → 0.3.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/src/normalize.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { validateLayoutKeyword, isComponentName, isPartialCompObject, isPartialChildren, isPartialSwitch, isPartialGetItemsExpr, isPartialGetItemsObj, isPartialSlotMarkdown, isPartialGetItemsFetch } from './layout-keyword/index.js'
2
- import { validateNormalizedLayout, normalizedLayoutSchema, isSectionLayout, isCompositeLayout } from './index.js'
2
+ import { validateNormalizedLayout, compositeCompNames } from './normalized-layout/index.js'
3
3
 
4
4
  /**
5
5
  * @typedef {import('./index.js').Child} Child
@@ -13,19 +13,15 @@ import { validateNormalizedLayout, normalizedLayoutSchema, isSectionLayout, isCo
13
13
  * @typedef {import("./types.js").SchemaFragment} SchemaFragment
14
14
  */
15
15
 
16
- // export type Markdown = (src: string) => string
17
-
18
- // this navice implementation is sufficient here, not used very often
19
- const clone = (/** @type {any} */ obj) => JSON.parse(JSON.stringify(obj))
20
-
21
16
  /**
22
17
  * @param {import('./types.js').SchemaFragment} schemaFragment
23
18
  * @returns {Children}
24
19
  */
25
20
  function getDefaultChildren (schemaFragment) {
21
+ const { type } = getType(schemaFragment)
26
22
  /** @type {Children} */
27
23
  const children = []
28
- if (schemaFragment.type === 'object') {
24
+ if (type === 'object') {
29
25
  if (schemaFragment.properties) {
30
26
  for (const key of Object.keys(schemaFragment.properties)) {
31
27
  children.push({ key })
@@ -40,7 +36,7 @@ function getDefaultChildren (schemaFragment) {
40
36
  children.push({ key: '$oneOf' })
41
37
  }
42
38
  }
43
- if (schemaFragment.type === 'array' && Array.isArray(schemaFragment.items)) {
39
+ if (type === 'array' && Array.isArray(schemaFragment.items)) {
44
40
  for (let i = 0; i < schemaFragment.items.length; i++) {
45
41
  children.push({ key: i })
46
42
  }
@@ -85,81 +81,69 @@ function getChildren (defaultChildren, partialChildren) {
85
81
  }
86
82
 
87
83
  /**
84
+ * @param {PartialCompObject} partial
88
85
  * @param {SchemaFragment} schemaFragment
89
- * @param {string} schemaPath
90
- * @returns {import('./index.js').CompObject}
86
+ * @param {'oneOf'} [arrayChild]
87
+ * @returns {import('./index.js').ComponentName}
91
88
  */
92
- function getDefaultCompObject (schemaFragment, schemaPath) {
93
- const key = schemaPath.slice(schemaPath.lastIndexOf('/') + 1)
94
- if ('const' in schemaFragment) return { comp: 'none' }
95
- if (!schemaFragment.type) return { comp: 'none' }
96
- if (schemaFragment.type === 'object') {
97
- return { comp: 'section', title: schemaFragment.title ?? null, children: getDefaultChildren(schemaFragment) }
89
+ function getDefaultComp (partial, schemaFragment, arrayChild) {
90
+ const { type } = getType(schemaFragment)
91
+ const hasSimpleType = ['string', 'integer', 'number'].includes(type)
92
+ if (arrayChild === 'oneOf') return 'one-of-select'
93
+ if (hasSimpleType && schemaFragment.enum) return schemaFragment.enum.length > 20 ? 'autocomplete' : 'select'
94
+ if (hasSimpleType && schemaFragment.oneOf) return schemaFragment.oneOf.length > 20 ? 'autocomplete' : 'select'
95
+ if (hasSimpleType && schemaFragment.examples) return type === 'string' ? 'combobox' : 'number-combobox'
96
+ if (hasSimpleType && schemaFragment.anyOf && schemaFragment.anyOf.length && Object.keys(schemaFragment.anyOf[schemaFragment.anyOf.length - 1]).length === 0) {
97
+ return type === 'string' ? 'combobox' : 'number-combobox'
98
98
  }
99
- if (schemaFragment.type === 'array') {
100
- if (!schemaFragment.items) return { comp: 'none' }
101
- if (Array.isArray(schemaFragment.items)) {
102
- return { comp: 'section', title: schemaFragment.title ?? null, children: getDefaultChildren(schemaFragment) } // tuples
103
- }
104
- return {
105
- comp: 'list',
106
- title: schemaFragment.title ?? key,
107
- listEditMode: schemaFragment.items.type === 'object' ? 'inline-single' : 'inline',
108
- listActions: ['add', 'edit', 'delete', 'duplicate', 'sort']
99
+ if (partial.items) return partial.items.length > 20 ? 'autocomplete' : 'select'
100
+ if (partial.getItems) {
101
+ if (isPartialGetItemsFetch(partial.getItems)) {
102
+ if (partial.getItems.qSearchParam) return 'autocomplete'
103
+ if (typeof partial.getItems.url === 'string' && partial.getItems.url.includes('{q}')) return 'autocomplete'
109
104
  }
105
+ return 'select'
110
106
  }
111
- const hasSimpleType = ['string', 'integer', 'number'].includes(schemaFragment.type)
112
- if (schemaFragment.enum && hasSimpleType) {
113
- return {
114
- comp: 'select',
115
- label: schemaFragment.title ?? key,
116
- items: schemaFragment.enum.map((/** @type {string} */ value) => ({ key: value + '', title: value + '', value }))
107
+ if (type === 'array' && schemaFragment.items) {
108
+ const hasSimpleTypeItems = ['string', 'integer', 'number'].includes(schemaFragment.items.type)
109
+ if (hasSimpleTypeItems && (schemaFragment.items.enum || schemaFragment.items.oneOf)) {
110
+ return (schemaFragment.items.enum || schemaFragment.items.oneOf).length > 20 ? 'autocomplete' : 'select'
117
111
  }
118
- }
119
- if (schemaFragment.oneOf && !(schemaFragment.oneOf.find((/** @type {any} */ oneOfItem) => !('const' in oneOfItem))) && hasSimpleType) {
120
- return {
121
- comp: 'select',
122
- label: schemaFragment.title ?? key,
123
- items: schemaFragment.oneOf.map((/** @type {{ const: string; title: any; }} */ oneOfItem) => ({ key: oneOfItem.const + '', title: (oneOfItem.title ?? oneOfItem.const) + '', value: oneOfItem.const }))
112
+ if (hasSimpleTypeItems && schemaFragment.items.examples) {
113
+ return schemaFragment.items.type === 'string' ? 'combobox' : 'number-combobox'
124
114
  }
125
- }
126
- if (schemaFragment.type === 'string') {
127
- if (schemaFragment.format) {
128
- /** @type {CompObject | null} */
129
- let formatCompObject = null
130
- if (schemaFragment.format === 'date') formatCompObject = { comp: 'date-picker', label: schemaFragment.title ?? key, format: 'date' }
131
- if (schemaFragment.format === 'date-time') formatCompObject = { comp: 'date-time-picker', label: schemaFragment.title ?? key }
132
- if (schemaFragment.format === 'time') formatCompObject = { comp: 'time-picker', label: schemaFragment.title ?? key }
133
- if (formatCompObject) {
134
- if ('formatMinimum' in schemaFragment) formatCompObject.min = schemaFragment.formatMinimum
135
- if ('formatMaximum' in schemaFragment) formatCompObject.max = schemaFragment.formatMaximum
136
- return formatCompObject
137
- }
115
+ if (hasSimpleTypeItems && schemaFragment.items.anyOf && schemaFragment.items.anyOf.length && Object.keys(schemaFragment.items.anyOf[schemaFragment.items.anyOf.length - 1]).length === 0) {
116
+ return schemaFragment.items.type === 'string' ? 'combobox' : 'number-combobox'
117
+ }
118
+ if (hasSimpleTypeItems && !schemaFragment.items.layout && !['date', 'date-time', 'time'].includes(schemaFragment.items.format)) {
119
+ return schemaFragment.items.type === 'string' ? 'combobox' : 'number-combobox'
138
120
  }
139
- return { comp: 'text-field', label: schemaFragment.title ?? key }
140
121
  }
141
- if (schemaFragment.type === 'integer' || schemaFragment.type === 'number') {
142
- /** @type {import('./index.js').NumberField} */
143
- const compObject = { comp: 'number-field', label: schemaFragment.title ?? key }
144
- if (schemaFragment.type === 'integer') compObject.step = 1
145
- if ('minimum' in schemaFragment) compObject.min = schemaFragment.minimum
146
- if ('maximum' in schemaFragment) compObject.max = schemaFragment.maximum
147
- return compObject
122
+ if (type === 'object') return 'section'
123
+ if (type === 'array') {
124
+ if (Array.isArray(schemaFragment.items)) return 'section'
125
+ else return 'list'
126
+ }
127
+ if (type === 'string') {
128
+ if (schemaFragment.format === 'date') return 'date-picker'
129
+ if (schemaFragment.format === 'date-time') return 'date-time-picker'
130
+ if (schemaFragment.format === 'time') return 'time-picker'
131
+ return 'text-field'
148
132
  }
149
- if (schemaFragment.type === 'boolean') return { comp: 'checkbox', label: schemaFragment.title ?? key }
150
- console.warn(`failed to calculate default layout for schema ${schemaPath}`, schemaFragment)
151
- return { comp: 'none' }
133
+ if (type === 'integer' || type === 'number') return 'number-field'
134
+ if (type === 'boolean') return 'checkbox'
135
+ throw new Error('failed to calculate default component for schema fragment')
152
136
  }
153
137
 
154
138
  /**
155
139
  * @param {LayoutKeyword} layoutKeyword
156
- * @returns {PartialCompObject | null}
140
+ * @returns {PartialCompObject}
157
141
  */
158
142
  function getPartialCompObject (layoutKeyword) {
159
143
  if (isPartialCompObject(layoutKeyword)) return { ...layoutKeyword }
160
144
  else if (isComponentName(layoutKeyword)) return { comp: layoutKeyword }
161
145
  else if (isPartialChildren(layoutKeyword)) return { children: layoutKeyword }
162
- return null
146
+ return {}
163
147
  }
164
148
 
165
149
  /**
@@ -172,24 +156,151 @@ function normalizeExpression (expression, defaultType = 'js-eval') {
172
156
  else return { ...expression, type: expression.type ?? defaultType }
173
157
  }
174
158
 
159
+ /**
160
+ * @param {SchemaFragment} schemaFragment
161
+ * @returns {import('./index.js').PartialSelectItem[] | null}
162
+ */
163
+ function getItemsFromSchema (schemaFragment) {
164
+ if (!schemaFragment) return null
165
+ const { type } = getType(schemaFragment)
166
+ const hasSimpleType = ['string', 'integer', 'number'].includes(type)
167
+ if (schemaFragment.enum && hasSimpleType) {
168
+ return schemaFragment.enum.map((/** @type {string} */ value) => ({ key: value + '', title: value + '', value }))
169
+ }
170
+ if (schemaFragment.examples && hasSimpleType) {
171
+ return schemaFragment.examples.map((/** @type {string} */ value) => ({ key: value + '', title: value + '', value }))
172
+ }
173
+ if (schemaFragment.anyOf && hasSimpleType && schemaFragment.anyOf.length && Object.keys(schemaFragment.anyOf[schemaFragment.anyOf.length - 1]).length === 0) {
174
+ const nonEmptyAnyOf = schemaFragment.anyOf.slice(0, -1)
175
+ if (!(nonEmptyAnyOf.find((/** @type {any} */ oneOfItem) => !('const' in oneOfItem)))) {
176
+ return nonEmptyAnyOf.map((/** @type {{ const: string; title: any; }} */ anyOfItem) => ({
177
+ ...anyOfItem,
178
+ key: anyOfItem.const + '',
179
+ title: (anyOfItem.title ?? anyOfItem.const) + '',
180
+ value: anyOfItem.const
181
+ }))
182
+ }
183
+ }
184
+ if (schemaFragment.oneOf && hasSimpleType && !(schemaFragment.oneOf.find((/** @type {any} */ oneOfItem) => !('const' in oneOfItem)))) {
185
+ return schemaFragment.oneOf.map((/** @type {{ const: string; title: any; }} */ oneOfItem) => ({
186
+ ...oneOfItem,
187
+ key: oneOfItem.const + '',
188
+ title: (oneOfItem.title ?? oneOfItem.const) + '',
189
+ value: oneOfItem.const
190
+ }))
191
+ }
192
+ return null
193
+ }
194
+
195
+ /**
196
+ * @param {SchemaFragment} schemaFragment
197
+ * @returns {{type: string, nullable: boolean}}
198
+ */
199
+ const getType = (schemaFragment) => {
200
+ if (Array.isArray(schemaFragment.type) && schemaFragment.type.length === 2 && schemaFragment.type.includes('null')) {
201
+ const type = schemaFragment.type.find(t => t !== 'null')
202
+ return { type, nullable: true }
203
+ }
204
+ return { type: schemaFragment.type, nullable: false }
205
+ }
206
+
175
207
  /**
176
208
  * @param {LayoutKeyword} layoutKeyword
177
- * @param {CompObject} defaultCompObject
178
209
  * @param {SchemaFragment} schemaFragment
210
+ * @param {string} schemaPath
179
211
  * @param {(text: string) => string} markdown
180
- * @returns {CompObject}
212
+ * @param {'oneOf'} [arrayChild]
213
+ * @returns {{normalized: CompObject, errors: string[]}}
181
214
  */
182
- function getCompObject (layoutKeyword, defaultCompObject, schemaFragment, markdown) {
215
+ function getCompObject (layoutKeyword, schemaFragment, schemaPath, markdown, arrayChild) {
216
+ /** @type {string[]} */
217
+ const errors = []
218
+ const key = schemaPath.slice(schemaPath.lastIndexOf('/') + 1)
219
+
220
+ const { type, nullable } = getType(schemaFragment)
221
+
222
+ if ('const' in schemaFragment) return { normalized: { comp: 'none' }, errors }
223
+ if (!type) return { normalized: { comp: 'none' }, errors }
224
+ if (type === 'array' && !schemaFragment.items) return { normalized: { comp: 'none' }, errors }
225
+
183
226
  const partial = getPartialCompObject(layoutKeyword)
184
- if (!partial) return defaultCompObject
227
+
228
+ // chose the default component for a schema fragment
229
+ if (!partial.comp) {
230
+ try {
231
+ partial.comp = getDefaultComp(partial, schemaFragment, arrayChild)
232
+ } catch (/** @type {any} */err) {
233
+ errors.push(err.message)
234
+ partial.comp = 'none'
235
+ }
236
+ }
237
+ if (partial.comp === 'none') return { normalized: { comp: 'none' }, errors }
238
+
239
+ if (nullable) partial.nullable = nullable
240
+
241
+ // @ts-ignore
242
+ if (compositeCompNames.includes(partial.comp)) {
243
+ partial.title = partial.title ?? schemaFragment.title ?? null
244
+ partial.children = getChildren(getDefaultChildren(schemaFragment), partial.children)
245
+ } else if (partial.comp === 'list') {
246
+ partial.title = partial.title ?? schemaFragment.title ?? key
247
+ partial.listEditMode = partial.listEditMode ?? (schemaFragment.items.type === 'object' ? 'inline-single' : 'inline')
248
+ partial.listActions = partial.listActions ?? ['add', 'edit', 'delete', 'duplicate', 'sort']
249
+ } else {
250
+ partial.label = partial.label ?? schemaFragment.title ?? key
251
+ }
252
+
253
+ if (['select', 'autocomplete', 'combobox'].includes(partial.comp) && !partial.items) {
254
+ let items
255
+ if (type === 'array') {
256
+ items = getItemsFromSchema(schemaFragment.items)
257
+ partial.multiple = true
258
+ } else {
259
+ items = getItemsFromSchema(schemaFragment)
260
+ }
261
+ if (items) {
262
+ if (partial.getItems && isPartialGetItemsObj(partial.getItems)) {
263
+ partial.getItems.expr = JSON.stringify(items)
264
+ } else {
265
+ partial.getItems = JSON.stringify(items)
266
+ }
267
+ }
268
+ }
269
+
270
+ if (['combobox', 'number-combobox'].includes(partial.comp)) {
271
+ if (type === 'array') {
272
+ partial.multiple = true
273
+ }
274
+ }
275
+
276
+ if (partial.comp === 'date-picker') {
277
+ if (schemaFragment.format === 'date') partial.format = 'date'
278
+ if (schemaFragment.format === 'date-time') partial.format = 'date-time'
279
+ }
280
+
281
+ if (['date-picker', 'date-time-picker', 'time-picker'].includes(partial.comp)) {
282
+ if ('formatMinimum' in schemaFragment) partial.min = partial.min ?? schemaFragment.formatMinimum
283
+ if ('formatMaximum' in schemaFragment) partial.max = partial.max ?? schemaFragment.formatMaximum
284
+ }
285
+
286
+ if (['number-field', 'slider'].includes(partial.comp)) {
287
+ if (type === 'integer') partial.step = partial.step ?? 1
288
+ if ('minimum' in schemaFragment) partial.min = partial.min ?? schemaFragment.minimum
289
+ if ('maximum' in schemaFragment) partial.max = partial.max ?? schemaFragment.maximum
290
+ }
185
291
 
186
292
  if (partial.if) partial.if = normalizeExpression(partial.if)
293
+
294
+ if (partial.defaultData) partial.defaultData = normalizeExpression(partial.defaultData)
295
+ if (partial.constData) partial.constData = normalizeExpression(partial.constData)
296
+
187
297
  if (partial.getItems && isPartialGetItemsExpr(partial.getItems)) partial.getItems = normalizeExpression(partial.getItems)
188
298
  if (partial.getItems && isPartialGetItemsObj(partial.getItems)) {
189
- if (schemaFragment.type === 'object') partial.getItems.returnObjects = true
299
+ if (type === 'object') partial.getItems.returnObjects = true
190
300
  if (partial.getItems.itemTitle) partial.getItems.itemTitle = normalizeExpression(partial.getItems.itemTitle)
191
301
  if (partial.getItems.itemKey) partial.getItems.itemKey = normalizeExpression(partial.getItems.itemKey)
192
302
  if (partial.getItems.itemValue) partial.getItems.itemValue = normalizeExpression(partial.getItems.itemValue)
303
+ if (partial.getItems.itemIcon) partial.getItems.itemIcon = normalizeExpression(partial.getItems.itemIcon)
193
304
  if (partial.getItems.itemsResults) partial.getItems.itemsResults = normalizeExpression(partial.getItems.itemsResults)
194
305
  }
195
306
  if (partial.getItems && isPartialGetItemsFetch(partial.getItems)) {
@@ -236,48 +347,70 @@ function getCompObject (layoutKeyword, defaultCompObject, schemaFragment, markdo
236
347
  }
237
348
  }
238
349
 
239
- /** @type {any} */
240
- const compObject = {}
241
- if (partial.comp && defaultCompObject.comp !== partial.comp) {
242
- const compProperties = normalizedLayoutSchema.$defs[partial.comp]?.properties
243
- if (typeof compProperties === 'object') {
244
- for (const key of Object.keys(compProperties).concat(['if', 'help', 'cols', 'props', 'slots', 'options'])) {
245
- if (key in defaultCompObject) compObject[key] = defaultCompObject[key]
246
- if (key in partial) compObject[key] = partial[key]
247
- }
248
- }
249
- } else {
250
- Object.assign(compObject, defaultCompObject, partial)
251
- }
350
+ if (schemaFragment.description && !partial.help) partial.help = schemaFragment.description
351
+ if (partial.help) partial.help = markdown(partial.help).trim()
252
352
 
253
- if (isSectionLayout(defaultCompObject) && isCompositeLayout(compObject)) {
254
- compObject.children = getChildren((defaultCompObject).children, partial.children)
255
- }
256
-
257
- if (compObject.description && !compObject.help) compObject.help = compObject.description
258
- if (compObject.help) compObject.help = markdown(compObject.help).trim()
259
-
260
- if (typeof compObject.cols === 'number') compObject.cols = { xs: compObject.cols }
261
- if (typeof compObject.cols === 'object' && compObject.cols.xs === undefined) compObject.cols.xs = 12
353
+ if (typeof partial.cols === 'number') partial.cols = { xs: partial.cols }
354
+ if (typeof partial.cols === 'object' && partial.cols.xs === undefined) partial.cols.xs = 12
262
355
 
263
- return compObject
356
+ return { normalized: /** @type {CompObject} */(partial), errors }
264
357
  }
265
358
 
266
359
  /**
267
360
  * @param {LayoutKeyword} layoutKeyword
268
- * @param {CompObject} defaultCompObject
269
361
  * @param {SchemaFragment} schemaFragment
362
+ * @param {string} schemaPath
270
363
  * @param {(text: string) => string} markdown
271
- * @returns {NormalizedLayout}
364
+ * @param {'oneOf'} [arrayChild]
365
+ * @returns {{normalized: NormalizedLayout, errors: string[]}}
272
366
  */
273
- function getNormalizedLayout (layoutKeyword, defaultCompObject, schemaFragment, markdown) {
367
+ function getNormalizedLayout (layoutKeyword, schemaFragment, schemaPath, markdown, arrayChild) {
274
368
  if (isPartialSwitch(layoutKeyword)) {
275
- const switchCases = layoutKeyword.switch.map(layout => getCompObject(layout, clone(defaultCompObject), schemaFragment, markdown))
276
- if (!switchCases.find(switchCase => !switchCase.if)) switchCases.push(clone(defaultCompObject))
277
- return { switch: switchCases }
369
+ /** @type {CompObject[]} */
370
+ const normalizedSwitchCases = []
371
+ const errors = []
372
+ const switchCases = [...layoutKeyword.switch]
373
+ if (!switchCases.find(s => !s.if)) {
374
+ switchCases.push({})
375
+ }
376
+ for (let i = 0; i < switchCases.length; i++) {
377
+ const switchCase = switchCases[i]
378
+ const compObjectResult = getCompObject(switchCase, schemaFragment, schemaPath, markdown, arrayChild)
379
+ normalizedSwitchCases.push(compObjectResult.normalized)
380
+ for (const error of compObjectResult.errors) errors.push(`switch ${i} - ${error}`)
381
+ }
382
+ return { normalized: { switch: normalizedSwitchCases }, errors: [] }
278
383
  } else {
279
- return getCompObject(layoutKeyword, defaultCompObject, schemaFragment, markdown)
384
+ return getCompObject(layoutKeyword, schemaFragment, schemaPath, markdown, arrayChild)
385
+ }
386
+ }
387
+
388
+ /**
389
+ * @param {import('ajv').ErrorObject} error
390
+ * @param {(error: import('ajv').ErrorObject) => boolean} fn
391
+ * @returns {boolean}
392
+ */
393
+ function matchValidationError (error, fn) {
394
+ if (error.keyword === 'errorMessage') {
395
+ error = error.params.errors[0]
280
396
  }
397
+ return fn(error)
398
+ }
399
+
400
+ /**
401
+ * @param {import('ajv').ErrorObject[]} errors
402
+ * @returns {string[]}
403
+ */
404
+ function lighterValidationErrors (errors) {
405
+ const compositeErrors = errors.filter(e => matchValidationError(e, (e) => e.keyword === 'anyOf' || e.keyword === 'oneOf'))
406
+ // in case of a anyOf/oneOf error there are some subschemas errors that mostly prevent readability
407
+ for (const compositeError of compositeErrors) {
408
+ const explicitError = errors.find(e => matchValidationError(e, (e) => e.instancePath === compositeError.instancePath && e.keyword !== 'type'))
409
+ if (explicitError) {
410
+ errors = errors.filter(e => matchValidationError(e, (e) => e.instancePath !== compositeError.instancePath || e.keyword !== 'type'))
411
+ }
412
+ }
413
+ return errors.map(e => e.message ?? e.keyword)
281
414
  }
282
415
 
283
416
  /**
@@ -285,29 +418,30 @@ function getNormalizedLayout (layoutKeyword, defaultCompObject, schemaFragment,
285
418
  * @param {string} schemaPath
286
419
  * @param {(text: string) => string} markdown
287
420
  * @param {'oneOf'} [arrayChild]
288
- * @returns {NormalizedLayout}
421
+ * @returns {{layout: NormalizedLayout, errors: string[]}}
289
422
  */
290
423
  export function normalizeLayoutFragment (schemaFragment, schemaPath, markdown = (src) => src, arrayChild) {
291
424
  let layoutKeyword
292
- /** @type {CompObject} */
293
- let defaultCompObject
294
425
  if (arrayChild === 'oneOf') {
295
426
  layoutKeyword = schemaFragment.oneOfLayout ?? {}
296
- defaultCompObject = { comp: 'one-of-select' } // TODO: default label ?
297
427
  } else {
298
428
  layoutKeyword = schemaFragment.layout ?? {}
299
- defaultCompObject = getDefaultCompObject(schemaFragment, schemaPath)
300
429
  }
301
430
  if (!validateLayoutKeyword(layoutKeyword)) {
302
431
  console.error(`layout keyword validation errors at path ${schemaPath}`, layoutKeyword, validateLayoutKeyword.errors)
303
- return defaultCompObject
304
- // throw new Error(`invalid layout keyword at path ${schemaPath}`, { cause: validateLayoutKeyword.errors })
432
+ return {
433
+ layout: getNormalizedLayout({}, schemaFragment, schemaPath, markdown, arrayChild).normalized,
434
+ errors: lighterValidationErrors(validateLayoutKeyword.errors)
435
+ }
305
436
  }
306
- const normalizedLayout = getNormalizedLayout(layoutKeyword, defaultCompObject, schemaFragment, markdown)
307
- if (!validateNormalizedLayout(normalizedLayout)) {
437
+ const normalizedLayout = getNormalizedLayout(layoutKeyword, schemaFragment, schemaPath, markdown, arrayChild)
438
+ if (!validateNormalizedLayout(normalizedLayout.normalized)) {
308
439
  console.error(`normalized layout validation errors at path ${schemaPath}`, normalizedLayout, validateNormalizedLayout.errors)
309
- return defaultCompObject
440
+ return {
441
+ layout: getNormalizedLayout({}, schemaFragment, schemaPath, markdown, arrayChild).normalized,
442
+ errors: lighterValidationErrors(validateNormalizedLayout.errors)
443
+ }
310
444
  // throw new Error(`invalid layout at path ${schemaPath}`, { cause: validateNormalizedLayout.errors })
311
445
  }
312
- return normalizedLayout
446
+ return { layout: normalizedLayout.normalized, errors: [] }
313
447
  }
@@ -9,6 +9,7 @@ import schema from './schema.js'
9
9
  * @typedef {import('./types.js').GetItems} GetItems
10
10
  * @typedef {import('./types.js').Expression} Expression
11
11
  * @typedef {import('./types.js').Select} Select
12
+ * @typedef {import('./types.js').Autocomplete} Autocomplete
12
13
  * @typedef {import('./types.js').SelectItem} SelectItem
13
14
  * @typedef {import('./types.js').SelectItems} SelectItems
14
15
  * @typedef {import('./types.js').GetItemsFetch} GetItemsFetch
@@ -29,7 +30,9 @@ import schema from './schema.js'
29
30
  * @typedef {import('./types.js').Tabs} Tabs
30
31
  * @typedef {import('./types.js').VerticalTabs} VerticalTabs
31
32
  * @typedef {import('./types.js').ExpansionPanels} ExpansionPanels
33
+ * @typedef {import('./types.js').Stepper} Stepper
32
34
  * @typedef {import('./types.js').List} List
35
+ * @typedef {import('./types.js').Combobox} Combobox
33
36
  * @typedef {import('./types.js').Cols} Cols
34
37
  * @typedef {import('./types.js').ColsObj} ColsObj
35
38
  * @typedef {import('./types.js').StateNodeOptions} StateNodeOptions
@@ -61,9 +64,11 @@ export function isSectionLayout (layout) {
61
64
  return layout.comp === 'section'
62
65
  }
63
66
 
67
+ export const compositeCompNames = ['section', 'tabs', 'vertical-tabs', 'expansion-panels', 'stepper']
68
+
64
69
  /** @type {(layout: CompObject) => layout is CompositeCompObject} */
65
70
  export function isCompositeLayout (layout) {
66
- return ['section', 'tabs', 'vertical-tabs', 'expansion-panels'].includes(layout.comp)
71
+ return compositeCompNames.includes(layout.comp)
67
72
  }
68
73
 
69
74
  /** @type {(layout: CompObject) => layout is TextField} */
@@ -76,6 +81,16 @@ export function isSelectLayout (layout) {
76
81
  return layout.comp === 'select'
77
82
  }
78
83
 
84
+ /** @type {(layout: CompObject) => layout is CompObject & {autofocus: boolean}} */
85
+ export function isFocusableLayout (layout) {
86
+ return ['text-field', 'number-field', 'textarea', 'select', 'combobox', 'number-combobox', 'autocomplete', 'markdown'].includes(layout.comp)
87
+ }
88
+
89
+ /** @type {(layout: CompObject) => layout is Select | Combobox | Autocomplete} */
90
+ export function isItemsLayout (layout) {
91
+ return layout.comp === 'select' || layout.comp === 'combobox' || layout.comp === 'autocomplete'
92
+ }
93
+
79
94
  /** @type {(getItems: GetItems) => getItems is Expression} */
80
95
  export function isGetItemsExpression (getItems) {
81
96
  return !!getItems.expr