@jsonui/core 0.9.0 → 0.10.8
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/dist/cjs/index.js +1038 -602
- package/dist/cjs/index.js.map +1 -1
- package/dist/esm/index.js +1038 -602
- package/dist/esm/index.js.map +1 -1
- package/dist/index.d.ts +219 -314
- package/package.json +47 -47
- package/LICENSE +0 -21
- package/README.md +0 -20
package/dist/esm/index.js
CHANGED
|
@@ -1,682 +1,1118 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
this.init = ({ components, functions }) => {
|
|
4
|
-
this.stock.components = Object.assign(Object.assign({}, this.stock.components), components);
|
|
5
|
-
this.stock.functions = Object.assign(Object.assign({}, this.stock.functions), functions);
|
|
6
|
-
};
|
|
7
|
-
this.registerComponent = (key, value) => {
|
|
8
|
-
if (!!key && typeof key === 'string' && key.length > 0 && !(key in this.stock.components)) {
|
|
9
|
-
this.stock.components[key] = value;
|
|
10
|
-
}
|
|
11
|
-
};
|
|
12
|
-
this.registerFunction = (key, value) => {
|
|
13
|
-
if (!!key && typeof key === 'string' && key.length > 0 && !(key in this.stock.functions)) {
|
|
14
|
-
this.stock.functions[key] = value;
|
|
15
|
-
}
|
|
16
|
-
};
|
|
17
|
-
this.callFunction = (name, attr, props, callerArgs) => {
|
|
18
|
-
if (!!attr && !!name && name in this.stock.functions) {
|
|
19
|
-
const result = this.stock.functions[name](attr, props, callerArgs, this);
|
|
20
|
-
return result;
|
|
21
|
-
}
|
|
22
|
-
return null;
|
|
23
|
-
};
|
|
24
|
-
this.getComponent = (componentName) => !!componentName && componentName in this.stock.components
|
|
25
|
-
? this.stock.components[componentName]
|
|
26
|
-
: // eslint-disable-next-line no-underscore-dangle
|
|
27
|
-
this.stock.components._Undefined;
|
|
28
|
-
this.stock = {
|
|
29
|
-
components: {},
|
|
30
|
-
functions: {},
|
|
31
|
-
};
|
|
32
|
-
this.Wrapper = Wrapper;
|
|
33
|
-
this.validations = [];
|
|
34
|
-
this.reduxStore = reduxStore;
|
|
35
|
-
this.init(newStock);
|
|
36
|
-
}
|
|
37
|
-
}const SEPARATOR = '/';
|
|
38
|
-
const STORE_ERROR_POSTFIX = '.error';
|
|
39
|
-
const STORE_TOUCH_POSTFIX = '.touch';
|
|
40
|
-
const PATH_MODIFIERS_KEY = '$pathModifiers';
|
|
41
|
-
const MODIFIER_KEY = '$modifier';
|
|
1
|
+
import Ajv from'ajv';import addFormats from'ajv-formats';import ajvErrors from'ajv-errors';const V_COMP = '$comp';
|
|
2
|
+
const V_CHILDREN = '$children';
|
|
42
3
|
const ACTION_KEY = '$action';
|
|
43
|
-
const
|
|
44
|
-
const
|
|
45
|
-
const REF_VALIDATES = '$validations';
|
|
46
|
-
const STYLE_WEB_NAME = 'styleWeb';
|
|
47
|
-
const STYLE_RN_NAME = 'styleRN';
|
|
48
|
-
const REDUX_GET_FUNCTION = 'get';
|
|
49
|
-
const REDUX_SET_FUNCTION = 'set';
|
|
50
|
-
const REDUX_FUNCTIONS = [REDUX_GET_FUNCTION, REDUX_SET_FUNCTION];
|
|
51
|
-
const PATHNAME = 'path';
|
|
52
|
-
const SIMPLE_DATA_TYPES = ['string', 'number', 'boolean', 'null'];
|
|
53
|
-
const V_CHILDREN_NAME = '$children';
|
|
54
|
-
const V_COMP_NAME = '$comp';
|
|
4
|
+
const MODIFIER_KEY = '$modifier';
|
|
5
|
+
const PATH_MODIFIERS_KEY = '$pathModifiers';
|
|
55
6
|
const LIST_SEMAPHORE = '$isList';
|
|
56
7
|
const LIST_ITEM = '$listItem';
|
|
8
|
+
/** Main JsonUI list pagination keys (parity). */
|
|
57
9
|
const LIST_PAGE = '$page';
|
|
58
10
|
const LIST_ITEM_PER_PAGE = '$itemPerPage';
|
|
59
11
|
const LIST_LENGTH = '$listLength';
|
|
60
|
-
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
const
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
12
|
+
/** Field-level inline validation spec key on a node. */
|
|
13
|
+
const V_VALIDATIONS = '$validations';
|
|
14
|
+
// Store-name suffixes for parallel trees (errors, touch-state, etc.).
|
|
15
|
+
const ERROR_STORE_SUFFIX = '.error';
|
|
16
|
+
/** Shadow store for field touched state (aligned with main JsonUI `.touch`). */
|
|
17
|
+
const TOUCH_STORE_SUFFIX = '.touch';
|
|
18
|
+
const JSON_SEPARATOR = '/';
|
|
19
|
+
// Single-root store layout helper.
|
|
20
|
+
// All logical stores live under `/storeRoot/{storeName}/...`.
|
|
21
|
+
const STORE_ROOT_PATH = '/storeRoot';/**
|
|
22
|
+
* Shared helpers for function handlers.
|
|
23
|
+
*/
|
|
24
|
+
const hasAnyError$1 = (value) => {
|
|
25
|
+
if (value === null || value === undefined)
|
|
26
|
+
return false;
|
|
27
|
+
if (Array.isArray(value)) {
|
|
28
|
+
return value.some((v) => hasAnyError$1(v));
|
|
29
|
+
}
|
|
30
|
+
if (typeof value === 'object') {
|
|
31
|
+
return Object.values(value).some((v) => hasAnyError$1(v));
|
|
32
|
+
}
|
|
33
|
+
// Any non-null / non-undefined primitive counts as "has error".
|
|
34
|
+
return true;
|
|
72
35
|
};
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
if (
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
36
|
+
const hasAnyTouched$1 = (value) => {
|
|
37
|
+
if (value === true)
|
|
38
|
+
return true;
|
|
39
|
+
if (value === null || value === undefined)
|
|
40
|
+
return false;
|
|
41
|
+
if (Array.isArray(value)) {
|
|
42
|
+
return value.some((v) => hasAnyTouched$1(v));
|
|
43
|
+
}
|
|
44
|
+
if (typeof value === 'object') {
|
|
45
|
+
return Object.values(value).some((v) => hasAnyTouched$1(v));
|
|
46
|
+
}
|
|
47
|
+
return false;
|
|
83
48
|
};
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
return
|
|
49
|
+
/**
|
|
50
|
+
* Throws if value contains anything that cannot round-trip through JSON:
|
|
51
|
+
* undefined, function, symbol, bigint, NaN, non-finite numbers, or circular references.
|
|
52
|
+
*/
|
|
53
|
+
const assertJsonCompatible = (value, seen = new WeakSet()) => {
|
|
54
|
+
if (value === null)
|
|
55
|
+
return;
|
|
56
|
+
if (value === undefined)
|
|
57
|
+
throw new Error('undefined is not JSON-compatible');
|
|
58
|
+
const type = typeof value;
|
|
59
|
+
if (type === 'string' || type === 'boolean')
|
|
60
|
+
return;
|
|
61
|
+
if (type === 'number') {
|
|
62
|
+
if (!Number.isFinite(value))
|
|
63
|
+
throw new Error(`Non-finite number is not JSON-compatible: ${value}`);
|
|
64
|
+
return;
|
|
91
65
|
}
|
|
92
|
-
|
|
93
|
-
|
|
66
|
+
if (type === 'function' || type === 'symbol' || type === 'bigint') {
|
|
67
|
+
throw new Error(`${type} is not JSON-compatible`);
|
|
68
|
+
}
|
|
69
|
+
const obj = value;
|
|
70
|
+
if (seen.has(obj))
|
|
71
|
+
throw new Error('Circular reference is not JSON-compatible');
|
|
72
|
+
seen.add(obj);
|
|
73
|
+
if (Array.isArray(value)) {
|
|
74
|
+
for (const item of value)
|
|
75
|
+
assertJsonCompatible(item, seen);
|
|
94
76
|
}
|
|
77
|
+
else {
|
|
78
|
+
for (const v of Object.values(value))
|
|
79
|
+
assertJsonCompatible(v, seen);
|
|
80
|
+
}
|
|
81
|
+
seen.delete(obj);
|
|
95
82
|
};
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
83
|
+
/**
|
|
84
|
+
* Deep clone helper used by get() so callers cannot mutate the internal store state.
|
|
85
|
+
* Only JSON-compatible values are accepted; functions and other non-JSON types throw.
|
|
86
|
+
*/
|
|
87
|
+
const cloneDeep = (value) => {
|
|
88
|
+
if (value === null || value === undefined)
|
|
89
|
+
return value;
|
|
90
|
+
const type = typeof value;
|
|
91
|
+
if (type !== 'object') {
|
|
92
|
+
if (type === 'function' || type === 'symbol' || type === 'bigint') {
|
|
93
|
+
throw new Error(`${type} is not JSON-compatible`);
|
|
94
|
+
}
|
|
95
|
+
if (type === 'number' && !Number.isFinite(value)) {
|
|
96
|
+
throw new Error(`Non-finite number is not JSON-compatible: ${value}`);
|
|
97
|
+
}
|
|
100
98
|
return value;
|
|
101
99
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
100
|
+
if (Array.isArray(value)) {
|
|
101
|
+
return value.map((item) => cloneDeep(item));
|
|
102
|
+
}
|
|
103
|
+
const obj = value;
|
|
104
|
+
const result = {};
|
|
105
|
+
for (const key of Object.keys(obj)) {
|
|
106
|
+
result[key] = cloneDeep(obj[key]);
|
|
106
107
|
}
|
|
107
|
-
|
|
108
|
-
|
|
108
|
+
return result;
|
|
109
|
+
};/**
|
|
110
|
+
* Minimal JSON Pointer implementation (RFC 6901).
|
|
111
|
+
* Supports get, set, and path resolution (absolute, relative, ./ ../).
|
|
112
|
+
*
|
|
113
|
+
* Paths and nesting are unbounded: e.g. /a/b/c/0/d/e/1/f is valid;
|
|
114
|
+
* segments can be object keys or array indices (numeric strings).
|
|
115
|
+
*/
|
|
116
|
+
/**
|
|
117
|
+
* Normalize a path: remove empty segments and trailing/leading slashes.
|
|
118
|
+
* Prevents accidental keys like "" from paths such as "//" or "/a/".
|
|
119
|
+
* Does not decode segments (e.g. ~1 stays so one segment is preserved).
|
|
120
|
+
*/
|
|
121
|
+
const normalizePath = (pathStr) => {
|
|
122
|
+
if (!pathStr || pathStr === '/')
|
|
123
|
+
return '/';
|
|
124
|
+
const trimmed = pathStr.startsWith('/') ? pathStr.slice(1) : pathStr;
|
|
125
|
+
const segments = trimmed.split(JSON_SEPARATOR).filter((s) => s !== '');
|
|
126
|
+
return segments.length === 0 ? '/' : JSON_SEPARATOR + segments.join(JSON_SEPARATOR);
|
|
127
|
+
};
|
|
128
|
+
const parsePath = (pathStr) => {
|
|
129
|
+
if (!pathStr || pathStr === '/')
|
|
130
|
+
return [];
|
|
131
|
+
if (!pathStr.startsWith(JSON_SEPARATOR)) {
|
|
132
|
+
throw new Error(`Invalid JSON Pointer path: ${pathStr}`);
|
|
109
133
|
}
|
|
134
|
+
return pathStr.slice(1).split(JSON_SEPARATOR).map(decode);
|
|
110
135
|
};
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
136
|
+
const decode = (segment) => {
|
|
137
|
+
for (let i = 0; i < segment.length; i++) {
|
|
138
|
+
if (segment[i] === '~') {
|
|
139
|
+
const next = segment[i + 1];
|
|
140
|
+
if (next !== '0' && next !== '1') {
|
|
141
|
+
throw new Error(`Invalid JSON Pointer escape in segment: ${segment}`);
|
|
142
|
+
}
|
|
143
|
+
i += 1;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return segment.replace(/~1/g, '/').replace(/~0/g, '~');
|
|
147
|
+
};
|
|
148
|
+
const encode = (segment) => {
|
|
149
|
+
return String(segment).replace(/~/g, '~0').replace(/\//g, '~1');
|
|
150
|
+
};
|
|
151
|
+
const get$1 = (obj, pathStr) => {
|
|
152
|
+
const segments = parsePath(pathStr); // parsePath normalizes, so /a//b/ -> /a/b
|
|
153
|
+
let current = obj;
|
|
154
|
+
for (const seg of segments) {
|
|
155
|
+
if (current == null || typeof current !== 'object')
|
|
156
|
+
return undefined;
|
|
157
|
+
current = current[seg];
|
|
158
|
+
}
|
|
159
|
+
return current;
|
|
160
|
+
};
|
|
161
|
+
/**
|
|
162
|
+
* Resolve a path against a base path (for relative paths like ./x, ../y).
|
|
163
|
+
* Supports arbitrary depth; excess ".." yields root then appends remaining segments.
|
|
164
|
+
*/
|
|
165
|
+
const resolvePath = (basePath, relativePath) => {
|
|
166
|
+
if (relativePath.startsWith('/'))
|
|
167
|
+
return normalizePath(relativePath);
|
|
168
|
+
const baseSegments = parsePath(basePath);
|
|
169
|
+
const relSegments = relativePath.split(JSON_SEPARATOR).filter(Boolean);
|
|
170
|
+
for (const seg of relSegments) {
|
|
171
|
+
if (seg === '..') {
|
|
172
|
+
baseSegments.pop();
|
|
173
|
+
}
|
|
174
|
+
else if (seg !== '.') {
|
|
175
|
+
baseSegments.push(decode(seg));
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return JSON_SEPARATOR + baseSegments.map(encode).join(JSON_SEPARATOR);
|
|
179
|
+
};/**
|
|
180
|
+
* Tree-shaped state management with multiple stores.
|
|
181
|
+
* Similar to Redux but tailored for JSON UI: per-store trees, JSON Pointer paths.
|
|
182
|
+
*
|
|
183
|
+
* Store paths are unbounded and can be arbitrarily nested (objects and arrays),
|
|
184
|
+
* e.g. /a/b/c/2/d/e/0/f. Logical paths are resolved via resolveStorePath and
|
|
185
|
+
* applied consistently in get/set.
|
|
186
|
+
*/
|
|
187
|
+
const isTouchOrErrorShadowStore = (storeName) => {
|
|
188
|
+
return storeName.endsWith(TOUCH_STORE_SUFFIX) || storeName.endsWith(ERROR_STORE_SUFFIX);
|
|
124
189
|
};
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
190
|
+
class FormStore {
|
|
191
|
+
constructor() {
|
|
192
|
+
this.state = {};
|
|
193
|
+
this.changeListeners = new Set();
|
|
194
|
+
}
|
|
195
|
+
getState() {
|
|
196
|
+
return cloneDeep(this.state);
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Logical JsonUI stores snapshot — same shape as `JsonUI` `defaultValues`:
|
|
200
|
+
* `{ data: {...}, "data.touch": {...}, "data.error": {...} }`.
|
|
201
|
+
* Omits the internal `/storeRoot` wrapper returned by {@link getState}.
|
|
202
|
+
*/
|
|
203
|
+
getLogicalStoresMap() {
|
|
204
|
+
const slice = get$1(this.state, STORE_ROOT_PATH);
|
|
205
|
+
if (slice === undefined || slice === null || typeof slice !== 'object' || Array.isArray(slice)) {
|
|
206
|
+
return {};
|
|
207
|
+
}
|
|
208
|
+
return cloneDeep(slice);
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Initialise a logical store root without marking fields as touched.
|
|
212
|
+
*/
|
|
213
|
+
initializeStore(storeName, value) {
|
|
214
|
+
this.set(storeName, '/', value, false);
|
|
215
|
+
}
|
|
216
|
+
getByPointer(path) {
|
|
217
|
+
const value = get$1(this.state, path);
|
|
218
|
+
return cloneDeep(value);
|
|
219
|
+
}
|
|
220
|
+
setByPointer(path, value) {
|
|
221
|
+
assertJsonCompatible(value);
|
|
222
|
+
this.state = setImmutable(this.state, path, value);
|
|
223
|
+
this.notify();
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Convenience helpers that work with logical store names and logical paths,
|
|
227
|
+
* instead of requiring callers to compose `/storeRoot/{storeName}/...`.
|
|
228
|
+
*
|
|
229
|
+
* Store isolation: each storeName (e.g. "data", "data.error", "data.touch") is
|
|
230
|
+
* a distinct store. get/set only read/write that store's subtree;
|
|
231
|
+
* "data.error" cannot access "data" or any other store.
|
|
232
|
+
*
|
|
233
|
+
* When trackTouch is true, set also writes to `${storeName}.touch` at
|
|
234
|
+
* the same logicalPath; path normalization applies (no empty segments).
|
|
235
|
+
*/
|
|
236
|
+
set(storeName, logicalPath, value, trackTouch = true) {
|
|
237
|
+
const internalPath = makeStorePath(storeName, logicalPath);
|
|
238
|
+
this.setByPointer(internalPath, value);
|
|
239
|
+
if (trackTouch && !isTouchOrErrorShadowStore(storeName)) {
|
|
240
|
+
const touchStoreName = `${storeName}${TOUCH_STORE_SUFFIX}`;
|
|
241
|
+
this.setByPointer(makeStorePath(touchStoreName, logicalPath), true);
|
|
242
|
+
}
|
|
243
|
+
// Notify fine-grained listeners with logical store name + path so
|
|
244
|
+
// JsonUI can re-resolve only components that depend on this slice.
|
|
245
|
+
this.notifyChange(storeName, logicalPath);
|
|
246
|
+
}
|
|
247
|
+
get(storeName, logicalPath) {
|
|
248
|
+
const internalPath = makeStorePath(storeName, logicalPath);
|
|
249
|
+
return this.getByPointer(internalPath);
|
|
250
|
+
}
|
|
251
|
+
// Backward-compatible aliases for existing consumers.
|
|
252
|
+
setForStore(storeName, logicalPath, value, trackTouch = true) {
|
|
253
|
+
this.set(storeName, logicalPath, value, trackTouch);
|
|
254
|
+
}
|
|
255
|
+
// Backward-compatible aliases for existing consumers.
|
|
256
|
+
getForStore(storeName, logicalPath) {
|
|
257
|
+
return this.get(storeName, logicalPath);
|
|
258
|
+
}
|
|
259
|
+
notify() {
|
|
260
|
+
// No coarse-grained subscription API is exposed intentionally.
|
|
261
|
+
}
|
|
262
|
+
subscribeChange(listener) {
|
|
263
|
+
this.changeListeners.add(listener);
|
|
264
|
+
return () => this.changeListeners.delete(listener);
|
|
265
|
+
}
|
|
266
|
+
notifyChange(storeName, logicalPath) {
|
|
267
|
+
this.changeListeners.forEach((l) => l(storeName, logicalPath));
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
const makeStorePath = (storeName, path) => {
|
|
271
|
+
if (!storeName || storeName.length === 0) {
|
|
272
|
+
throw new Error('storeName must be a non-empty string');
|
|
273
|
+
}
|
|
274
|
+
const base = `${STORE_ROOT_PATH}/${storeName}`;
|
|
275
|
+
const pointerPath = !path || path === '/' ? '/' : path.startsWith('/') ? path : `/${path}`;
|
|
276
|
+
const normalized = normalizePath(pointerPath);
|
|
277
|
+
if (normalized === '/')
|
|
278
|
+
return base;
|
|
279
|
+
return base + normalized;
|
|
154
280
|
};
|
|
155
281
|
/**
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
282
|
+
* Immutable variant of ptrSet: returns a new root object with the change applied,
|
|
283
|
+
* without mutating the original tree. Only the objects/arrays along the path are
|
|
284
|
+
* shallow-copied; unrelated subtrees are structurally shared for performance.
|
|
159
285
|
*/
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
286
|
+
const setImmutable = (root, pathStr, value) => {
|
|
287
|
+
const segments = parsePath(pathStr);
|
|
288
|
+
if (segments.length === 0)
|
|
289
|
+
return root;
|
|
290
|
+
const originalRoot = root;
|
|
291
|
+
const cloneContainer = (container) => {
|
|
292
|
+
if (Array.isArray(container)) {
|
|
293
|
+
return container.slice();
|
|
294
|
+
}
|
|
295
|
+
if (container && typeof container === 'object') {
|
|
296
|
+
return { ...container };
|
|
297
|
+
}
|
|
298
|
+
return {};
|
|
299
|
+
};
|
|
300
|
+
const setAt = (current, index) => {
|
|
301
|
+
const isLast = index === segments.length - 1;
|
|
302
|
+
const seg = segments[index];
|
|
303
|
+
const container = cloneContainer(current);
|
|
304
|
+
if (isLast) {
|
|
305
|
+
const lastSeg = seg;
|
|
306
|
+
const lastKey = lastSeg === '' || /^\d+$/.test(lastSeg) ? parseInt(lastSeg, 10) : lastSeg;
|
|
307
|
+
if (Array.isArray(container)) {
|
|
308
|
+
container[lastKey] = value;
|
|
171
309
|
}
|
|
172
310
|
else {
|
|
173
|
-
|
|
311
|
+
container[lastSeg] = value;
|
|
174
312
|
}
|
|
313
|
+
return { cloned: container, result: container };
|
|
175
314
|
}
|
|
315
|
+
const nextSeg = segments[index + 1];
|
|
316
|
+
const nextKey = nextSeg === '' || /^\d+$/.test(nextSeg) ? parseInt(nextSeg, 10) : nextSeg;
|
|
317
|
+
// Non-last segment: ensure child container exists before descending.
|
|
318
|
+
const keyForChild = seg;
|
|
319
|
+
let next = Array.isArray(container) && /^\d+$/.test(seg) ? container[parseInt(seg, 10)] : container[seg];
|
|
320
|
+
if (next == null ||
|
|
321
|
+
typeof next !== 'object' ||
|
|
322
|
+
(Array.isArray(next) && typeof nextKey === 'string') ||
|
|
323
|
+
(!Array.isArray(next) && typeof nextKey === 'number')) {
|
|
324
|
+
next = typeof nextKey === 'number' ? [] : {};
|
|
325
|
+
}
|
|
326
|
+
const { result: childClone } = setAt(next, index + 1);
|
|
327
|
+
if (Array.isArray(container) && /^\d+$/.test(String(keyForChild))) {
|
|
328
|
+
container[parseInt(String(keyForChild), 10)] = childClone;
|
|
329
|
+
}
|
|
330
|
+
else {
|
|
331
|
+
container[String(keyForChild)] = childClone;
|
|
332
|
+
}
|
|
333
|
+
return { cloned: container, result: container };
|
|
334
|
+
};
|
|
335
|
+
const { result } = setAt(originalRoot, 0);
|
|
336
|
+
return result;
|
|
337
|
+
};
|
|
338
|
+
/**
|
|
339
|
+
* Resolve a path for a given store, using pathModifiers (from list context) and currentPath for relative paths.
|
|
340
|
+
* - Absolute path: /firstname -> unchanged (then normalized).
|
|
341
|
+
* - Relative path: firstname -> resolved against currentPath (or pathModifiers[storeName] when in list context).
|
|
342
|
+
* - pathModifiers apply per store: only pathModifiers[storeName] is used for that store; "data.error" cannot
|
|
343
|
+
* access "data" or any other store.
|
|
344
|
+
* Returns a normalized path (no empty segments, no trailing slash).
|
|
345
|
+
*/
|
|
346
|
+
const resolveStorePath = (pathStr, currentPath, pathModifiers, storeName) => {
|
|
347
|
+
let resolved;
|
|
348
|
+
const modifier = pathModifiers !== undefined && storeName !== undefined && Object.prototype.hasOwnProperty.call(pathModifiers, storeName)
|
|
349
|
+
? pathModifiers[storeName]
|
|
350
|
+
: undefined;
|
|
351
|
+
if (modifier !== undefined) {
|
|
352
|
+
resolved = resolvePath(modifier.path, pathStr);
|
|
176
353
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
const collectObjMerge = (refConst, json) => {
|
|
180
|
-
const res = {};
|
|
181
|
-
if (refConst && json && typeof json === 'object') {
|
|
182
|
-
const refs = [];
|
|
183
|
-
// eslint-disable-next-line func-names
|
|
184
|
-
traverse(json).forEach(function (x) {
|
|
185
|
-
if (x && !!x[refConst] && !!this && !this.circular) {
|
|
186
|
-
refs.push(x[refConst]);
|
|
187
|
-
}
|
|
188
|
-
});
|
|
189
|
-
refs.filter((i) => !!i).forEach((i) => mergeDeep(res, i));
|
|
354
|
+
else if (pathStr.startsWith('/')) {
|
|
355
|
+
resolved = pathStr;
|
|
190
356
|
}
|
|
191
|
-
|
|
357
|
+
else {
|
|
358
|
+
resolved = resolvePath(currentPath, pathStr);
|
|
359
|
+
}
|
|
360
|
+
return normalizePath(resolved);
|
|
361
|
+
};const createSetAction = (formStore) => {
|
|
362
|
+
return async (params, ctx) => {
|
|
363
|
+
const storeName = params.store;
|
|
364
|
+
const path = params.path;
|
|
365
|
+
let value = params.value;
|
|
366
|
+
const jsonataDef = params.jsonataDef;
|
|
367
|
+
if (!storeName || storeName.length === 0)
|
|
368
|
+
return;
|
|
369
|
+
if (typeof jsonataDef === 'string' && jsonataDef) {
|
|
370
|
+
try {
|
|
371
|
+
const jsonata = (await import('jsonata')).default;
|
|
372
|
+
const expr = jsonata(jsonataDef);
|
|
373
|
+
value = await expr.evaluate(value);
|
|
374
|
+
}
|
|
375
|
+
catch {
|
|
376
|
+
value = params.value;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
const logicalPath = resolveStorePath(path, ctx?.currentPath ?? '/', ctx?.pathModifiers, storeName);
|
|
380
|
+
formStore.set(storeName, logicalPath, value);
|
|
381
|
+
};
|
|
382
|
+
};const set = (params, ctx) => {
|
|
383
|
+
const setFn = createSetAction(ctx.formStore);
|
|
384
|
+
return setFn(params, ctx);
|
|
385
|
+
};const actions = {
|
|
386
|
+
set,
|
|
387
|
+
};/**
|
|
388
|
+
* Expands simplified node format into full JSON UI node so components receive
|
|
389
|
+
* value, onChange, error, etc. without the model having to specify every binding.
|
|
390
|
+
*
|
|
391
|
+
* When a node has store + path (non-empty store, string path), we add:
|
|
392
|
+
* - value: { $modifier: 'get', store, path }
|
|
393
|
+
* - onChange: { $action: 'set', store, path }
|
|
394
|
+
* - error / fieldErrors: { $modifier: 'get', store: store + '.error', path }
|
|
395
|
+
* - fieldTouched: { $modifier: 'get', store: store + '.touch', path }
|
|
396
|
+
*
|
|
397
|
+
* The component is unchanged; RenderNode uses the expanded node for prop
|
|
398
|
+
* resolution and the expanded props are passed through normally.
|
|
399
|
+
*/
|
|
400
|
+
const isSimplifiedNode = (node) => {
|
|
401
|
+
if (typeof node !== 'object' || node === null)
|
|
402
|
+
return false;
|
|
403
|
+
const r = node;
|
|
404
|
+
const store = r.store;
|
|
405
|
+
const path = r.path;
|
|
406
|
+
return typeof store === 'string' && store.length > 0 && typeof path === 'string';
|
|
192
407
|
};
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
408
|
+
/**
|
|
409
|
+
* Returns an expanded node with value, onChange, error, fieldErrors, fieldTouched
|
|
410
|
+
* derived from store + path. Strips store and path from the result so they are
|
|
411
|
+
* not passed as component props.
|
|
412
|
+
* If the node is not simplified, returns the same node reference.
|
|
413
|
+
*/
|
|
414
|
+
const expandSimplifiedNode = (node) => {
|
|
415
|
+
if (!isSimplifiedNode(node) && node[V_COMP] !== 'SubmitButton') {
|
|
416
|
+
return node;
|
|
417
|
+
}
|
|
418
|
+
const { store, path, ...rest } = node;
|
|
419
|
+
const expanded = {
|
|
420
|
+
...(node[V_COMP] === 'SubmitButton'
|
|
421
|
+
? {
|
|
422
|
+
onClick: {
|
|
423
|
+
[ACTION_KEY]: 'submit',
|
|
424
|
+
},
|
|
200
425
|
}
|
|
201
|
-
|
|
202
|
-
|
|
426
|
+
: {}),
|
|
427
|
+
...(isSimplifiedNode(node)
|
|
428
|
+
? {
|
|
429
|
+
value: {
|
|
430
|
+
[MODIFIER_KEY]: 'get',
|
|
431
|
+
store,
|
|
432
|
+
path,
|
|
433
|
+
},
|
|
434
|
+
onChange: {
|
|
435
|
+
[ACTION_KEY]: 'set',
|
|
436
|
+
store,
|
|
437
|
+
path,
|
|
438
|
+
},
|
|
439
|
+
fieldErrors: {
|
|
440
|
+
[MODIFIER_KEY]: 'get',
|
|
441
|
+
store,
|
|
442
|
+
path,
|
|
443
|
+
type: 'ERROR',
|
|
444
|
+
},
|
|
445
|
+
fieldTouched: {
|
|
446
|
+
[MODIFIER_KEY]: 'get',
|
|
447
|
+
store,
|
|
448
|
+
path,
|
|
449
|
+
type: 'TOUCH',
|
|
450
|
+
},
|
|
451
|
+
}
|
|
452
|
+
: {
|
|
453
|
+
//if is not a simplified node, we should add store and path props
|
|
454
|
+
store,
|
|
455
|
+
path,
|
|
456
|
+
}),
|
|
457
|
+
...rest,
|
|
458
|
+
};
|
|
459
|
+
return expanded;
|
|
460
|
+
};const computeListSliceRange = ({ realDataLength, page: pageRaw, itemPerPage: itemPerPageRaw, listLength: listLengthRaw, }) => {
|
|
461
|
+
const coerceNonNegInt = (v, fallback) => {
|
|
462
|
+
if (typeof v === 'number' && Number.isInteger(v) && v >= 0)
|
|
463
|
+
return v;
|
|
464
|
+
return fallback;
|
|
465
|
+
};
|
|
466
|
+
let listLength = coerceNonNegInt(listLengthRaw, realDataLength);
|
|
467
|
+
if (listLengthRaw === undefined) {
|
|
468
|
+
listLength = realDataLength;
|
|
203
469
|
}
|
|
204
|
-
|
|
470
|
+
const itemPerPage = coerceNonNegInt(itemPerPageRaw, listLength);
|
|
471
|
+
const page = coerceNonNegInt(pageRaw, 0);
|
|
472
|
+
const offset = page * itemPerPage <= listLength ? page * itemPerPage : 0;
|
|
473
|
+
const end = Math.min(listLength, offset + itemPerPage);
|
|
474
|
+
const indices = [];
|
|
475
|
+
for (let i = offset; i < end; i++)
|
|
476
|
+
indices.push(i);
|
|
477
|
+
return { offset, end, indices };
|
|
478
|
+
};const getOwnPathModifiers = (node) => {
|
|
479
|
+
return node[PATH_MODIFIERS_KEY];
|
|
205
480
|
};
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
|
|
481
|
+
const mergeEffectivePathModifiers = ({ ownPathModifiers, pathModifiers, currentPath, }) => {
|
|
482
|
+
if (!ownPathModifiers || typeof ownPathModifiers !== 'object') {
|
|
483
|
+
return pathModifiers;
|
|
484
|
+
}
|
|
485
|
+
const merged = {
|
|
486
|
+
...(pathModifiers ?? {}),
|
|
487
|
+
};
|
|
488
|
+
for (const [storeName, spec] of Object.entries(ownPathModifiers)) {
|
|
489
|
+
if (typeof spec !== 'object')
|
|
490
|
+
continue;
|
|
491
|
+
const rawPath = spec.path;
|
|
492
|
+
if (typeof rawPath !== 'string' || rawPath.length === 0)
|
|
493
|
+
continue;
|
|
494
|
+
const resolved = resolveStorePath(rawPath, currentPath, pathModifiers, storeName);
|
|
495
|
+
merged[storeName] = { path: resolved };
|
|
209
496
|
}
|
|
210
|
-
|
|
497
|
+
return merged;
|
|
498
|
+
};/** Segment-aware path overlap check (aligned with validation.ts). */
|
|
499
|
+
const isPathPrefix$1 = (rulePath, targetPath) => {
|
|
500
|
+
const r = rulePath === '' ? '/' : rulePath;
|
|
501
|
+
const t = targetPath === '' ? '/' : targetPath;
|
|
502
|
+
if (r === '/')
|
|
503
|
+
return true;
|
|
504
|
+
if (t === r)
|
|
505
|
+
return true;
|
|
506
|
+
return t.startsWith(r.endsWith('/') ? r : `${r}/`);
|
|
507
|
+
};const hasAnyError = (value) => {
|
|
508
|
+
if (value === null || value === undefined)
|
|
211
509
|
return false;
|
|
510
|
+
if (Array.isArray(value)) {
|
|
511
|
+
return value.some((v) => hasAnyError(v));
|
|
512
|
+
}
|
|
513
|
+
if (typeof value === 'object') {
|
|
514
|
+
return Object.values(value).some((v) => hasAnyError(v));
|
|
212
515
|
}
|
|
213
516
|
return true;
|
|
214
517
|
};
|
|
215
|
-
const
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
const value = obj[key];
|
|
223
|
-
if (typeof value === 'object') {
|
|
224
|
-
return hasLeaf(value);
|
|
225
|
-
}
|
|
226
|
-
return isPrimitiveValue(value, emptyStringAllowed);
|
|
227
|
-
}
|
|
228
|
-
}
|
|
518
|
+
const hasAnyTouched = (value) => {
|
|
519
|
+
if (value === true)
|
|
520
|
+
return true;
|
|
521
|
+
if (value === null || value === undefined)
|
|
522
|
+
return false;
|
|
523
|
+
if (Array.isArray(value)) {
|
|
524
|
+
return value.some((v) => hasAnyTouched(v));
|
|
229
525
|
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
526
|
+
if (typeof value === 'object') {
|
|
527
|
+
return Object.values(value).some((v) => hasAnyTouched(v));
|
|
528
|
+
}
|
|
529
|
+
return false;
|
|
530
|
+
};
|
|
531
|
+
const createGetModifier = (formStore) => {
|
|
532
|
+
return async (params, ctx) => {
|
|
533
|
+
const storeName = params.store;
|
|
534
|
+
const path = params.path;
|
|
535
|
+
const type = params.type;
|
|
536
|
+
const jsonataDef = params.jsonataDef;
|
|
537
|
+
const resolvedStoreName = type === 'ERROR' ? `${storeName}${ERROR_STORE_SUFFIX}` : type === 'TOUCH' ? `${storeName}${TOUCH_STORE_SUFFIX}` : storeName;
|
|
538
|
+
// Path modifiers are keyed by the logical/base store (e.g. "data"),
|
|
539
|
+
// not by shadow stores like "data.error".
|
|
540
|
+
const logicalPath = resolveStorePath(path, ctx.currentPath, ctx.pathModifiers, storeName);
|
|
541
|
+
let value = formStore.get(resolvedStoreName, logicalPath);
|
|
542
|
+
// For ERROR lookups, leaf-less containers (e.g. { players: [{}] })
|
|
543
|
+
// mean there is no actual validation message in the subtree.
|
|
544
|
+
if (type === 'ERROR' && !hasAnyError(value)) {
|
|
545
|
+
value = undefined;
|
|
546
|
+
}
|
|
547
|
+
// TODO need to test hasAnyTouched and hasAnyError
|
|
548
|
+
if (type === 'TOUCH') {
|
|
549
|
+
value = hasAnyTouched(value);
|
|
550
|
+
}
|
|
551
|
+
if (jsonataDef && value !== undefined) {
|
|
552
|
+
try {
|
|
553
|
+
const jsonata = (await import('jsonata')).default;
|
|
554
|
+
const expr = jsonata(jsonataDef);
|
|
555
|
+
value = await expr.evaluate(value);
|
|
240
556
|
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
for (const i of Object.keys(oo)) {
|
|
244
|
-
const itemPath = path.concat(i);
|
|
245
|
-
if (itemPath && itemPath.length > 1 && typeof oo[i] === 'object' && ((_a = oo[i]) === null || _a === void 0 ? void 0 : _a[V_COMP_NAME])) {
|
|
246
|
-
// eslint-disable-next-line no-continue
|
|
247
|
-
continue;
|
|
248
|
-
}
|
|
249
|
-
if (i === PARENT_PROP_NAME || i === CURRENT_PATH_NAME || i === LIST_ITEM) {
|
|
250
|
-
// eslint-disable-next-line no-continue
|
|
251
|
-
continue;
|
|
252
|
-
}
|
|
253
|
-
if (func({ key: i, value: oo[i], path, parent: oo, level: path.length })) {
|
|
254
|
-
yield { key: i, value: oo[i], path, parent: oo, level: path.length };
|
|
255
|
-
}
|
|
256
|
-
if (oo[i] !== null && typeof oo[i] === 'object') {
|
|
257
|
-
yield* innerTraversal(oo[i], itemPath);
|
|
258
|
-
}
|
|
557
|
+
catch {
|
|
558
|
+
// fallback
|
|
259
559
|
}
|
|
260
560
|
}
|
|
261
|
-
|
|
561
|
+
return value;
|
|
562
|
+
};
|
|
563
|
+
};/**
|
|
564
|
+
* Resolves $modifier (and nested values) anywhere in the tree.
|
|
565
|
+
* Modifiers can appear at component root (value, onChange, etc.), in nested
|
|
566
|
+
* structures (e.g. style.fontSize, style.base.padding), or in $child* slot content.
|
|
567
|
+
* We recurse into objects and arrays so every occurrence is resolved.
|
|
568
|
+
*/
|
|
569
|
+
//TODO add unit test to the resolveModifier
|
|
570
|
+
async function resolveModifier(value, modifiers, ctx) {
|
|
571
|
+
// If it's a direct modifier object: { $modifier: 'x', ...params }
|
|
572
|
+
if (value != null && typeof value === 'object' && MODIFIER_KEY in value) {
|
|
573
|
+
const { [MODIFIER_KEY]: mod, ...params } = value;
|
|
574
|
+
const resolvedParams = {};
|
|
575
|
+
for (const [k, v] of Object.entries(params)) {
|
|
576
|
+
resolvedParams[k] = await resolveModifier(v, modifiers, ctx);
|
|
577
|
+
}
|
|
578
|
+
const handler = modifiers[mod] ?? (mod === 'get' ? createGetModifier(ctx.formStore) : undefined);
|
|
579
|
+
if (!handler)
|
|
580
|
+
return undefined;
|
|
581
|
+
const result = handler(resolvedParams, ctx);
|
|
582
|
+
return result instanceof Promise ? await result : result;
|
|
583
|
+
}
|
|
584
|
+
// If it's an array, resolve modifiers in each element.
|
|
585
|
+
if (Array.isArray(value)) {
|
|
586
|
+
const resolved = await Promise.all(value.map((item) => resolveModifier(item, modifiers, ctx)));
|
|
587
|
+
return resolved;
|
|
262
588
|
}
|
|
263
|
-
//
|
|
264
|
-
|
|
265
|
-
|
|
589
|
+
// If it's a plain object without $modifier, traverse its properties
|
|
590
|
+
// so nested fields like www2.bbb or payload.value get resolved.
|
|
591
|
+
if (value != null && typeof value === 'object') {
|
|
592
|
+
const obj = value;
|
|
593
|
+
const entries = Object.entries(obj);
|
|
594
|
+
if (entries.length === 0)
|
|
595
|
+
return value;
|
|
596
|
+
const resolvedObj = {};
|
|
597
|
+
for (const [k, v] of entries) {
|
|
598
|
+
resolvedObj[k] = await resolveModifier(v, modifiers, ctx);
|
|
599
|
+
}
|
|
600
|
+
return resolvedObj;
|
|
266
601
|
}
|
|
267
|
-
|
|
602
|
+
// Primitive or null – nothing to do.
|
|
603
|
+
return value;
|
|
604
|
+
}/**
|
|
605
|
+
* Style types for the shared style layer (web + React Native).
|
|
606
|
+
* Canonical style is a subset of CSS-like keys that we support and map to both platforms.
|
|
607
|
+
*/
|
|
608
|
+
/** Default breakpoint width thresholds (min-width in px) */
|
|
609
|
+
const DEFAULT_BREAKPOINTS = {
|
|
610
|
+
base: 0,
|
|
611
|
+
xs: 0,
|
|
612
|
+
sm: 640,
|
|
613
|
+
md: 768,
|
|
614
|
+
lg: 1024,
|
|
615
|
+
xl: 1280,
|
|
268
616
|
};
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
617
|
+
/** Order for merging: base first, then xs..xl */
|
|
618
|
+
const BREAKPOINT_ORDER = ['base', 'xs', 'sm', 'md', 'lg', 'xl'];/**
|
|
619
|
+
* Resolves canonical or responsive style to platform-specific style (web or React Native).
|
|
620
|
+
* Pure and synchronous; no dependencies.
|
|
621
|
+
*/
|
|
622
|
+
const isResponsiveStyle = (style) => {
|
|
623
|
+
if (typeof style !== 'object')
|
|
624
|
+
return false;
|
|
625
|
+
const keys = Object.keys(style);
|
|
626
|
+
return keys.some((k) => BREAKPOINT_ORDER.includes(k));
|
|
278
627
|
};
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
628
|
+
/**
|
|
629
|
+
* Merge responsive style into a single style: base + all breakpoints up to and including current.
|
|
630
|
+
* Mobile-first: current breakpoint index determines how many layers we merge.
|
|
631
|
+
*/
|
|
632
|
+
const mergeResponsive = (responsive, currentBreakpoint) => {
|
|
633
|
+
const idx = BREAKPOINT_ORDER.indexOf(currentBreakpoint);
|
|
634
|
+
const merged = {};
|
|
635
|
+
for (let i = 0; i <= idx; i++) {
|
|
636
|
+
const key = BREAKPOINT_ORDER[i];
|
|
637
|
+
const block = responsive[key];
|
|
638
|
+
if (block && typeof block === 'object') {
|
|
639
|
+
Object.assign(merged, block);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
return merged;
|
|
291
643
|
};
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
}
|
|
309
|
-
});
|
|
644
|
+
/**
|
|
645
|
+
* Parse "1px solid #ccc" or similar border shorthand into parts.
|
|
646
|
+
*/
|
|
647
|
+
const parseBorder = (value) => {
|
|
648
|
+
const parts = value.trim().split(/\s+/);
|
|
649
|
+
const result = {};
|
|
650
|
+
for (const p of parts) {
|
|
651
|
+
if (/^\d+(\.\d+)?(px)?$/.test(p)) {
|
|
652
|
+
result.width = parseFloat(p);
|
|
653
|
+
}
|
|
654
|
+
else if (['solid', 'dashed', 'dotted', 'none'].includes(p)) {
|
|
655
|
+
result.style = p;
|
|
656
|
+
}
|
|
657
|
+
else if (p.startsWith('#') || p.startsWith('rgb')) {
|
|
658
|
+
result.color = p;
|
|
659
|
+
}
|
|
310
660
|
}
|
|
311
|
-
return
|
|
661
|
+
return result;
|
|
312
662
|
};
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
663
|
+
/**
|
|
664
|
+
* Convert canonical style to React Native-compatible style.
|
|
665
|
+
* - border shorthand -> borderWidth, borderColor, borderStyle
|
|
666
|
+
* - cursor is not supported on RN, omit
|
|
667
|
+
* - numeric values kept as-is where RN expects numbers
|
|
668
|
+
*/
|
|
669
|
+
const toNativeStyle = (canonical) => {
|
|
670
|
+
const out = {};
|
|
671
|
+
for (const [key, value] of Object.entries(canonical)) {
|
|
672
|
+
if (value === undefined)
|
|
673
|
+
continue;
|
|
674
|
+
if (key === 'cursor')
|
|
675
|
+
continue; // not supported on RN
|
|
676
|
+
if (key === 'border' && typeof value === 'string') {
|
|
677
|
+
const { width, style, color } = parseBorder(value);
|
|
678
|
+
if (width !== undefined)
|
|
679
|
+
out.borderWidth = width;
|
|
680
|
+
if (style)
|
|
681
|
+
out.borderStyle = style;
|
|
682
|
+
if (color)
|
|
683
|
+
out.borderColor = color;
|
|
684
|
+
continue;
|
|
685
|
+
}
|
|
686
|
+
if (key === 'borderRadius' && typeof value === 'number') {
|
|
687
|
+
out.borderRadius = value;
|
|
688
|
+
continue;
|
|
689
|
+
}
|
|
690
|
+
if ((key === 'borderTop' || key === 'borderBottom' || key === 'borderLeft' || key === 'borderRight') && typeof value === 'string') {
|
|
691
|
+
const { width, style, color } = parseBorder(value);
|
|
692
|
+
if (width !== undefined)
|
|
693
|
+
out[`${key}Width`] = width;
|
|
694
|
+
if (style)
|
|
695
|
+
out[`${key}Style`] = style;
|
|
696
|
+
if (color)
|
|
697
|
+
out[`${key}Color`] = color;
|
|
698
|
+
continue;
|
|
699
|
+
}
|
|
700
|
+
out[key] = value;
|
|
316
701
|
}
|
|
317
|
-
|
|
318
|
-
|
|
702
|
+
return out;
|
|
703
|
+
};
|
|
704
|
+
/**
|
|
705
|
+
* Length-like style keys that need a unit (px) when the value is a bare number
|
|
706
|
+
* or numeric string (e.g. from a store/modifier like sliderValue).
|
|
707
|
+
*/
|
|
708
|
+
const LENGTH_KEYS = new Set([
|
|
709
|
+
'fontSize',
|
|
710
|
+
'width',
|
|
711
|
+
'height',
|
|
712
|
+
'minWidth',
|
|
713
|
+
'minHeight',
|
|
714
|
+
'maxWidth',
|
|
715
|
+
'maxHeight',
|
|
716
|
+
'margin',
|
|
717
|
+
'marginTop',
|
|
718
|
+
'marginRight',
|
|
719
|
+
'marginBottom',
|
|
720
|
+
'marginLeft',
|
|
721
|
+
'padding',
|
|
722
|
+
'paddingTop',
|
|
723
|
+
'paddingRight',
|
|
724
|
+
'paddingBottom',
|
|
725
|
+
'paddingLeft',
|
|
726
|
+
'top',
|
|
727
|
+
'right',
|
|
728
|
+
'bottom',
|
|
729
|
+
'left',
|
|
730
|
+
'gap',
|
|
731
|
+
]);
|
|
732
|
+
const ensureLengthUnit = (key, value) => {
|
|
733
|
+
if (value === undefined)
|
|
734
|
+
return value;
|
|
735
|
+
if (!LENGTH_KEYS.has(key))
|
|
736
|
+
return value;
|
|
737
|
+
if (typeof value === 'number')
|
|
738
|
+
return `${value}px`;
|
|
739
|
+
if (typeof value === 'string' && /^\d+(\.\d+)?$/.test(value.trim())) {
|
|
740
|
+
return `${value.trim()}px`;
|
|
319
741
|
}
|
|
320
|
-
return
|
|
742
|
+
return value;
|
|
321
743
|
};
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
744
|
+
/**
|
|
745
|
+
* Web: pass through with minimal changes. Normalize length props (e.g. fontSize)
|
|
746
|
+
* so numeric or numeric-string values get "px" and the browser applies them.
|
|
747
|
+
*/
|
|
748
|
+
const toWebStyle = (canonical) => {
|
|
749
|
+
const out = {};
|
|
750
|
+
for (const [key, value] of Object.entries(canonical)) {
|
|
751
|
+
out[key] = ensureLengthUnit(key, value);
|
|
752
|
+
}
|
|
753
|
+
return out;
|
|
754
|
+
};
|
|
755
|
+
/**
|
|
756
|
+
* Resolve style input (canonical or responsive) to platform-specific style.
|
|
757
|
+
* - If style is responsive (has base/xs/sm/md/lg/xl), merge up to current breakpoint, then resolve.
|
|
758
|
+
* - If no breakpoint is provided for a responsive style, only "base" is used.
|
|
759
|
+
*/
|
|
760
|
+
const resolveStyle = (style, options) => {
|
|
761
|
+
if (!style || typeof style !== 'object')
|
|
334
762
|
return undefined;
|
|
335
|
-
|
|
336
|
-
if (
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
listLength = !!list && Array.isArray(list) ? list.length : 0;
|
|
340
|
-
}
|
|
763
|
+
let canonical;
|
|
764
|
+
if (isResponsiveStyle(style)) {
|
|
765
|
+
const breakpoint = options.breakpoint ?? 'base';
|
|
766
|
+
canonical = mergeResponsive(style, breakpoint);
|
|
341
767
|
}
|
|
342
|
-
|
|
343
|
-
|
|
768
|
+
else {
|
|
769
|
+
canonical = style;
|
|
344
770
|
}
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
},
|
|
356
|
-
} }));
|
|
771
|
+
if (Object.keys(canonical).length === 0)
|
|
772
|
+
return undefined;
|
|
773
|
+
return options.platform === 'native' ? toNativeStyle(canonical) : toWebStyle(canonical);
|
|
774
|
+
};let inlineAjv = null;
|
|
775
|
+
const getInlineAjv = () => {
|
|
776
|
+
if (!inlineAjv) {
|
|
777
|
+
// TODO why it's no strict?
|
|
778
|
+
inlineAjv = new Ajv({ allErrors: true, strict: false });
|
|
779
|
+
addFormats(inlineAjv);
|
|
780
|
+
ajvErrors(inlineAjv);
|
|
357
781
|
}
|
|
358
|
-
return
|
|
782
|
+
return inlineAjv;
|
|
359
783
|
};
|
|
360
|
-
const
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
if (
|
|
364
|
-
|
|
365
|
-
props[V_CHILDREN_NAME] = genChildenFromListItem(props, stock);
|
|
366
|
-
}
|
|
367
|
-
// eslint-disable-next-line no-param-reassign
|
|
368
|
-
props[REDUX_GET_SUBSCRIBERS_NAME] = subscriberPaths;
|
|
369
|
-
};
|
|
370
|
-
const isChildrenProp = (propName) => !!propName && typeof propName === 'string' && propName.startsWith(V_CHILDREN_PREFIX);
|
|
371
|
-
const getParentProps = (props) => {
|
|
372
|
-
return props && typeof props === 'object' && !Array.isArray(props)
|
|
373
|
-
? Object.keys(props)
|
|
374
|
-
.filter((key) => !isChildrenProp(key) && key !== PARENT_PROP_NAME)
|
|
375
|
-
.reduce((newObj, key) => {
|
|
376
|
-
// eslint-disable-next-line no-param-reassign
|
|
377
|
-
newObj[key] = props[key];
|
|
378
|
-
return newObj;
|
|
379
|
-
}, {})
|
|
380
|
-
: {};
|
|
381
|
-
};
|
|
382
|
-
const getPropsChildrenFilter = ({ props, filter }) => props && typeof props === 'object' && !Array.isArray(props)
|
|
383
|
-
? Object.keys(props)
|
|
384
|
-
.filter((key) => ((filter === 'withoutChildren' && !isChildrenProp(key)) || (filter === 'onlyChildren' && isChildrenProp(key))) && key !== PARENT_PROP_NAME)
|
|
385
|
-
.reduce((newObj, key) => {
|
|
386
|
-
// eslint-disable-next-line no-param-reassign
|
|
387
|
-
newObj[key] = props[key];
|
|
388
|
-
return newObj;
|
|
389
|
-
}, {})
|
|
390
|
-
: {};
|
|
391
|
-
const getChildrensForRoot = (props, children, Wrapper) => {
|
|
392
|
-
// eslint-disable-next-line no-nested-ternary
|
|
393
|
-
if (!!props && Array.isArray(children)) {
|
|
394
|
-
return children.map((childrenItem, index) => {
|
|
395
|
-
// eslint-disable-next-line react/no-array-index-key
|
|
396
|
-
return jsx(Wrapper, { props: normalisePrimitives(childrenItem, getParentProps(props)) }, index);
|
|
397
|
-
});
|
|
784
|
+
const stringifyValidationError = (error) => {
|
|
785
|
+
if (typeof error === 'string')
|
|
786
|
+
return error;
|
|
787
|
+
if (typeof error === 'object' && error !== null && 'message' in error && typeof error.message === 'string') {
|
|
788
|
+
return error.message;
|
|
398
789
|
}
|
|
399
|
-
|
|
400
|
-
return
|
|
790
|
+
try {
|
|
791
|
+
return String(error);
|
|
401
792
|
}
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
const generateChildren = (props, { Wrapper }) => props[V_COMP_NAME] !== '_PrimitiveProp' ? getChildrensForRoot(props, props[V_CHILDREN_NAME], Wrapper) : props[V_CHILDREN_NAME];
|
|
405
|
-
const generateNewChildren = (props, { Wrapper }) => {
|
|
406
|
-
// eslint-disable-next-line no-nested-ternary
|
|
407
|
-
if (props) {
|
|
408
|
-
if (Array.isArray(props)) {
|
|
409
|
-
return props.map((childrenItem, index) => {
|
|
410
|
-
// eslint-disable-next-line react/no-array-index-key
|
|
411
|
-
return jsx(Wrapper, { props: normalisePrimitives(childrenItem, getParentProps(props)) }, index);
|
|
412
|
-
});
|
|
413
|
-
}
|
|
414
|
-
return jsx(Wrapper, { props: normalisePrimitives(props, getParentProps(props)) });
|
|
793
|
+
catch {
|
|
794
|
+
return 'error';
|
|
415
795
|
}
|
|
416
|
-
return undefined;
|
|
417
796
|
};
|
|
418
|
-
const
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
const _a = changeableProps, _b = PARENT_PROP_NAME; _a[_b]; const { style } = _a, _c = STYLE_WEB_NAME; _a[_c]; const _d = V_COMP_NAME; _a[_d]; const _e = PATH_MODIFIERS_KEY; _a[_e]; const _f = CURRENT_PATH_NAME; _a[_f]; const _g = REDUX_GET_SUBSCRIBERS_NAME; _a[_g]; const _h = PATH_MODIFIERS_KEY; _a[_h]; const _j = LIST_SEMAPHORE; _a[_j]; const _k = LIST_ITEM; _a[_k]; const _l = LIST_PAGE; _a[_l]; const _m = LIST_ITEM_PER_PAGE; _a[_m]; const _o = LIST_LENGTH; _a[_o]; const _p = REF_VALIDATES; _a[_p]; const newProps = __rest(_a, [_b + "", "style", _c + "", _d + "", _e + "", _f + "", _g + "", _h + "", _j + "", _k + "", _l + "", _m + "", _o + "", _p + ""]);
|
|
436
|
-
return newProps;
|
|
437
|
-
};var wrapperUtil=/*#__PURE__*/Object.freeze({__proto__:null,actionBuilder:actionBuilder,calculatePropsFromModifier:calculatePropsFromModifier,generateChildren:generateChildren,generateNewChildren:generateNewChildren,getChildrensForRoot:getChildrensForRoot,getCurrentPaths:getCurrentPaths,getFilteredPath:getFilteredPath,getParentProps:getParentProps,getPropsChildrenFilter:getPropsChildrenFilter,getRootWrapperProps:getRootWrapperProps,isChildrenProp:isChildrenProp,isTechnicalProp:isTechnicalProp,normalisePrimitives:normalisePrimitives,removeTechnicalProps:removeTechnicalProps});const I18nSchema = {
|
|
438
|
-
$id: 'http://example.com/schemas/schema.json',
|
|
439
|
-
type: 'object',
|
|
440
|
-
additionalProperties: {
|
|
441
|
-
type: 'object',
|
|
442
|
-
properties: {
|
|
443
|
-
translation: {
|
|
444
|
-
type: 'object',
|
|
445
|
-
additionalProperties: {
|
|
446
|
-
type: 'string',
|
|
447
|
-
},
|
|
448
|
-
propertyNames: {
|
|
449
|
-
type: 'string',
|
|
450
|
-
},
|
|
451
|
-
minProperties: 1,
|
|
452
|
-
},
|
|
453
|
-
additionalProperties: false,
|
|
454
|
-
},
|
|
455
|
-
},
|
|
456
|
-
propertyNames: {
|
|
457
|
-
pattern: '^[A-Za-z0-9_-]*$',
|
|
458
|
-
type: 'string',
|
|
459
|
-
},
|
|
460
|
-
minProperties: 1,
|
|
797
|
+
const buildValidationRegistry = (rules) => {
|
|
798
|
+
const registry = {};
|
|
799
|
+
if (!rules || rules.length === 0)
|
|
800
|
+
return registry;
|
|
801
|
+
const ajv = new Ajv({ allErrors: true, strict: false });
|
|
802
|
+
// TODO: check test how looks like in jsonui
|
|
803
|
+
addFormats(ajv);
|
|
804
|
+
ajvErrors(ajv);
|
|
805
|
+
for (const rule of rules) {
|
|
806
|
+
if (rule.schema === undefined || rule.schema === null || !rule.store || !rule.path)
|
|
807
|
+
continue;
|
|
808
|
+
const validate = ajv.compile(rule.schema);
|
|
809
|
+
const byStore = registry[rule.store] ?? (registry[rule.store] = {});
|
|
810
|
+
const list = byStore[rule.path] ?? (byStore[rule.path] = []);
|
|
811
|
+
list.push(validate);
|
|
812
|
+
}
|
|
813
|
+
return registry;
|
|
461
814
|
};
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
this.keyPostfix = keyPostfix;
|
|
489
|
-
const ajv = new Ajv();
|
|
490
|
-
const validate = ajv.compile(I18nSchema);
|
|
491
|
-
const isValid = validate(resources);
|
|
492
|
-
if (isValid) {
|
|
493
|
-
this.resources = resources;
|
|
494
|
-
}
|
|
495
|
-
this.languages = Object.keys(resources);
|
|
496
|
-
if (this.languages && this.languages.includes(this.language)) {
|
|
497
|
-
this.availableLanguageKey = this.language;
|
|
498
|
-
}
|
|
499
|
-
else if (this.languages && this.languages.includes(this.getLocales())) {
|
|
500
|
-
this.availableLanguageKey = this.getLocales();
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
}const StockContext = createContext(null);
|
|
504
|
-
const PathModifierContext = createContext({});// eslint-disable-next-line no-shadow
|
|
505
|
-
var ReduxPathTypeEnum;
|
|
506
|
-
(function (ReduxPathTypeEnum) {
|
|
507
|
-
ReduxPathTypeEnum["ERROR"] = "ERROR";
|
|
508
|
-
ReduxPathTypeEnum["TOUCH"] = "TOUCH";
|
|
509
|
-
ReduxPathTypeEnum["NORMAL"] = "NORMAL";
|
|
510
|
-
})(ReduxPathTypeEnum || (ReduxPathTypeEnum = {}));const getState = (state) => state === null || state === void 0 ? void 0 : state.root;
|
|
511
|
-
const getValue = (state, store, path) => jsonPointerGet(state[store], path);
|
|
512
|
-
const getStoreNameFromType = (store, type) =>
|
|
513
|
-
// eslint-disable-next-line no-nested-ternary
|
|
514
|
-
type === ReduxPathTypeEnum.ERROR ? `${store}${STORE_ERROR_POSTFIX}` : type === ReduxPathTypeEnum.TOUCH ? `${store}${STORE_TOUCH_POSTFIX}` : `${store}`;
|
|
515
|
-
const getStateValue = (globalState, { store, path, type, jsonataDef }, currentPaths) => {
|
|
516
|
-
const state = getState(globalState);
|
|
517
|
-
if (state && store && path) {
|
|
518
|
-
const convertedPath = currentPaths && currentPaths[store] && currentPaths[store].path ? changeRelativePath(`${currentPaths[store].path}${SEPARATOR}${path}`) : path;
|
|
519
|
-
const storeName = getStoreNameFromType(store, type);
|
|
520
|
-
let value = getValue(state, storeName, convertedPath);
|
|
521
|
-
if (jsonataDef) {
|
|
522
|
-
try {
|
|
523
|
-
const expression = jsonata$1(jsonataDef);
|
|
524
|
-
value = expression.evaluate(value);
|
|
525
|
-
}
|
|
526
|
-
catch (error) {
|
|
527
|
-
// eslint-disable-next-line no-console
|
|
528
|
-
console.error('jsonata error', error, jsonataDef);
|
|
815
|
+
/**
|
|
816
|
+
* Run a single inline (field-level) validation spec against the current store state.
|
|
817
|
+
*
|
|
818
|
+
* The component's own store name and resolved logical path are passed in directly —
|
|
819
|
+
* they come from the simplified component's `store`/`path` props, not from the spec.
|
|
820
|
+
*
|
|
821
|
+
* Supports two validation styles:
|
|
822
|
+
* - schema: AJV JSON Schema validation
|
|
823
|
+
* - jsonataDef + errorMessage (not mandatory): JSONata expression; error shown when result is
|
|
824
|
+
* not null, undefined, empty string, or true. errorMessage may be a plain string
|
|
825
|
+
* or a { $modifier: ... } expression resolved via resolveModifier.
|
|
826
|
+
*/
|
|
827
|
+
const runInlineValidation = async (spec, componentStoreName, componentLogicalPath, modifiers, ctx) => {
|
|
828
|
+
const { formStore } = ctx;
|
|
829
|
+
const errorStoreName = `${componentStoreName}${ERROR_STORE_SUFFIX}`;
|
|
830
|
+
const value = formStore.get(componentStoreName, componentLogicalPath);
|
|
831
|
+
if (spec.schema != null) {
|
|
832
|
+
const ajv = getInlineAjv();
|
|
833
|
+
const validate = ajv.compile(spec.schema);
|
|
834
|
+
const messages = [];
|
|
835
|
+
const valid = validate(value);
|
|
836
|
+
//TODO: need to outsource validation to separate function and test it
|
|
837
|
+
if (!valid && validate.errors) {
|
|
838
|
+
for (const err of validate.errors) {
|
|
839
|
+
if (err.message)
|
|
840
|
+
messages.push(err.message);
|
|
529
841
|
}
|
|
530
842
|
}
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
843
|
+
const newError = messages.length > 0 ? messages.join('; ') : null;
|
|
844
|
+
const currentError = formStore.get(errorStoreName, componentLogicalPath);
|
|
845
|
+
if ((currentError ?? null) !== newError) {
|
|
846
|
+
formStore.set(errorStoreName, componentLogicalPath, newError, false);
|
|
534
847
|
}
|
|
535
|
-
|
|
536
|
-
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
if (spec.jsonataDef != null) {
|
|
851
|
+
let result;
|
|
852
|
+
try {
|
|
853
|
+
const jsonata = (await import('jsonata')).default;
|
|
854
|
+
const expr = jsonata(spec.jsonataDef);
|
|
855
|
+
result = await expr.evaluate(value);
|
|
537
856
|
}
|
|
538
|
-
|
|
857
|
+
catch (e) {
|
|
858
|
+
result = stringifyValidationError(e);
|
|
859
|
+
}
|
|
860
|
+
const hasError = result !== null && result !== undefined && result !== '' && result !== true;
|
|
861
|
+
const newError = hasError ? (spec.errorMessage ? String(await resolveModifier(spec.errorMessage, modifiers, ctx)) : String(result)) : null;
|
|
862
|
+
const currentError = formStore.get(errorStoreName, componentLogicalPath);
|
|
863
|
+
if ((currentError ?? null) !== newError) {
|
|
864
|
+
formStore.set(errorStoreName, componentLogicalPath, newError, false);
|
|
865
|
+
}
|
|
866
|
+
return;
|
|
539
867
|
}
|
|
540
|
-
return null;
|
|
541
868
|
};
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
869
|
+
// Runs all validators whose rule path matches the changed path prefix for the given store.
|
|
870
|
+
// Aggregates current validation messages and writes or clears them in the matching .error store paths.
|
|
871
|
+
const runValidationsForPath = (registry, formStore, storeName, path) => {
|
|
872
|
+
const storeValidators = registry[storeName];
|
|
873
|
+
// No validators registered for this store at all
|
|
874
|
+
if (!storeValidators) {
|
|
875
|
+
return;
|
|
545
876
|
}
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
const
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
const
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
const evaluate = expression.evaluate(attr);
|
|
568
|
-
// console.log('evaluate: ', evaluate)
|
|
569
|
-
return evaluate;
|
|
877
|
+
const errorStoreName = `${storeName}${ERROR_STORE_SUFFIX}`;
|
|
878
|
+
// Collect new error messages per *concrete* target path (e.g. '/players/0/score').
|
|
879
|
+
const perPathMessages = {};
|
|
880
|
+
const affectedErrorPaths = new Set();
|
|
881
|
+
// Include any existing error leaf paths under matching rule paths so we can clear
|
|
882
|
+
// them if they become valid.
|
|
883
|
+
const collectExistingPaths = (basePath, value) => {
|
|
884
|
+
if (value == null)
|
|
885
|
+
return;
|
|
886
|
+
if (typeof value !== 'object') {
|
|
887
|
+
affectedErrorPaths.add(basePath);
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
if (Array.isArray(value)) {
|
|
891
|
+
value.forEach((v, i) => {
|
|
892
|
+
collectExistingPaths(`${basePath}/${i}`, v);
|
|
893
|
+
});
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
for (const [k, v] of Object.entries(value)) {
|
|
897
|
+
collectExistingPaths(basePath === '/' ? `/${k}` : `${basePath}/${k}`, v);
|
|
570
898
|
}
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
899
|
+
};
|
|
900
|
+
// Run all validators whose rule path is a prefix of the affected path.
|
|
901
|
+
// Example: action path '/a/b/c' should trigger validators registered for
|
|
902
|
+
// '/', '/a', '/a/b', and '/a/b/c'.
|
|
903
|
+
for (const [rulePath, validators] of Object.entries(storeValidators)) {
|
|
904
|
+
if (!validators)
|
|
905
|
+
continue;
|
|
906
|
+
if (!isPathPrefix(rulePath, path))
|
|
907
|
+
continue;
|
|
908
|
+
// Track existing error paths under this rule so we can clear them.
|
|
909
|
+
const existingSubtree = formStore.get(errorStoreName, rulePath);
|
|
910
|
+
if (existingSubtree !== undefined) {
|
|
911
|
+
collectExistingPaths(rulePath === '' ? '/' : rulePath, existingSubtree);
|
|
912
|
+
}
|
|
913
|
+
const valueAtRulePath = formStore.get(storeName, rulePath);
|
|
914
|
+
for (const validate of validators) {
|
|
915
|
+
const valid = validate(valueAtRulePath);
|
|
916
|
+
if (!valid && validate.errors) {
|
|
917
|
+
for (const err of validate.errors) {
|
|
918
|
+
if (!err.message)
|
|
919
|
+
continue;
|
|
920
|
+
const instancePath = err.instancePath;
|
|
921
|
+
// instancePath is relative to rulePath, e.g. '/0/score'
|
|
922
|
+
let targetPath;
|
|
923
|
+
if (rulePath === '' || rulePath === '/') {
|
|
924
|
+
targetPath = instancePath && instancePath.length > 0 ? instancePath : '/';
|
|
925
|
+
}
|
|
926
|
+
else {
|
|
927
|
+
targetPath = instancePath && instancePath.length > 0 ? `${rulePath}${instancePath}` : rulePath;
|
|
928
|
+
}
|
|
929
|
+
const list = perPathMessages[targetPath] ?? (perPathMessages[targetPath] = []);
|
|
930
|
+
list.push(err.message);
|
|
931
|
+
affectedErrorPaths.add(targetPath);
|
|
932
|
+
}
|
|
933
|
+
}
|
|
574
934
|
}
|
|
575
935
|
}
|
|
576
|
-
|
|
936
|
+
// Apply updates for all affected error paths (both existing and new).
|
|
937
|
+
for (const targetPath of affectedErrorPaths) {
|
|
938
|
+
const messages = perPathMessages[targetPath] ?? [];
|
|
939
|
+
const newError = messages.length > 0 ? messages.join('; ') : null;
|
|
940
|
+
const currentError = formStore.get(errorStoreName, targetPath);
|
|
941
|
+
if ((currentError ?? null) === newError)
|
|
942
|
+
continue;
|
|
943
|
+
formStore.set(errorStoreName, targetPath, newError, false);
|
|
944
|
+
}
|
|
577
945
|
};
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
946
|
+
/** Match whole segments only; works for paths of any depth (e.g. /a/b/0/c). */
|
|
947
|
+
const isPathPrefix = (rulePath, targetPath) => {
|
|
948
|
+
const r = rulePath === '' ? '/' : rulePath;
|
|
949
|
+
const t = targetPath === '' ? '/' : targetPath;
|
|
950
|
+
if (r === '/')
|
|
951
|
+
return true;
|
|
952
|
+
if (t === r)
|
|
953
|
+
return true;
|
|
954
|
+
return t.startsWith(r.endsWith('/') ? r : `${r}/`);
|
|
955
|
+
};/**
|
|
956
|
+
* Recursively walks a node's props value tree and collects all store path dependencies
|
|
957
|
+
* introduced by `$modifier: 'get'` entries. For each `get` modifier found, it resolves
|
|
958
|
+
* the logical store path (accounting for path modifiers and ERROR/TOUCH type variants)
|
|
959
|
+
* and appends it to the `deps` array. Nodes with a `$comp` key are skipped to avoid
|
|
960
|
+
* descending into nested component definitions.
|
|
961
|
+
*/
|
|
962
|
+
const collectGetModifierDependencies = (val, currentPath, deps, effectivePathModifiers) => {
|
|
963
|
+
if (val && typeof val === 'object' && !Array.isArray(val) && V_COMP in val) {
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
966
|
+
if (val && typeof val === 'object' && !Array.isArray(val) && MODIFIER_KEY in val && val[MODIFIER_KEY] === 'get') {
|
|
967
|
+
const storeName = val.store;
|
|
968
|
+
const type = val.type;
|
|
969
|
+
const path = val.path;
|
|
970
|
+
const alteredStoreName = type === 'ERROR' ? `${storeName ?? ''}${ERROR_STORE_SUFFIX}` : type === 'TOUCH' ? `${storeName ?? ''}${TOUCH_STORE_SUFFIX}` : storeName;
|
|
971
|
+
if (storeName && alteredStoreName) {
|
|
972
|
+
if (typeof path === 'string' && path) {
|
|
973
|
+
const logicalPath = resolveStorePath(path, currentPath, effectivePathModifiers, storeName);
|
|
974
|
+
deps.push({ store: alteredStoreName, path: logicalPath });
|
|
595
975
|
}
|
|
596
976
|
else {
|
|
597
|
-
|
|
977
|
+
deps.push({ store: alteredStoreName, path: '/' });
|
|
598
978
|
}
|
|
599
|
-
}
|
|
979
|
+
}
|
|
980
|
+
return;
|
|
981
|
+
}
|
|
982
|
+
if (Array.isArray(val)) {
|
|
983
|
+
val.forEach((v) => collectGetModifierDependencies(v, currentPath, deps, effectivePathModifiers));
|
|
984
|
+
return;
|
|
985
|
+
}
|
|
986
|
+
if (val && typeof val === 'object') {
|
|
987
|
+
for (const v of Object.values(val)) {
|
|
988
|
+
collectGetModifierDependencies(v, currentPath, deps, effectivePathModifiers);
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
};const runValidationSpecsFromNode = async (node, modifiers, ctx, componentStoreName, componentLogicalPath) => {
|
|
992
|
+
const rawValidation = node[V_VALIDATIONS];
|
|
993
|
+
if (!rawValidation || !Array.isArray(rawValidation) || rawValidation.length === 0)
|
|
994
|
+
return;
|
|
995
|
+
if (!componentStoreName || componentLogicalPath == null)
|
|
996
|
+
return;
|
|
997
|
+
for (const item of rawValidation) {
|
|
998
|
+
if (item === null || typeof item !== 'object')
|
|
999
|
+
continue;
|
|
1000
|
+
await runInlineValidation(item, componentStoreName, componentLogicalPath, modifiers, ctx);
|
|
600
1001
|
}
|
|
601
|
-
// TODO: if it's a root level validation, looks like : {-: 'should be email'} should be converted
|
|
602
|
-
return res && res['-'] ? res['-'] : res;
|
|
603
|
-
};
|
|
604
|
-
const validateJSON = (schema, data) => {
|
|
605
|
-
const ajv = new Ajv({ allErrors: true });
|
|
606
|
-
ajvErrors(ajv);
|
|
607
|
-
ajvFormats(ajv);
|
|
608
|
-
const validate = ajv.compile(schema);
|
|
609
|
-
const valid = validate(data);
|
|
610
|
-
return {
|
|
611
|
-
valid: validate(data),
|
|
612
|
-
error: valid ? null : errorConverter(validate.errors),
|
|
613
|
-
};
|
|
614
1002
|
};
|
|
615
|
-
const
|
|
616
|
-
const
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
if (validateItem.schema) {
|
|
628
|
-
const state = current(newState);
|
|
629
|
-
const stateToBeValidated = jsonPointerGet(state, `${SEPARATOR}${actionStore}${validateItem.path}`);
|
|
630
|
-
const errors = validateJSONAndStore(validateItem.schema, actionStore, stateToBeValidated);
|
|
631
|
-
// eslint-disable-next-line no-param-reassign
|
|
632
|
-
newState = jsonPointerSet(newState, `${SEPARATOR}${errors.store}${validateItem.path}`, errors.value);
|
|
633
|
-
}
|
|
1003
|
+
const runRenderNodeResolution = async ({ node, modifiers, ctx, currentPath, effectivePathModifiers, stylePlatform, styleBreakpoint, componentStore, componentPath, }) => {
|
|
1004
|
+
const props = {};
|
|
1005
|
+
const deps = [];
|
|
1006
|
+
const resolvedSlots = {};
|
|
1007
|
+
for (const [key, value] of Object.entries(node)) {
|
|
1008
|
+
if (key.startsWith('$child') || !key.startsWith('$')) {
|
|
1009
|
+
collectGetModifierDependencies(value, currentPath, deps, effectivePathModifiers);
|
|
1010
|
+
if (key.startsWith('$child')) {
|
|
1011
|
+
resolvedSlots[key] = await resolveModifier(value, modifiers, ctx);
|
|
1012
|
+
}
|
|
1013
|
+
else if (!key.startsWith('$')) {
|
|
1014
|
+
props[key] = await resolveModifier(value, modifiers, ctx);
|
|
634
1015
|
}
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
if (props.style != null && typeof props.style === 'object') {
|
|
1019
|
+
const resolved = resolveStyle(props.style, {
|
|
1020
|
+
platform: stylePlatform,
|
|
1021
|
+
breakpoint: styleBreakpoint,
|
|
635
1022
|
});
|
|
1023
|
+
props.style = resolved ?? props.style;
|
|
636
1024
|
}
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
const expression = jsonata$1(jsonataDef);
|
|
653
|
-
const newValue = expression.evaluate(value);
|
|
654
|
-
// eslint-disable-next-line no-param-reassign
|
|
655
|
-
draft = jsonPointerSet(draft, absolutePathWithStoreKey, newValue);
|
|
656
|
-
}
|
|
657
|
-
catch (error) {
|
|
658
|
-
// eslint-disable-next-line no-console
|
|
659
|
-
console.error('jsonata error', error, jsonataDef);
|
|
660
|
-
// eslint-disable-next-line no-param-reassign
|
|
661
|
-
draft = jsonPointerSet(draft, absolutePathWithStoreKey, value);
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
|
-
else {
|
|
665
|
-
// eslint-disable-next-line no-param-reassign
|
|
666
|
-
draft = jsonPointerSet(draft, absolutePathWithStoreKey, value);
|
|
667
|
-
}
|
|
668
|
-
// set, if a leaf touched
|
|
669
|
-
// TODO if array or object touched, can easily overwite the leaf touched
|
|
670
|
-
// eslint-disable-next-line no-param-reassign
|
|
671
|
-
draft = jsonPointerSet(draft, `${SEPARATOR}${storekey}${STORE_TOUCH_POSTFIX}${convertedPath}`, true);
|
|
672
|
-
// if validatior has match, need to validate it synchronously
|
|
673
|
-
globalValidateNewState(stock, draft, store, convertedPath);
|
|
674
|
-
});
|
|
675
|
-
return newState;
|
|
676
|
-
}
|
|
677
|
-
return state;
|
|
1025
|
+
const resolvedComponentPath = componentStore && componentPath != null ? resolveStorePath(componentPath, currentPath, effectivePathModifiers, componentStore) : undefined;
|
|
1026
|
+
await runValidationSpecsFromNode(node, modifiers, ctx, componentStore, resolvedComponentPath);
|
|
1027
|
+
return {
|
|
1028
|
+
state: { props, resolvedSlots },
|
|
1029
|
+
deps,
|
|
1030
|
+
};
|
|
1031
|
+
};const resolveAction = (value, actions, modifiers, ctx) => {
|
|
1032
|
+
if (value != null && typeof value === 'object' && ACTION_KEY in value) {
|
|
1033
|
+
const { [ACTION_KEY]: actionName, ...params } = value;
|
|
1034
|
+
const hasExplicitValue = Object.prototype.hasOwnProperty.call(value, 'value');
|
|
1035
|
+
// Extract modifiers from action ctx.
|
|
1036
|
+
const { componentProps, validators, ...modifierCtx } = ctx;
|
|
1037
|
+
let handler = actions[actionName];
|
|
1038
|
+
if (!handler && actionName === 'set') {
|
|
1039
|
+
handler = createSetAction(ctx.formStore);
|
|
678
1040
|
}
|
|
679
|
-
|
|
680
|
-
return
|
|
1041
|
+
if (!handler)
|
|
1042
|
+
return undefined;
|
|
1043
|
+
return async (e) => {
|
|
1044
|
+
// TODO: research, why componentProps need
|
|
1045
|
+
const resolvedParams = { ...componentProps, ...params };
|
|
1046
|
+
// Case 1: value from event (input onChange) – only when the model
|
|
1047
|
+
// did NOT define a value explicitly.
|
|
1048
|
+
if (!hasExplicitValue && e != null && typeof e === 'object' && 'target' in e) {
|
|
1049
|
+
const target = e.target;
|
|
1050
|
+
if (target?.value !== undefined)
|
|
1051
|
+
resolvedParams.value = target.value;
|
|
1052
|
+
}
|
|
1053
|
+
// Cases 2 & 3: static JSON value or nested $modifier value – both
|
|
1054
|
+
// flow through resolveModifier below and are preserved.
|
|
1055
|
+
for (const [k, v] of Object.entries(resolvedParams)) {
|
|
1056
|
+
resolvedParams[k] = await resolveModifier(v, modifiers, modifierCtx);
|
|
1057
|
+
}
|
|
1058
|
+
const result = handler(resolvedParams, ctx);
|
|
1059
|
+
if (result instanceof Promise)
|
|
1060
|
+
await result;
|
|
1061
|
+
// Run validations for this store/path if configured
|
|
1062
|
+
const storeName = resolvedParams.store;
|
|
1063
|
+
const rawPath = resolvedParams.path;
|
|
1064
|
+
if (validators && storeName && rawPath) {
|
|
1065
|
+
// Resolve to logical path so validations work with lists, pathModifiers,
|
|
1066
|
+
// and relative paths (e.g. "score" inside /players/0).
|
|
1067
|
+
const logicalPath = resolveStorePath(rawPath, ctx.currentPath, ctx.pathModifiers, storeName);
|
|
1068
|
+
runValidationsForPath(validators, ctx.formStore, storeName, logicalPath);
|
|
1069
|
+
}
|
|
1070
|
+
};
|
|
681
1071
|
}
|
|
682
|
-
|
|
1072
|
+
return undefined;
|
|
1073
|
+
};const get = (params, ctx) => {
|
|
1074
|
+
const getFn = createGetModifier(ctx.formStore);
|
|
1075
|
+
return getFn(params, ctx);
|
|
1076
|
+
};async function jsonata(params) {
|
|
1077
|
+
const { jsonataDef, ...input } = params;
|
|
1078
|
+
if (typeof jsonataDef !== 'string' || !jsonataDef)
|
|
1079
|
+
return undefined;
|
|
1080
|
+
try {
|
|
1081
|
+
const jsonata = (await import('jsonata')).default;
|
|
1082
|
+
const expr = jsonata(jsonataDef);
|
|
1083
|
+
// Evaluate with the remaining params (e.g. { error, touched }) as input.
|
|
1084
|
+
return await expr.evaluate(input);
|
|
1085
|
+
}
|
|
1086
|
+
catch {
|
|
1087
|
+
// On any jsonata error, fall back to undefined so UI doesn't break.
|
|
1088
|
+
return undefined;
|
|
1089
|
+
}
|
|
1090
|
+
}const isNotTouchedOrHasError = (params) => {
|
|
1091
|
+
const { error, touched } = params;
|
|
1092
|
+
const hasError = hasAnyError$1(error);
|
|
1093
|
+
const isTouched = hasAnyTouched$1(touched);
|
|
1094
|
+
return !isTouched || hasError;
|
|
1095
|
+
};/**
|
|
1096
|
+
* Translation function: looks up key in ctx.translations using active language.
|
|
1097
|
+
*/
|
|
1098
|
+
const t = (params, ctx) => {
|
|
1099
|
+
const key = params.key;
|
|
1100
|
+
if (!key)
|
|
1101
|
+
return undefined;
|
|
1102
|
+
const translations = ctx.translations ?? {};
|
|
1103
|
+
const langParam = params.lang;
|
|
1104
|
+
const baseLang = ctx.defaultLanguage || 'en';
|
|
1105
|
+
const activeLang = langParam || ctx.activeLanguage || baseLang;
|
|
1106
|
+
const entry = translations[key];
|
|
1107
|
+
// If we're in the base language, or there is no translation table
|
|
1108
|
+
// for this key, just return the key as-is (baseline text).
|
|
1109
|
+
if (activeLang === baseLang || entry === undefined)
|
|
1110
|
+
return key;
|
|
1111
|
+
// Otherwise, use the requested language if available, falling back to key.
|
|
1112
|
+
return entry[activeLang] ?? key;
|
|
1113
|
+
};const modifiers = {
|
|
1114
|
+
get,
|
|
1115
|
+
jsonata,
|
|
1116
|
+
isNotTouchedOrHasError,
|
|
1117
|
+
t,
|
|
1118
|
+
};export{ACTION_KEY,BREAKPOINT_ORDER,DEFAULT_BREAKPOINTS,ERROR_STORE_SUFFIX,FormStore,LIST_ITEM,LIST_ITEM_PER_PAGE,LIST_LENGTH,LIST_PAGE,LIST_SEMAPHORE,MODIFIER_KEY,PATH_MODIFIERS_KEY,TOUCH_STORE_SUFFIX,V_CHILDREN,V_COMP,V_VALIDATIONS,actions,buildValidationRegistry,computeListSliceRange,expandSimplifiedNode,getOwnPathModifiers,isPathPrefix$1 as isPathPrefix,mergeEffectivePathModifiers,modifiers,normalizePath,resolveAction,resolveStorePath,runRenderNodeResolution};//# sourceMappingURL=index.js.map
|