@json-layout/core 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/package.json +4 -3
- package/src/compile/index.js +14 -2
- package/src/compile/serialize.js +3 -2
- package/src/compile/skeleton-node.js +49 -18
- package/src/compile/skeleton-tree.js +3 -1
- package/src/compile/types.ts +3 -4
- package/src/i18n/en.js +23 -1
- package/src/i18n/fr.js +23 -1
- package/src/i18n/types.ts +22 -0
- package/src/state/index.js +115 -22
- package/src/state/state-node.js +88 -28
- package/src/state/state-tree.js +4 -4
- package/src/state/types.ts +38 -1
- package/types/compile/index.d.ts.map +1 -1
- package/types/compile/serialize.d.ts.map +1 -1
- package/types/compile/skeleton-node.d.ts +2 -1
- package/types/compile/skeleton-node.d.ts.map +1 -1
- package/types/compile/skeleton-tree.d.ts +2 -1
- package/types/compile/skeleton-tree.d.ts.map +1 -1
- package/types/compile/types.d.ts +2 -3
- package/types/compile/types.d.ts.map +1 -1
- package/types/i18n/types.d.ts +22 -0
- package/types/i18n/types.d.ts.map +1 -1
- package/types/state/index.d.ts +37 -4
- package/types/state/index.d.ts.map +1 -1
- package/types/state/state-node.d.ts.map +1 -1
- package/types/state/state-tree.d.ts +2 -2
- package/types/state/state-tree.d.ts.map +1 -1
- package/types/state/types.d.ts +20 -1
- package/types/state/types.d.ts.map +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@json-layout/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Compilation and state management utilities for JSON Layout.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
},
|
|
20
20
|
"files": [
|
|
21
21
|
"src",
|
|
22
|
-
"types"
|
|
22
|
+
"types",
|
|
23
|
+
"LICENSE"
|
|
23
24
|
],
|
|
24
25
|
"scripts": {
|
|
25
26
|
"test:single": "node --no-experimental-fetch --test --test-only test/*.spec.js",
|
|
@@ -46,7 +47,7 @@
|
|
|
46
47
|
},
|
|
47
48
|
"homepage": "https://github.com/json-layout/json-layout#readme",
|
|
48
49
|
"dependencies": {
|
|
49
|
-
"@json-layout/vocabulary": "^0.
|
|
50
|
+
"@json-layout/vocabulary": "^0.3.0",
|
|
50
51
|
"@types/markdown-it": "^13.0.1",
|
|
51
52
|
"ajv": "^8.12.0",
|
|
52
53
|
"ajv-errors": "^3.0.0",
|
package/src/compile/index.js
CHANGED
|
@@ -27,7 +27,7 @@ const Ajv = /** @type {typeof ajvModule.default} */ (ajvModule)
|
|
|
27
27
|
// @ts-ignore
|
|
28
28
|
const ajvLocalize = /** @type {typeof ajvLocalizeModule.default} */ (ajvLocalizeModule)
|
|
29
29
|
|
|
30
|
-
const expressionsParams = ['data', 'options', 'display']
|
|
30
|
+
const expressionsParams = ['data', 'options', 'context', 'display']
|
|
31
31
|
|
|
32
32
|
const clone = rfdc()
|
|
33
33
|
// const exprEvalParser = new ExprEvalParser()
|
|
@@ -87,8 +87,19 @@ export function compile (_schema, partialOptions = {}) {
|
|
|
87
87
|
const normalizedLayouts = {}
|
|
88
88
|
/** @type {import('@json-layout/vocabulary').Expression[]} */
|
|
89
89
|
const expressionsDefinitions = []
|
|
90
|
+
/** @type {Record<string, string[]>} */
|
|
91
|
+
const validationErrors = {}
|
|
90
92
|
|
|
91
|
-
const skeletonTree = makeSkeletonTree(
|
|
93
|
+
const skeletonTree = makeSkeletonTree(
|
|
94
|
+
schema,
|
|
95
|
+
options,
|
|
96
|
+
validatePointers,
|
|
97
|
+
validationErrors,
|
|
98
|
+
normalizedLayouts,
|
|
99
|
+
expressionsDefinitions,
|
|
100
|
+
`${schema.$id}#`,
|
|
101
|
+
'main'
|
|
102
|
+
)
|
|
92
103
|
|
|
93
104
|
options.ajv.addSchema(schema)
|
|
94
105
|
|
|
@@ -126,6 +137,7 @@ export function compile (_schema, partialOptions = {}) {
|
|
|
126
137
|
schema,
|
|
127
138
|
skeletonTree,
|
|
128
139
|
validates,
|
|
140
|
+
validationErrors,
|
|
129
141
|
normalizedLayouts,
|
|
130
142
|
expressions,
|
|
131
143
|
locale: options.locale,
|
package/src/compile/serialize.js
CHANGED
|
@@ -26,7 +26,7 @@ export function serialize (compiledLayout) {
|
|
|
26
26
|
|
|
27
27
|
code = code.replace('"use strict";', '')
|
|
28
28
|
|
|
29
|
-
// some internal imports to ajv are not translated to
|
|
29
|
+
// some internal imports to ajv are not translated to esm, we do it here
|
|
30
30
|
// cf https://github.com/ajv-validator/ajv-formats/pull/73
|
|
31
31
|
if (code.includes('require("ajv-formats/dist/formats")')) {
|
|
32
32
|
code = 'import { fullFormats } from "ajv-formats/dist/formats.js";\n' + code
|
|
@@ -37,7 +37,7 @@ export function serialize (compiledLayout) {
|
|
|
37
37
|
code = code.replace(/require\("ajv\/dist\/runtime\/ucs2length"\)/g, 'ucs2length')
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
//
|
|
40
|
+
// import only the current locale from ajv-i18n
|
|
41
41
|
code = `import localizeErrors from "ajv-i18n/localize/${compiledLayout.locale}/index.js";
|
|
42
42
|
export const exportLocalizeErrors = localizeErrors;\n` + code
|
|
43
43
|
|
|
@@ -55,6 +55,7 @@ export const exportLocalizeErrors = localizeErrors;\n` + code
|
|
|
55
55
|
skeletonTree: compiledLayout.skeletonTree,
|
|
56
56
|
normalizedLayouts: compiledLayout.normalizedLayouts,
|
|
57
57
|
validates: {},
|
|
58
|
+
validationErrors: compiledLayout.validationErrors,
|
|
58
59
|
expressions: expressionsNodes,
|
|
59
60
|
locale: compiledLayout.locale,
|
|
60
61
|
messages: compiledLayout.messages,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// import Debug from 'debug'
|
|
2
|
-
import { normalizeLayoutFragment, isSwitchStruct, isGetItemsExpression,
|
|
2
|
+
import { normalizeLayoutFragment, isSwitchStruct, isGetItemsExpression, isGetItemsFetch, isItemsLayout } from '@json-layout/vocabulary'
|
|
3
3
|
import { makeSkeletonTree } from './skeleton-tree.js'
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -20,6 +20,7 @@ const pushExpression = (expressions, expression) => {
|
|
|
20
20
|
* @param {any} schema
|
|
21
21
|
* @param {import('./index.js').CompileOptions} options
|
|
22
22
|
* @param {string[]} validates
|
|
23
|
+
* @param {Record<string, string[]>} validationErrors
|
|
23
24
|
* @param {Record<string, import('@json-layout/vocabulary').NormalizedLayout>} normalizedLayouts
|
|
24
25
|
* @param {import('@json-layout/vocabulary').Expression[]} expressions
|
|
25
26
|
* @param {string | number} key
|
|
@@ -32,6 +33,7 @@ export function makeSkeletonNode (
|
|
|
32
33
|
schema,
|
|
33
34
|
options,
|
|
34
35
|
validates,
|
|
36
|
+
validationErrors,
|
|
35
37
|
normalizedLayouts,
|
|
36
38
|
expressions,
|
|
37
39
|
key,
|
|
@@ -44,36 +46,46 @@ export function makeSkeletonNode (
|
|
|
44
46
|
|
|
45
47
|
// improve on ajv error messages based on ajv-errors (https://ajv.js.org/packages/ajv-errors.html)
|
|
46
48
|
schema.errorMessage = schema.errorMessage ?? {}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
49
|
+
if (!normalizedLayouts[pointer]) {
|
|
50
|
+
const normalizationResult = normalizeLayoutFragment(/** @type {import('@json-layout/vocabulary').SchemaFragment} */(schema), pointer, options.markdown)
|
|
51
|
+
normalizedLayouts[pointer] = normalizationResult.layout
|
|
52
|
+
if (normalizationResult.errors.length) {
|
|
53
|
+
validationErrors[pointer.replace('_jl#', '/')] = normalizationResult.errors
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const normalizedLayout = normalizedLayouts[pointer]
|
|
57
|
+
|
|
58
|
+
let defaultData
|
|
59
|
+
if ('default' in schema) defaultData = schema.default
|
|
60
|
+
else if (required) {
|
|
61
|
+
if (schema.type === 'object') defaultData = {}
|
|
62
|
+
if (schema.type === 'array') defaultData = []
|
|
63
|
+
}
|
|
50
64
|
|
|
51
65
|
const compObjects = isSwitchStruct(normalizedLayout) ? normalizedLayout.switch : [normalizedLayout]
|
|
52
66
|
for (const compObject of compObjects) {
|
|
53
67
|
if (schema.description && !compObject.help) compObject.help = schema.description
|
|
54
68
|
if (compObject.if) pushExpression(expressions, compObject.if)
|
|
55
|
-
|
|
69
|
+
|
|
70
|
+
if ('const' in schema) compObject.constData = { type: 'js-eval', expr: JSON.stringify(schema.const) }
|
|
71
|
+
if (compObject.constData) pushExpression(expressions, compObject.constData)
|
|
72
|
+
|
|
73
|
+
if (defaultData && !compObject.defaultData) compObject.defaultData = { type: 'js-eval', expr: JSON.stringify(defaultData) }
|
|
74
|
+
if (compObject.defaultData) pushExpression(expressions, compObject.defaultData)
|
|
75
|
+
|
|
76
|
+
if (isItemsLayout(compObject) && compObject.getItems) {
|
|
56
77
|
if (isGetItemsExpression(compObject.getItems)) pushExpression(expressions, compObject.getItems)
|
|
57
78
|
if (isGetItemsFetch(compObject.getItems)) pushExpression(expressions, compObject.getItems.url)
|
|
58
79
|
if (compObject.getItems.itemTitle) pushExpression(expressions, compObject.getItems.itemTitle)
|
|
59
80
|
if (compObject.getItems.itemKey) pushExpression(expressions, compObject.getItems.itemKey)
|
|
60
81
|
if (compObject.getItems.itemValue) pushExpression(expressions, compObject.getItems.itemValue)
|
|
82
|
+
if (compObject.getItems.itemIcon) pushExpression(expressions, compObject.getItems.itemIcon)
|
|
61
83
|
if (compObject.getItems.itemsResults) pushExpression(expressions, compObject.getItems.itemsResults)
|
|
62
84
|
}
|
|
63
85
|
}
|
|
64
86
|
|
|
65
|
-
let defaultData
|
|
66
|
-
if (schema.const) defaultData = schema.const
|
|
67
|
-
else if (schema.default) defaultData = schema.default
|
|
68
|
-
if (required) {
|
|
69
|
-
if (schema.type === 'object') defaultData = {} // TODO: this is only true if property is required ?
|
|
70
|
-
if (schema.type === 'array') defaultData = []
|
|
71
|
-
if (schema.type === 'string' && !schema.format) defaultData = ''
|
|
72
|
-
}
|
|
73
|
-
|
|
74
87
|
/** @type {import('./types.js').SkeletonNode} */
|
|
75
|
-
const node = { key: key ?? '', pointer, parentPointer
|
|
76
|
-
if (schema.const) node.const = schema.const
|
|
88
|
+
const node = { key: key ?? '', pointer, parentPointer }
|
|
77
89
|
if (schema.type === 'object') {
|
|
78
90
|
if (schema.properties) {
|
|
79
91
|
node.children = node.children ?? []
|
|
@@ -82,6 +94,7 @@ export function makeSkeletonNode (
|
|
|
82
94
|
schema.properties[propertyKey],
|
|
83
95
|
options,
|
|
84
96
|
validates,
|
|
97
|
+
validationErrors,
|
|
85
98
|
normalizedLayouts,
|
|
86
99
|
expressions,
|
|
87
100
|
propertyKey,
|
|
@@ -102,6 +115,7 @@ export function makeSkeletonNode (
|
|
|
102
115
|
schema.allOf[i],
|
|
103
116
|
options,
|
|
104
117
|
validates,
|
|
118
|
+
validationErrors,
|
|
105
119
|
normalizedLayouts,
|
|
106
120
|
expressions,
|
|
107
121
|
`$allOf-${i}`,
|
|
@@ -113,14 +127,29 @@ export function makeSkeletonNode (
|
|
|
113
127
|
}
|
|
114
128
|
if (schema.oneOf) {
|
|
115
129
|
const oneOfPointer = `${pointer}/oneOf`
|
|
116
|
-
|
|
130
|
+
if (!normalizedLayouts[oneOfPointer]) {
|
|
131
|
+
const normalizationResult = normalizeLayoutFragment(schema, oneOfPointer, options.markdown, 'oneOf')
|
|
132
|
+
normalizedLayouts[oneOfPointer] = normalizationResult.layout
|
|
133
|
+
if (normalizationResult.errors.length) {
|
|
134
|
+
validationErrors[oneOfPointer.replace('_jl#', '/')] = normalizationResult.errors
|
|
135
|
+
}
|
|
136
|
+
}
|
|
117
137
|
/** @type {import('./types.js').SkeletonTree[]} */
|
|
118
138
|
const childrenTrees = []
|
|
119
139
|
for (let i = 0; i < schema.oneOf.length; i++) {
|
|
120
140
|
if (!schema.oneOf[i].type) schema.oneOf[i].type = schema.type
|
|
121
141
|
const title = schema.oneOf[i].title ?? `option ${i}`
|
|
122
142
|
delete schema.oneOf[i].title
|
|
123
|
-
childrenTrees.push(makeSkeletonTree(
|
|
143
|
+
childrenTrees.push(makeSkeletonTree(
|
|
144
|
+
schema.oneOf[i],
|
|
145
|
+
options,
|
|
146
|
+
validates,
|
|
147
|
+
validationErrors,
|
|
148
|
+
normalizedLayouts,
|
|
149
|
+
expressions,
|
|
150
|
+
`${oneOfPointer}/${i}`,
|
|
151
|
+
title
|
|
152
|
+
))
|
|
124
153
|
}
|
|
125
154
|
node.children = node.children ?? []
|
|
126
155
|
node.children.push({ key: '$oneOf', pointer: `${pointer}/oneOf`, parentPointer: pointer, childrenTrees })
|
|
@@ -136,6 +165,7 @@ export function makeSkeletonNode (
|
|
|
136
165
|
itemSchema,
|
|
137
166
|
options,
|
|
138
167
|
validates,
|
|
168
|
+
validationErrors,
|
|
139
169
|
normalizedLayouts,
|
|
140
170
|
expressions,
|
|
141
171
|
i,
|
|
@@ -150,6 +180,7 @@ export function makeSkeletonNode (
|
|
|
150
180
|
schema.items,
|
|
151
181
|
options,
|
|
152
182
|
validates,
|
|
183
|
+
validationErrors,
|
|
153
184
|
normalizedLayouts,
|
|
154
185
|
expressions,
|
|
155
186
|
`${pointer}/items`,
|
|
@@ -7,6 +7,7 @@ import { makeSkeletonNode } from './skeleton-node.js'
|
|
|
7
7
|
* @param {any} schema
|
|
8
8
|
* @param {import('./index.js').CompileOptions} options
|
|
9
9
|
* @param {string[]} validates
|
|
10
|
+
* @param {Record<string, string[]>} validationErrors
|
|
10
11
|
* @param {Record<string, import('@json-layout/vocabulary').NormalizedLayout>} normalizedLayouts
|
|
11
12
|
* @param {import('@json-layout/vocabulary').Expression[]} expressions
|
|
12
13
|
* @param {string} pointer
|
|
@@ -17,12 +18,13 @@ export function makeSkeletonTree (
|
|
|
17
18
|
schema,
|
|
18
19
|
options,
|
|
19
20
|
validates,
|
|
21
|
+
validationErrors,
|
|
20
22
|
normalizedLayouts,
|
|
21
23
|
expressions,
|
|
22
24
|
pointer,
|
|
23
25
|
title
|
|
24
26
|
) {
|
|
25
|
-
const root = makeSkeletonNode(schema, options, validates, normalizedLayouts, expressions, '', pointer, null, true)
|
|
27
|
+
const root = makeSkeletonNode(schema, options, validates, validationErrors, normalizedLayouts, expressions, '', pointer, null, true)
|
|
26
28
|
validates.push(pointer)
|
|
27
29
|
return { title, root }
|
|
28
30
|
}
|
package/src/compile/types.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import type ajvModule from 'ajv'
|
|
2
2
|
import type MarkdownIt from 'markdown-it'
|
|
3
3
|
import { type NormalizedLayout, type StateNodeOptions } from '@json-layout/vocabulary'
|
|
4
|
-
import { type ValidateFunction, type SchemaObject } from 'ajv'
|
|
4
|
+
import { type ValidateFunction, type SchemaObject, type ErrorObject } from 'ajv'
|
|
5
5
|
import { type Display } from '../state/utils/display.js'
|
|
6
6
|
import { type LocaleMessages } from '../i18n/types.js'
|
|
7
7
|
|
|
8
|
-
export type CompiledExpression = (data: any, options: StateNodeOptions, display: Display) => any
|
|
8
|
+
export type CompiledExpression = (data: any, options: StateNodeOptions, context: object, display: Display) => any
|
|
9
9
|
|
|
10
10
|
export interface CompileOptions {
|
|
11
11
|
ajv: ajvModule.default
|
|
@@ -23,6 +23,7 @@ export interface CompiledLayout {
|
|
|
23
23
|
schema?: SchemaObject
|
|
24
24
|
skeletonTree: SkeletonTree
|
|
25
25
|
validates: Record<string, ValidateFunction>
|
|
26
|
+
validationErrors: Record<string, string[]>
|
|
26
27
|
normalizedLayouts: Record<string, NormalizedLayout>
|
|
27
28
|
expressions: CompiledExpression[]
|
|
28
29
|
locale: string
|
|
@@ -43,8 +44,6 @@ export interface SkeletonNode {
|
|
|
43
44
|
key: string | number
|
|
44
45
|
pointer: string
|
|
45
46
|
parentPointer: string | null
|
|
46
|
-
defaultData?: unknown
|
|
47
|
-
const?: unknown
|
|
48
47
|
children?: SkeletonNode[] // optional children in the case of arrays and object nodes
|
|
49
48
|
childrenTrees?: SkeletonTree[] // other trees that can be instantiated with separate validation (for example in the case of new array items of oneOfs, etc)
|
|
50
49
|
}
|
package/src/i18n/en.js
CHANGED
|
@@ -6,5 +6,27 @@ export default {
|
|
|
6
6
|
delete: 'Delete',
|
|
7
7
|
edit: 'Edit',
|
|
8
8
|
duplicate: 'Duplicate',
|
|
9
|
-
sort: 'Sort'
|
|
9
|
+
sort: 'Sort',
|
|
10
|
+
up: 'Move up',
|
|
11
|
+
down: 'Move down',
|
|
12
|
+
showHelp: 'Show a help message',
|
|
13
|
+
mdeLink1: '[Link title',
|
|
14
|
+
mdeLink2: '](link url)',
|
|
15
|
+
mdeImg1: '',
|
|
17
|
+
mdeTable1: '',
|
|
18
|
+
mdeTable2: '\n\n| Column 1 | Column 2 | ColoColumnnne 3 |\n| -------- | -------- | -------- |\n| Text | Text | Text |\n\n',
|
|
19
|
+
bold: 'Bold',
|
|
20
|
+
italic: 'Italic',
|
|
21
|
+
heading: 'Title',
|
|
22
|
+
quote: 'Quote',
|
|
23
|
+
unorderedList: 'Unordered list',
|
|
24
|
+
orderedList: 'Ordered list',
|
|
25
|
+
createLink: 'Create a link',
|
|
26
|
+
insertImage: 'Insert an image',
|
|
27
|
+
createTable: 'Create a table',
|
|
28
|
+
preview: 'Aperçu du rendu',
|
|
29
|
+
mdeGuide: 'Documentation de la syntaxe',
|
|
30
|
+
undo: 'Undo',
|
|
31
|
+
redo: 'Redo'
|
|
10
32
|
}
|
package/src/i18n/fr.js
CHANGED
|
@@ -6,5 +6,27 @@ export default {
|
|
|
6
6
|
delete: 'Supprimer',
|
|
7
7
|
edit: 'Éditer',
|
|
8
8
|
duplicate: 'Dupliquer',
|
|
9
|
-
sort: 'Trier'
|
|
9
|
+
sort: 'Trier',
|
|
10
|
+
up: 'Décaler vers le haut',
|
|
11
|
+
down: 'Décaler vers le bas',
|
|
12
|
+
showHelp: 'Afficher un message d\'aide',
|
|
13
|
+
mdeLink1: '[titre du lien',
|
|
14
|
+
mdeLink2: '](adresse du lien)',
|
|
15
|
+
mdeImg1: '',
|
|
17
|
+
mdeTable1: '',
|
|
18
|
+
mdeTable2: '\n\n| Colonne 1 | Colonne 2 | Colonne 3 |\n| -------- | -------- | -------- |\n| Texte | Texte | Texte |\n\n',
|
|
19
|
+
bold: 'Gras',
|
|
20
|
+
italic: 'Italique',
|
|
21
|
+
heading: 'Titre',
|
|
22
|
+
quote: 'Citation',
|
|
23
|
+
unorderedList: 'Liste à puce',
|
|
24
|
+
orderedList: 'Liste numérotée',
|
|
25
|
+
createLink: 'Créer un lien',
|
|
26
|
+
insertImage: 'Insérer une image',
|
|
27
|
+
createTable: 'Créer un tableau',
|
|
28
|
+
preview: 'Preview',
|
|
29
|
+
mdeGuide: 'Syntax documentation',
|
|
30
|
+
undo: 'Défaire',
|
|
31
|
+
redo: 'Refaire'
|
|
10
32
|
}
|
package/src/i18n/types.ts
CHANGED
|
@@ -9,6 +9,28 @@ export interface StateOptionsMessages {
|
|
|
9
9
|
edit: string
|
|
10
10
|
duplicate: string
|
|
11
11
|
sort: string
|
|
12
|
+
up: string
|
|
13
|
+
down: string
|
|
14
|
+
showHelp: string
|
|
15
|
+
mdeLink1: string
|
|
16
|
+
mdeLink2: string
|
|
17
|
+
mdeImg1: string
|
|
18
|
+
mdeImg2: string
|
|
19
|
+
mdeTable1: string
|
|
20
|
+
mdeTable2: string
|
|
21
|
+
bold: string
|
|
22
|
+
italic: string
|
|
23
|
+
heading: string
|
|
24
|
+
quote: string
|
|
25
|
+
unorderedList: string
|
|
26
|
+
orderedList: string
|
|
27
|
+
createLink: string
|
|
28
|
+
insertImage: string
|
|
29
|
+
createTable: string
|
|
30
|
+
preview: string
|
|
31
|
+
mdeGuide: string
|
|
32
|
+
undo: string
|
|
33
|
+
redo: string
|
|
12
34
|
}
|
|
13
35
|
|
|
14
36
|
export type LocaleMessages = CompileOptionsMessages & StateOptionsMessages
|
package/src/state/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import debug from 'debug'
|
|
|
4
4
|
import { evalExpression, producePatchedData } from './state-node.js'
|
|
5
5
|
import { createStateTree } from './state-tree.js'
|
|
6
6
|
import { Display } from './utils/display.js'
|
|
7
|
-
import { isGetItemsExpression, isGetItemsFetch } from '@json-layout/vocabulary'
|
|
7
|
+
import { isGetItemsExpression, isGetItemsFetch, isItemsLayout } from '@json-layout/vocabulary'
|
|
8
8
|
|
|
9
9
|
export { Display } from './utils/display.js'
|
|
10
10
|
|
|
@@ -20,6 +20,8 @@ export { Display } from './utils/display.js'
|
|
|
20
20
|
* @typedef {import('./types.js').SliderNode} SliderNode
|
|
21
21
|
* @typedef {import('./types.js').SectionNode} SectionNode
|
|
22
22
|
* @typedef {import('./types.js').SelectNode} SelectNode
|
|
23
|
+
* @typedef {import('./types.js').AutocompleteNode} AutocompleteNode
|
|
24
|
+
* @typedef {import('./types.js').ComboboxNode} ComboboxNode
|
|
23
25
|
* @typedef {import('./types.js').CheckboxNode} CheckboxNode
|
|
24
26
|
* @typedef {import('./types.js').SwitchNode} SwitchNode
|
|
25
27
|
* @typedef {import('./types.js').ColorPickerNode} ColorPickerNode
|
|
@@ -29,6 +31,7 @@ export { Display } from './utils/display.js'
|
|
|
29
31
|
* @typedef {import('./types.js').ExpansionPanelsNode} ExpansionPanelsNode
|
|
30
32
|
* @typedef {import('./types.js').TabsNode} TabsNode
|
|
31
33
|
* @typedef {import('./types.js').VerticalTabsNode} VerticalTabsNode
|
|
34
|
+
* @typedef {import('./types.js').StepperNode} StepperNode
|
|
32
35
|
* @typedef {import('./types.js').OneOfSelectNode} OneOfSelectNode
|
|
33
36
|
* @typedef {import('./types.js').ListNode} ListNode
|
|
34
37
|
*/
|
|
@@ -36,8 +39,8 @@ export { Display } from './utils/display.js'
|
|
|
36
39
|
/** @type {(node: StateNode | undefined) => node is SectionNode} */
|
|
37
40
|
export const isSection = (node) => !!node && node.layout.comp === 'section'
|
|
38
41
|
|
|
39
|
-
/** @type {(node: StateNode | undefined) => node is SelectNode} */
|
|
40
|
-
export const
|
|
42
|
+
/** @type {(node: StateNode | undefined) => node is SelectNode | ComboboxNode | AutocompleteNode} */
|
|
43
|
+
export const isItemsNode = (node) => !!node && isItemsLayout(node.layout)
|
|
41
44
|
|
|
42
45
|
const logDataBinding = debug('jl:data-binding')
|
|
43
46
|
|
|
@@ -61,6 +64,8 @@ function fillOptions (partialOptions, compiledLayout) {
|
|
|
61
64
|
titleDepth: 2,
|
|
62
65
|
validateOn: 'input',
|
|
63
66
|
initialValidation: 'withData',
|
|
67
|
+
defaultOn: 'empty',
|
|
68
|
+
autofocus: false,
|
|
64
69
|
...partialOptions,
|
|
65
70
|
messages
|
|
66
71
|
}
|
|
@@ -166,22 +171,36 @@ export class StatefulLayout {
|
|
|
166
171
|
// @ts-ignore
|
|
167
172
|
_lastCreateStateTreeContext
|
|
168
173
|
|
|
174
|
+
/**
|
|
175
|
+
* @private
|
|
176
|
+
* @type {string | null}
|
|
177
|
+
*/
|
|
178
|
+
_autofocusTarget
|
|
179
|
+
/**
|
|
180
|
+
* @private
|
|
181
|
+
* @type {string | null}
|
|
182
|
+
*/
|
|
183
|
+
_previousAutofocusTarget
|
|
184
|
+
|
|
169
185
|
/**
|
|
170
186
|
* @param {import("../index.js").CompiledLayout} compiledLayout
|
|
171
187
|
* @param {import("../index.js").SkeletonTree} skeletonTree
|
|
172
188
|
* @param {Partial<StatefulLayoutOptions>} options
|
|
173
|
-
* @param {unknown} data
|
|
189
|
+
* @param {unknown} [data]
|
|
174
190
|
*/
|
|
175
|
-
constructor (compiledLayout, skeletonTree, options, data
|
|
191
|
+
constructor (compiledLayout, skeletonTree, options, data) {
|
|
176
192
|
this._compiledLayout = compiledLayout
|
|
177
193
|
this.skeletonTree = skeletonTree
|
|
178
194
|
/** @type {import('mitt').Emitter<StatefulLayoutEvents>} */
|
|
179
195
|
this.events = mitt()
|
|
180
196
|
this.prepareOptions(options)
|
|
197
|
+
this._autofocusTarget = this.options.autofocus ? '' : null
|
|
198
|
+
this._previousAutofocusTarget = null
|
|
181
199
|
this._data = data
|
|
182
200
|
this.initValidationState()
|
|
183
201
|
this.activeItems = {}
|
|
184
202
|
this.updateState()
|
|
203
|
+
this.handleAutofocus()
|
|
185
204
|
}
|
|
186
205
|
|
|
187
206
|
/**
|
|
@@ -210,10 +229,11 @@ export class StatefulLayout {
|
|
|
210
229
|
*/
|
|
211
230
|
updateState () {
|
|
212
231
|
this.createStateTree()
|
|
213
|
-
if (this._data !== this._stateTree.root.data) {
|
|
232
|
+
if (this._data !== this._stateTree.root.data || this._autofocusTarget !== this._lastCreateStateTreeContext.autofocusTarget) {
|
|
214
233
|
logDataBinding('hydrating state tree changed the data, do it again', this._data, this._stateTree.root.data)
|
|
215
234
|
// this is necessary because a first hydration can add default values and change validity, etc
|
|
216
235
|
this._data = this._stateTree.root.data
|
|
236
|
+
this._autofocusTarget = this._lastCreateStateTreeContext.autofocusTarget
|
|
217
237
|
this.createStateTree()
|
|
218
238
|
}
|
|
219
239
|
logDataBinding('emit update event', this._data, this._stateTree)
|
|
@@ -225,7 +245,12 @@ export class StatefulLayout {
|
|
|
225
245
|
*/
|
|
226
246
|
createStateTree () {
|
|
227
247
|
/** @type {CreateStateTreeContext} */
|
|
228
|
-
const createStateTreeContext = {
|
|
248
|
+
const createStateTreeContext = {
|
|
249
|
+
nodes: [],
|
|
250
|
+
activeItems: this.activeItems,
|
|
251
|
+
autofocusTarget: this._autofocusTarget,
|
|
252
|
+
initial: !this._lastCreateStateTreeContext
|
|
253
|
+
}
|
|
229
254
|
this._stateTree = createStateTree(
|
|
230
255
|
createStateTreeContext,
|
|
231
256
|
this._options,
|
|
@@ -238,7 +263,11 @@ export class StatefulLayout {
|
|
|
238
263
|
)
|
|
239
264
|
this._lastCreateStateTreeContext = createStateTreeContext
|
|
240
265
|
if (!this.validationState.initialized) {
|
|
241
|
-
this.
|
|
266
|
+
this._validationState = {
|
|
267
|
+
initialized: true,
|
|
268
|
+
validatedForm: this._validationState.validatedForm,
|
|
269
|
+
validatedChildren: createStateTreeContext.nodes.filter(n => n.validated).map(n => n.fullKey)
|
|
270
|
+
}
|
|
242
271
|
}
|
|
243
272
|
}
|
|
244
273
|
|
|
@@ -258,6 +287,13 @@ export class StatefulLayout {
|
|
|
258
287
|
return this.stateTree.valid
|
|
259
288
|
}
|
|
260
289
|
|
|
290
|
+
/**
|
|
291
|
+
* @returns {string[]}
|
|
292
|
+
*/
|
|
293
|
+
get errors () {
|
|
294
|
+
return this._lastCreateStateTreeContext.nodes.filter(n => !!n.error).map(n => /** @type {string} */(n.error))
|
|
295
|
+
}
|
|
296
|
+
|
|
261
297
|
/**
|
|
262
298
|
* @returns {boolean}
|
|
263
299
|
*/
|
|
@@ -277,6 +313,7 @@ export class StatefulLayout {
|
|
|
277
313
|
}
|
|
278
314
|
if (activateKey !== undefined) {
|
|
279
315
|
this.activeItems[node.fullKey] = activateKey
|
|
316
|
+
this._autofocusTarget = node.fullKey + '/' + activateKey
|
|
280
317
|
}
|
|
281
318
|
if (node.parentFullKey === null) {
|
|
282
319
|
this.data = data
|
|
@@ -287,6 +324,10 @@ export class StatefulLayout {
|
|
|
287
324
|
if (!parentNode) throw new Error(`parent with key "${node.parentFullKey}" not found`)
|
|
288
325
|
const newParentValue = producePatchedData(parentNode.data ?? {}, node, data)
|
|
289
326
|
this.input(parentNode, newParentValue)
|
|
327
|
+
|
|
328
|
+
if (activateKey !== undefined) {
|
|
329
|
+
this.handleAutofocus()
|
|
330
|
+
}
|
|
290
331
|
}
|
|
291
332
|
|
|
292
333
|
/**
|
|
@@ -304,11 +345,26 @@ export class StatefulLayout {
|
|
|
304
345
|
|
|
305
346
|
/**
|
|
306
347
|
* @param {StateNode} node
|
|
307
|
-
* @returns {Promise<import('@json-layout/vocabulary').SelectItems>}
|
|
308
348
|
*/
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
if (node.
|
|
349
|
+
validateNodeRecurse (node) {
|
|
350
|
+
this.validationState = { validatedChildren: this.validationState.validatedChildren.concat([node.fullKey]) }
|
|
351
|
+
if (node.children) {
|
|
352
|
+
for (const child of node.children) {
|
|
353
|
+
this.validateNodeRecurse(child)
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* @private
|
|
360
|
+
* @param {StateNode} node
|
|
361
|
+
* @param {string} q
|
|
362
|
+
* @returns {Promise<[import('@json-layout/vocabulary').SelectItems, boolean]>}
|
|
363
|
+
*/
|
|
364
|
+
async getSourceItems (node, q = '') {
|
|
365
|
+
if (!isItemsNode(node)) throw new Error('node is not a component with an items list')
|
|
366
|
+
|
|
367
|
+
if (node.layout.items) return [node.layout.items, false]
|
|
312
368
|
|
|
313
369
|
/** @type {(expression: import('@json-layout/vocabulary').Expression, data: any) => any} */
|
|
314
370
|
const evalSelectExpression = (expression, data) => {
|
|
@@ -316,12 +372,24 @@ export class StatefulLayout {
|
|
|
316
372
|
}
|
|
317
373
|
|
|
318
374
|
let rawItems
|
|
375
|
+
let appliedQ = false
|
|
319
376
|
if (node.layout.getItems && isGetItemsExpression(node.layout.getItems)) {
|
|
320
377
|
rawItems = evalSelectExpression(node.layout.getItems, null)
|
|
321
378
|
if (!Array.isArray(rawItems)) throw new Error('getItems expression didn\'t return an array')
|
|
322
379
|
}
|
|
323
380
|
if (node.layout.getItems && isGetItemsFetch(node.layout.getItems)) {
|
|
324
|
-
const url = evalSelectExpression(node.layout.getItems.url, null)
|
|
381
|
+
const url = new URL(evalSelectExpression(node.layout.getItems.url, null))
|
|
382
|
+
let qSearchParam = node.layout.getItems.qSearchParam
|
|
383
|
+
if (!qSearchParam) {
|
|
384
|
+
for (const searchParam of url.searchParams.entries()) {
|
|
385
|
+
if (searchParam[1] === '{q}') qSearchParam = searchParam[0]
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
if (qSearchParam) {
|
|
389
|
+
appliedQ = true
|
|
390
|
+
if (q) url.searchParams.set(qSearchParam, q)
|
|
391
|
+
else url.searchParams.delete(qSearchParam)
|
|
392
|
+
}
|
|
325
393
|
rawItems = await (await fetch(url)).json()
|
|
326
394
|
}
|
|
327
395
|
|
|
@@ -329,28 +397,40 @@ export class StatefulLayout {
|
|
|
329
397
|
if (node.layout.getItems?.itemsResults) {
|
|
330
398
|
rawItems = evalSelectExpression(node.layout.getItems.itemsResults, rawItems)
|
|
331
399
|
}
|
|
332
|
-
|
|
400
|
+
/** @type {import('@json-layout/vocabulary').SelectItems} */
|
|
401
|
+
const items = rawItems.map((/** @type {any} */ rawItem) => {
|
|
402
|
+
/** @type {Partial<import('@json-layout/vocabulary').SelectItem>} */
|
|
403
|
+
const item = {}
|
|
333
404
|
if (typeof rawItem === 'object') {
|
|
334
|
-
/** @type {Partial<import('@json-layout/vocabulary').SelectItem>} */
|
|
335
|
-
const item = {}
|
|
336
405
|
item.value = node.layout.getItems?.itemValue ? evalSelectExpression(node.layout.getItems.itemValue, rawItem) : (node.layout.getItems?.returnObjects ? rawItem : rawItem.value)
|
|
337
406
|
item.key = node.layout.getItems?.itemKey ? evalSelectExpression(node.layout.getItems.itemKey, rawItem) : rawItem.key
|
|
338
407
|
item.title = node.layout.getItems?.itemTitle ? evalSelectExpression(node.layout.getItems.itemTitle, rawItem) : rawItem.title
|
|
339
408
|
item.value = item.value ?? item.key
|
|
340
409
|
item.key = item.key ?? item.value + ''
|
|
341
410
|
item.title = item.title ?? item.key
|
|
342
|
-
|
|
411
|
+
if (!item.icon && rawItem.icon) item.icon = rawItem.icon
|
|
343
412
|
} else {
|
|
344
|
-
/** @type {Partial<import('@json-layout/vocabulary').SelectItem>} */
|
|
345
|
-
const item = {}
|
|
346
413
|
item.value = node.layout.getItems?.itemValue ? evalSelectExpression(node.layout.getItems.itemValue, rawItem) : rawItem
|
|
347
414
|
item.key = node.layout.getItems?.itemKey ? evalSelectExpression(node.layout.getItems.itemKey, rawItem) : item.value
|
|
348
415
|
item.title = node.layout.getItems?.itemTitle ? evalSelectExpression(node.layout.getItems.itemTitle, rawItem) : item.value
|
|
349
|
-
return item
|
|
350
416
|
}
|
|
417
|
+
if (node.layout.getItems?.itemIcon) item.icon = evalSelectExpression(node.layout.getItems?.itemIcon, rawItem)
|
|
418
|
+
return item
|
|
351
419
|
})
|
|
420
|
+
return [items, appliedQ]
|
|
352
421
|
}
|
|
353
|
-
throw new Error(
|
|
422
|
+
throw new Error(`node ${node.fullKey} is missing items or getItems parameters`)
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* @param {StateNode} node
|
|
427
|
+
* @param {string} q
|
|
428
|
+
* @returns {Promise<import('@json-layout/vocabulary').SelectItems>}
|
|
429
|
+
*/
|
|
430
|
+
async getItems (node, q = '') {
|
|
431
|
+
const [sourceItems, appliedQ] = await this.getSourceItems(node, q)
|
|
432
|
+
if (q && !appliedQ) return sourceItems.filter(item => item.title.toLowerCase().includes(q.toLowerCase()))
|
|
433
|
+
return sourceItems
|
|
354
434
|
}
|
|
355
435
|
|
|
356
436
|
/**
|
|
@@ -364,11 +444,13 @@ export class StatefulLayout {
|
|
|
364
444
|
*/
|
|
365
445
|
activateItem (node, key) {
|
|
366
446
|
this.activeItems[node.fullKey] = key
|
|
447
|
+
this._autofocusTarget = node.fullKey + '/' + key
|
|
367
448
|
if (node.key === '$oneOf') {
|
|
368
|
-
this.input(node,
|
|
449
|
+
this.input(node, undefined)
|
|
369
450
|
} else {
|
|
370
451
|
this.updateState()
|
|
371
452
|
}
|
|
453
|
+
this.handleAutofocus()
|
|
372
454
|
}
|
|
373
455
|
|
|
374
456
|
/**
|
|
@@ -378,4 +460,15 @@ export class StatefulLayout {
|
|
|
378
460
|
delete this.activeItems[node.fullKey]
|
|
379
461
|
this.updateState()
|
|
380
462
|
}
|
|
463
|
+
|
|
464
|
+
handleAutofocus () {
|
|
465
|
+
const autofocusTarget = this._autofocusTarget
|
|
466
|
+
if (autofocusTarget !== null && this._autofocusTarget !== this._previousAutofocusTarget) {
|
|
467
|
+
this._previousAutofocusTarget = autofocusTarget
|
|
468
|
+
setTimeout(() => {
|
|
469
|
+
logDataBinding('emit autofocus event', autofocusTarget)
|
|
470
|
+
this.events.emit('autofocus', autofocusTarget)
|
|
471
|
+
})
|
|
472
|
+
}
|
|
473
|
+
}
|
|
381
474
|
}
|