@rettangoli/ui 1.0.0-rc13 → 1.0.0-rc15
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/rettangoli-iife-layout.min.js +81 -49
- package/dist/rettangoli-iife-ui.min.js +150 -49
- package/package.json +10 -5
- package/src/common/dimensions.js +85 -0
- package/src/common/responsive.js +72 -0
- package/src/common.js +6 -4
- package/src/components/dropdownMenu/dropdownMenu.schema.yaml +1 -1
- package/src/components/form/form.handlers.js +328 -152
- package/src/components/form/form.methods.js +205 -0
- package/src/components/form/form.schema.yaml +16 -271
- package/src/components/form/form.store.js +535 -95
- package/src/components/form/form.view.yaml +73 -52
- package/src/components/globalUi/globalUi.handlers.js +4 -4
- package/src/components/popoverInput/popoverInput.handlers.js +64 -50
- package/src/components/popoverInput/popoverInput.schema.yaml +3 -1
- package/src/components/popoverInput/popoverInput.store.js +9 -3
- package/src/components/popoverInput/popoverInput.view.yaml +4 -4
- package/src/components/select/select.handlers.js +15 -19
- package/src/components/select/select.schema.yaml +2 -0
- package/src/components/select/select.store.js +8 -6
- package/src/components/select/select.view.yaml +4 -4
- package/src/components/sliderInput/sliderInput.handlers.js +15 -1
- package/src/components/sliderInput/sliderInput.schema.yaml +3 -0
- package/src/components/sliderInput/sliderInput.store.js +2 -1
- package/src/components/sliderInput/sliderInput.view.yaml +2 -2
- package/src/components/tooltip/tooltip.schema.yaml +1 -1
- package/src/deps/createGlobalUI.js +4 -4
- package/src/entry-iife-layout.js +6 -0
- package/src/entry-iife-ui.js +8 -0
- package/src/index.js +8 -0
- package/src/primitives/checkbox.js +295 -0
- package/src/primitives/input-date.js +31 -0
- package/src/primitives/input-datetime.js +31 -0
- package/src/primitives/input-time.js +31 -0
- package/src/primitives/input.js +43 -1
- package/src/primitives/textarea.js +3 -0
- package/src/primitives/view.js +6 -2
|
@@ -15,35 +15,74 @@ const encode = (input) => {
|
|
|
15
15
|
return ""
|
|
16
16
|
}
|
|
17
17
|
return `"${escapeHtml(String(input))}"`;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const isObjectLike = (value) => value !== null && typeof value === "object";
|
|
21
|
+
const isPlainObject = (value) => isObjectLike(value) && !Array.isArray(value);
|
|
22
|
+
const isPathLike = (path) => typeof path === "string" && path.includes(".");
|
|
23
|
+
const hasBracketPathToken = (path) => typeof path === "string" && /[\[\]]/.test(path);
|
|
24
|
+
|
|
25
|
+
function pickByPaths(obj, paths) {
|
|
26
|
+
const result = {};
|
|
27
|
+
for (const path of paths) {
|
|
28
|
+
if (typeof path !== "string" || path.length === 0) continue;
|
|
29
|
+
const value = get(obj, path);
|
|
30
|
+
if (value !== undefined) {
|
|
31
|
+
set(result, path, value);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return result;
|
|
18
35
|
}
|
|
19
36
|
|
|
20
|
-
function
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
37
|
+
function normalizeWhenDirectives(form) {
|
|
38
|
+
if (!isPlainObject(form) || !Array.isArray(form.fields)) {
|
|
39
|
+
return form;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const normalizeFields = (fields = []) =>
|
|
43
|
+
fields.map((field) => {
|
|
44
|
+
if (!isPlainObject(field)) {
|
|
45
|
+
return field;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (typeof field.$when === "string" && field.$when.trim().length > 0) {
|
|
49
|
+
const { $when, ...rest } = field;
|
|
50
|
+
const normalizedField = Array.isArray(rest.fields)
|
|
51
|
+
? { ...rest, fields: normalizeFields(rest.fields) }
|
|
52
|
+
: rest;
|
|
53
|
+
return {
|
|
54
|
+
[`$if ${$when}`]: normalizedField,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (Array.isArray(field.fields)) {
|
|
59
|
+
return {
|
|
60
|
+
...field,
|
|
61
|
+
fields: normalizeFields(field.fields),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return field;
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
...form,
|
|
70
|
+
fields: normalizeFields(form.fields),
|
|
71
|
+
};
|
|
25
72
|
}
|
|
26
73
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
content: ''
|
|
34
|
-
},
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
// Lodash-like utility functions for nested property access
|
|
38
|
-
const get = (obj, path, defaultValue = undefined) => {
|
|
39
|
-
if (!path) {
|
|
40
|
-
return;
|
|
41
|
-
}
|
|
42
|
-
const keys = path.split(/[\[\].]/).filter((key) => key !== "");
|
|
74
|
+
// Nested property access utilities
|
|
75
|
+
export const get = (obj, path, defaultValue = undefined) => {
|
|
76
|
+
if (!path) return defaultValue;
|
|
77
|
+
if (!isObjectLike(obj)) return defaultValue;
|
|
78
|
+
if (hasBracketPathToken(path)) return defaultValue;
|
|
79
|
+
const keys = path.split(".").filter((key) => key !== "");
|
|
43
80
|
let current = obj;
|
|
44
|
-
|
|
45
81
|
for (const key of keys) {
|
|
46
82
|
if (current === null || current === undefined || !(key in current)) {
|
|
83
|
+
if (Object.prototype.hasOwnProperty.call(obj, path)) {
|
|
84
|
+
return obj[path];
|
|
85
|
+
}
|
|
47
86
|
return defaultValue;
|
|
48
87
|
}
|
|
49
88
|
current = current[key];
|
|
@@ -51,16 +90,21 @@ const get = (obj, path, defaultValue = undefined) => {
|
|
|
51
90
|
return current;
|
|
52
91
|
};
|
|
53
92
|
|
|
54
|
-
const set = (obj, path, value) => {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
if (
|
|
93
|
+
export const set = (obj, path, value) => {
|
|
94
|
+
if (!isObjectLike(obj) || typeof path !== "string" || path.length === 0) {
|
|
95
|
+
return obj;
|
|
96
|
+
}
|
|
97
|
+
if (hasBracketPathToken(path)) {
|
|
98
|
+
return obj;
|
|
99
|
+
}
|
|
100
|
+
const keys = path.split(".").filter((key) => key !== "");
|
|
101
|
+
if (keys.length === 0) {
|
|
102
|
+
return obj;
|
|
103
|
+
}
|
|
104
|
+
if (isPathLike(path) && Object.prototype.hasOwnProperty.call(obj, path)) {
|
|
59
105
|
delete obj[path];
|
|
60
106
|
}
|
|
61
|
-
|
|
62
107
|
let current = obj;
|
|
63
|
-
|
|
64
108
|
for (let i = 0; i < keys.length - 1; i++) {
|
|
65
109
|
const key = keys[i];
|
|
66
110
|
if (
|
|
@@ -68,19 +112,15 @@ const set = (obj, path, value) => {
|
|
|
68
112
|
typeof current[key] !== "object" ||
|
|
69
113
|
current[key] === null
|
|
70
114
|
) {
|
|
71
|
-
|
|
72
|
-
const nextKey = keys[i + 1];
|
|
73
|
-
const isArrayIndex = /^\d+$/.test(nextKey);
|
|
74
|
-
current[key] = isArrayIndex ? [] : {};
|
|
115
|
+
current[key] = {};
|
|
75
116
|
}
|
|
76
117
|
current = current[key];
|
|
77
118
|
}
|
|
78
|
-
|
|
79
119
|
current[keys[keys.length - 1]] = value;
|
|
80
120
|
return obj;
|
|
81
121
|
};
|
|
82
122
|
|
|
83
|
-
const blacklistedAttrs = ["id", "class", "style", "slot", "form", "defaultValues", "
|
|
123
|
+
const blacklistedAttrs = ["id", "class", "style", "slot", "form", "defaultValues", "disabled"];
|
|
84
124
|
|
|
85
125
|
const stringifyAttrs = (props = {}) => {
|
|
86
126
|
return Object.entries(props)
|
|
@@ -89,100 +129,500 @@ const stringifyAttrs = (props = {}) => {
|
|
|
89
129
|
.join(" ");
|
|
90
130
|
};
|
|
91
131
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
132
|
+
// --- Validation ---
|
|
133
|
+
|
|
134
|
+
const PATTERN_PRESETS = {
|
|
135
|
+
email: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
|
|
136
|
+
url: /^https?:\/\/.+/,
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const DEFAULT_MESSAGES = {
|
|
140
|
+
required: "This field is required",
|
|
141
|
+
minLength: (val) => `Must be at least ${val} characters`,
|
|
142
|
+
maxLength: (val) => `Must be at most ${val} characters`,
|
|
143
|
+
pattern: "Invalid format",
|
|
144
|
+
invalidDate: "Invalid date format",
|
|
145
|
+
invalidTime: "Invalid time format",
|
|
146
|
+
invalidDateTime: "Invalid date and time format",
|
|
147
|
+
minTemporal: (val) => `Must be on or after ${val}`,
|
|
148
|
+
maxTemporal: (val) => `Must be on or before ${val}`,
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const DATE_FIELD_TYPE = "input-date";
|
|
152
|
+
const TIME_FIELD_TYPE = "input-time";
|
|
153
|
+
const DATETIME_FIELD_TYPE = "input-datetime";
|
|
154
|
+
|
|
155
|
+
const DATE_REGEX = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
156
|
+
const TIME_REGEX = /^(\d{2}):(\d{2})(?::(\d{2}))?$/;
|
|
157
|
+
const DATETIME_REGEX = /^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}(?::\d{2})?)$/;
|
|
158
|
+
|
|
159
|
+
const parseDateParts = (value) => {
|
|
160
|
+
if (typeof value !== "string") return null;
|
|
161
|
+
const match = DATE_REGEX.exec(value);
|
|
162
|
+
if (!match) return null;
|
|
163
|
+
|
|
164
|
+
const year = Number(match[1]);
|
|
165
|
+
const month = Number(match[2]);
|
|
166
|
+
const day = Number(match[3]);
|
|
167
|
+
if (!Number.isInteger(year) || !Number.isInteger(month) || !Number.isInteger(day)) {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
if (month < 1 || month > 12 || day < 1 || day > 31) {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const date = new Date(Date.UTC(year, month - 1, day));
|
|
175
|
+
const valid = date.getUTCFullYear() === year
|
|
176
|
+
&& date.getUTCMonth() === month - 1
|
|
177
|
+
&& date.getUTCDate() === day;
|
|
178
|
+
if (!valid) return null;
|
|
179
|
+
|
|
180
|
+
return { year, month, day };
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const parseTimeParts = (value) => {
|
|
184
|
+
if (typeof value !== "string") return null;
|
|
185
|
+
const match = TIME_REGEX.exec(value);
|
|
186
|
+
if (!match) return null;
|
|
187
|
+
|
|
188
|
+
const hour = Number(match[1]);
|
|
189
|
+
const minute = Number(match[2]);
|
|
190
|
+
const second = match[3] === undefined ? 0 : Number(match[3]);
|
|
191
|
+
if (!Number.isInteger(hour) || !Number.isInteger(minute) || !Number.isInteger(second)) {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
if (hour < 0 || hour > 23 || minute < 0 || minute > 59 || second < 0 || second > 59) {
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return { hour, minute, second };
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const normalizeTimeComparable = (value) => {
|
|
202
|
+
const parts = parseTimeParts(value);
|
|
203
|
+
if (!parts) return null;
|
|
204
|
+
return `${String(parts.hour).padStart(2, "0")}:${String(parts.minute).padStart(2, "0")}:${String(parts.second).padStart(2, "0")}`;
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const normalizeDateComparable = (value) => {
|
|
208
|
+
return parseDateParts(value) ? value : null;
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const normalizeDateTimeComparable = (value) => {
|
|
212
|
+
if (typeof value !== "string") return null;
|
|
213
|
+
const match = DATETIME_REGEX.exec(value);
|
|
214
|
+
if (!match) return null;
|
|
215
|
+
|
|
216
|
+
const date = normalizeDateComparable(match[1]);
|
|
217
|
+
const time = normalizeTimeComparable(match[2]);
|
|
218
|
+
if (!date || !time) return null;
|
|
219
|
+
|
|
220
|
+
return `${date}T${time}`;
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const getTemporalNormalization = (fieldType) => {
|
|
224
|
+
if (fieldType === DATE_FIELD_TYPE) {
|
|
225
|
+
return {
|
|
226
|
+
normalize: normalizeDateComparable,
|
|
227
|
+
invalidMessage: DEFAULT_MESSAGES.invalidDate,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
if (fieldType === TIME_FIELD_TYPE) {
|
|
231
|
+
return {
|
|
232
|
+
normalize: normalizeTimeComparable,
|
|
233
|
+
invalidMessage: DEFAULT_MESSAGES.invalidTime,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
if (fieldType === DATETIME_FIELD_TYPE) {
|
|
237
|
+
return {
|
|
238
|
+
normalize: normalizeDateTimeComparable,
|
|
239
|
+
invalidMessage: DEFAULT_MESSAGES.invalidDateTime,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
return null;
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
const validateTemporalField = (field, value) => {
|
|
246
|
+
const temporal = getTemporalNormalization(field.type);
|
|
247
|
+
if (!temporal) return null;
|
|
248
|
+
|
|
249
|
+
if (value === undefined || value === null || value === "") {
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const comparableValue = temporal.normalize(String(value));
|
|
254
|
+
if (!comparableValue) {
|
|
255
|
+
return temporal.invalidMessage;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (field.min !== undefined && field.min !== null && String(field.min) !== "") {
|
|
259
|
+
const minComparable = temporal.normalize(String(field.min));
|
|
260
|
+
if (minComparable && comparableValue < minComparable) {
|
|
261
|
+
return DEFAULT_MESSAGES.minTemporal(String(field.min));
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (field.max !== undefined && field.max !== null && String(field.max) !== "") {
|
|
266
|
+
const maxComparable = temporal.normalize(String(field.max));
|
|
267
|
+
if (maxComparable && comparableValue > maxComparable) {
|
|
268
|
+
return DEFAULT_MESSAGES.maxTemporal(String(field.max));
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return null;
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
export const validateField = (field, value) => {
|
|
276
|
+
// Check required
|
|
277
|
+
if (field.required) {
|
|
278
|
+
const isEmpty =
|
|
279
|
+
value === undefined ||
|
|
280
|
+
value === null ||
|
|
281
|
+
value === "" ||
|
|
282
|
+
(typeof value === "boolean" && value === false);
|
|
283
|
+
// For numbers, 0 is a valid value
|
|
284
|
+
const isEmptyNumber = field.type === "input-number" && value === null;
|
|
285
|
+
const shouldFail = field.type === "input-number" ? isEmptyNumber : isEmpty;
|
|
286
|
+
|
|
287
|
+
if (shouldFail) {
|
|
288
|
+
if (typeof field.required === "object" && field.required.message) {
|
|
289
|
+
return field.required.message;
|
|
290
|
+
}
|
|
291
|
+
return DEFAULT_MESSAGES.required;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const temporalError = validateTemporalField(field, value);
|
|
296
|
+
if (temporalError) {
|
|
297
|
+
return temporalError;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Check rules
|
|
301
|
+
if (Array.isArray(field.rules)) {
|
|
302
|
+
for (const rule of field.rules) {
|
|
303
|
+
const error = validateRule(rule, value);
|
|
304
|
+
if (error) return error;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return null;
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
const validateRule = (rule, value) => {
|
|
312
|
+
// Skip validation on empty values (required handles that)
|
|
313
|
+
if (value === undefined || value === null || value === "") return null;
|
|
95
314
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
315
|
+
const strValue = String(value);
|
|
316
|
+
|
|
317
|
+
switch (rule.rule) {
|
|
318
|
+
case "minLength": {
|
|
319
|
+
if (strValue.length < rule.value) {
|
|
320
|
+
return rule.message || DEFAULT_MESSAGES.minLength(rule.value);
|
|
321
|
+
}
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
case "maxLength": {
|
|
325
|
+
if (strValue.length > rule.value) {
|
|
326
|
+
return rule.message || DEFAULT_MESSAGES.maxLength(rule.value);
|
|
327
|
+
}
|
|
328
|
+
return null;
|
|
329
|
+
}
|
|
330
|
+
case "pattern": {
|
|
331
|
+
const preset = PATTERN_PRESETS[rule.value];
|
|
332
|
+
let regex = preset;
|
|
333
|
+
if (!regex) {
|
|
334
|
+
try {
|
|
335
|
+
regex = new RegExp(rule.value);
|
|
336
|
+
} catch {
|
|
337
|
+
return rule.message || DEFAULT_MESSAGES.pattern;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
if (!regex.test(strValue)) {
|
|
341
|
+
return rule.message || DEFAULT_MESSAGES.pattern;
|
|
342
|
+
}
|
|
343
|
+
return null;
|
|
344
|
+
}
|
|
345
|
+
default:
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
export const validateForm = (fields, formValues) => {
|
|
351
|
+
const errors = {};
|
|
352
|
+
const dataFields = collectAllDataFields(fields);
|
|
353
|
+
|
|
354
|
+
for (const field of dataFields) {
|
|
355
|
+
const value = get(formValues, field.name);
|
|
356
|
+
const error = validateField(field, value);
|
|
357
|
+
if (error) {
|
|
358
|
+
errors[field.name] = error;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return {
|
|
363
|
+
valid: Object.keys(errors).length === 0,
|
|
364
|
+
errors,
|
|
365
|
+
};
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
// --- Field helpers ---
|
|
369
|
+
|
|
370
|
+
const DISPLAY_TYPES = ["section", "read-only-text", "slot"];
|
|
371
|
+
|
|
372
|
+
export const isDataField = (field) => {
|
|
373
|
+
return !DISPLAY_TYPES.includes(field.type);
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
export const collectAllDataFields = (fields) => {
|
|
377
|
+
const result = [];
|
|
378
|
+
for (const field of fields) {
|
|
379
|
+
if (field.type === "section" && Array.isArray(field.fields)) {
|
|
380
|
+
result.push(...collectAllDataFields(field.fields));
|
|
381
|
+
} else if (isDataField(field)) {
|
|
382
|
+
result.push(field);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
return result;
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
export const getDefaultValue = (field) => {
|
|
389
|
+
switch (field.type) {
|
|
390
|
+
case "input-text":
|
|
391
|
+
case "input-date":
|
|
392
|
+
case "input-time":
|
|
393
|
+
case "input-datetime":
|
|
394
|
+
case "input-textarea":
|
|
395
|
+
case "popover-input":
|
|
396
|
+
return "";
|
|
397
|
+
case "input-number":
|
|
398
|
+
return null;
|
|
399
|
+
case "select":
|
|
400
|
+
return null;
|
|
401
|
+
case "checkbox":
|
|
402
|
+
return false;
|
|
403
|
+
case "color-picker":
|
|
404
|
+
return "#000000";
|
|
405
|
+
case "slider":
|
|
406
|
+
case "slider-with-input":
|
|
407
|
+
return field.min !== undefined ? field.min : 0;
|
|
408
|
+
case "image":
|
|
409
|
+
return null;
|
|
410
|
+
default:
|
|
411
|
+
return null;
|
|
412
|
+
}
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
export const flattenFields = (fields, startIdx = 0) => {
|
|
416
|
+
const result = [];
|
|
417
|
+
let idx = startIdx;
|
|
418
|
+
|
|
419
|
+
for (const field of fields) {
|
|
420
|
+
if (field.type === "section") {
|
|
421
|
+
result.push({
|
|
422
|
+
...field,
|
|
423
|
+
_isSection: true,
|
|
424
|
+
_idx: idx,
|
|
425
|
+
});
|
|
426
|
+
idx++;
|
|
427
|
+
if (Array.isArray(field.fields)) {
|
|
428
|
+
const nested = flattenFields(field.fields, idx);
|
|
429
|
+
result.push(...nested);
|
|
430
|
+
idx += nested.length;
|
|
431
|
+
}
|
|
432
|
+
} else {
|
|
433
|
+
result.push({
|
|
434
|
+
...field,
|
|
435
|
+
_isSection: false,
|
|
436
|
+
_idx: idx,
|
|
437
|
+
});
|
|
438
|
+
idx++;
|
|
439
|
+
}
|
|
99
440
|
}
|
|
100
441
|
|
|
101
|
-
return
|
|
442
|
+
return result;
|
|
102
443
|
};
|
|
103
444
|
|
|
445
|
+
// --- Store ---
|
|
446
|
+
|
|
447
|
+
export const createInitialState = () =>
|
|
448
|
+
Object.freeze({
|
|
449
|
+
formValues: {},
|
|
450
|
+
errors: {},
|
|
451
|
+
reactiveMode: false,
|
|
452
|
+
tooltipState: {
|
|
453
|
+
open: false,
|
|
454
|
+
x: 0,
|
|
455
|
+
y: 0,
|
|
456
|
+
content: "",
|
|
457
|
+
},
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
export const selectForm = ({ state, props }) => {
|
|
461
|
+
const { form = {} } = props || {};
|
|
462
|
+
const normalizedForm = normalizeWhenDirectives(form);
|
|
463
|
+
const context = isPlainObject(props?.context) ? props.context : {};
|
|
464
|
+
const stateFormValues = isPlainObject(state?.formValues)
|
|
465
|
+
? state.formValues
|
|
466
|
+
: {};
|
|
467
|
+
const mergedContext = {
|
|
468
|
+
...context,
|
|
469
|
+
...stateFormValues,
|
|
470
|
+
formValues: stateFormValues,
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
if (Object.keys(mergedContext).length > 0) {
|
|
474
|
+
return parseAndRender(normalizedForm, mergedContext);
|
|
475
|
+
}
|
|
476
|
+
return normalizedForm;
|
|
477
|
+
};
|
|
104
478
|
|
|
105
479
|
export const selectViewData = ({ state, props }) => {
|
|
106
480
|
const containerAttrString = stringifyAttrs(props);
|
|
107
|
-
|
|
108
481
|
const form = selectForm({ state, props });
|
|
109
|
-
const fields =
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
482
|
+
const fields = form.fields || [];
|
|
483
|
+
const formDisabled = !!props?.disabled;
|
|
484
|
+
|
|
485
|
+
// Flatten fields for template iteration
|
|
486
|
+
const flatFields = flattenFields(fields);
|
|
487
|
+
|
|
488
|
+
// Enrich each field with computed properties
|
|
489
|
+
flatFields.forEach((field, arrIdx) => {
|
|
490
|
+
field._arrIdx = arrIdx;
|
|
491
|
+
|
|
492
|
+
if (field._isSection) return;
|
|
493
|
+
|
|
494
|
+
const isData = isDataField(field);
|
|
495
|
+
field._disabled = formDisabled || !!field.disabled;
|
|
496
|
+
|
|
497
|
+
if (isData && field.name) {
|
|
498
|
+
field._error = state.errors[field.name] || null;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Type-specific computed props
|
|
502
|
+
if (field.type === "input-text") {
|
|
503
|
+
field._inputType = field.inputType || "text";
|
|
121
504
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
field.
|
|
126
|
-
|
|
127
|
-
|
|
505
|
+
|
|
506
|
+
if (field.type === "select") {
|
|
507
|
+
const val = get(state.formValues, field.name);
|
|
508
|
+
field._selectedValue = val !== undefined ? val : null;
|
|
509
|
+
field.placeholder = field.placeholder || "";
|
|
510
|
+
// clearable defaults to true; noClear is the inverse
|
|
511
|
+
field.noClear = field.clearable === false;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
if (field.type === "image") {
|
|
515
|
+
const src = get(state.formValues, field.name);
|
|
516
|
+
field._imageSrc = src && String(src).trim() ? src : null;
|
|
517
|
+
field.placeholderText = field.placeholderText || "No Image";
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
if (field.type === "read-only-text") {
|
|
521
|
+
field.content = field.content || "";
|
|
128
522
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
field.
|
|
523
|
+
|
|
524
|
+
if (field.type === "checkbox") {
|
|
525
|
+
const inlineText = typeof field.content === "string"
|
|
526
|
+
? field.content
|
|
527
|
+
: (typeof field.checkboxLabel === "string" ? field.checkboxLabel : "");
|
|
528
|
+
field._checkboxText = inlineText;
|
|
135
529
|
}
|
|
136
530
|
});
|
|
137
531
|
|
|
532
|
+
// Actions
|
|
533
|
+
const actions = form.actions || { buttons: [] };
|
|
534
|
+
const layout = actions.layout || "split";
|
|
535
|
+
const buttons = (actions.buttons || []).map((btn, i) => ({
|
|
536
|
+
...btn,
|
|
537
|
+
_globalIdx: i,
|
|
538
|
+
variant: btn.variant || "se",
|
|
539
|
+
_disabled: formDisabled || !!btn.disabled,
|
|
540
|
+
pre: btn.pre || "",
|
|
541
|
+
suf: btn.suf || "",
|
|
542
|
+
}));
|
|
543
|
+
|
|
544
|
+
let actionsData;
|
|
545
|
+
if (layout === "split") {
|
|
546
|
+
actionsData = {
|
|
547
|
+
_layout: "split",
|
|
548
|
+
buttons,
|
|
549
|
+
_leftButtons: buttons.filter((b) => b.align === "left"),
|
|
550
|
+
_rightButtons: buttons.filter((b) => b.align !== "left"),
|
|
551
|
+
};
|
|
552
|
+
} else {
|
|
553
|
+
actionsData = {
|
|
554
|
+
_layout: layout,
|
|
555
|
+
buttons,
|
|
556
|
+
_allButtons: buttons,
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
|
|
138
560
|
return {
|
|
139
|
-
key: props?.key,
|
|
140
561
|
containerAttrString,
|
|
141
562
|
title: form?.title || "",
|
|
142
563
|
description: form?.description || "",
|
|
143
|
-
|
|
144
|
-
actions:
|
|
145
|
-
buttons: [],
|
|
146
|
-
},
|
|
564
|
+
flatFields,
|
|
565
|
+
actions: actionsData,
|
|
147
566
|
formValues: state.formValues,
|
|
148
567
|
tooltipState: state.tooltipState,
|
|
149
568
|
};
|
|
150
569
|
};
|
|
151
570
|
|
|
152
|
-
export const selectState = ({ state }) => {
|
|
153
|
-
return state;
|
|
154
|
-
};
|
|
155
|
-
|
|
156
571
|
export const selectFormValues = ({ state, props }) => {
|
|
157
572
|
const form = selectForm({ state, props });
|
|
158
|
-
|
|
159
|
-
return
|
|
573
|
+
const dataFields = collectAllDataFields(form.fields || []);
|
|
574
|
+
return pickByPaths(
|
|
160
575
|
state.formValues,
|
|
161
|
-
|
|
576
|
+
dataFields.map((f) => f.name).filter((name) => typeof name === "string" && name.length > 0),
|
|
162
577
|
);
|
|
163
578
|
};
|
|
164
579
|
|
|
165
|
-
export const getFormFieldValue = ({ state }, name) => {
|
|
166
|
-
return get(state.formValues, name);
|
|
167
|
-
};
|
|
168
|
-
|
|
169
|
-
export const setFormValues = ({ state }, payload = {}) => {
|
|
170
|
-
state.formValues = payload.formValues || {};
|
|
171
|
-
};
|
|
172
|
-
|
|
173
580
|
export const setFormFieldValue = ({ state, props }, payload = {}) => {
|
|
174
581
|
const { name, value } = payload;
|
|
175
|
-
if (!name)
|
|
176
|
-
return;
|
|
177
|
-
}
|
|
582
|
+
if (!name) return;
|
|
178
583
|
set(state.formValues, name, value);
|
|
179
|
-
|
|
584
|
+
pruneHiddenValues({ state, props });
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
export const pruneHiddenValues = ({ state, props }) => {
|
|
588
|
+
if (!props) return;
|
|
589
|
+
// Prune to only visible field names
|
|
180
590
|
const form = selectForm({ state, props });
|
|
181
|
-
const
|
|
591
|
+
const dataFields = collectAllDataFields(form.fields || []);
|
|
592
|
+
state.formValues = pickByPaths(
|
|
182
593
|
state.formValues,
|
|
183
|
-
|
|
594
|
+
dataFields.map((f) => f.name).filter((name) => typeof name === "string" && name.length > 0),
|
|
184
595
|
);
|
|
185
|
-
|
|
596
|
+
};
|
|
597
|
+
|
|
598
|
+
export const setFormValues = ({ state }, payload = {}) => {
|
|
599
|
+
const { values } = payload;
|
|
600
|
+
if (!values || typeof values !== "object") return;
|
|
601
|
+
Object.keys(values).forEach((key) => {
|
|
602
|
+
set(state.formValues, key, values[key]);
|
|
603
|
+
});
|
|
604
|
+
};
|
|
605
|
+
|
|
606
|
+
export const resetFormValues = ({ state }, payload = {}) => {
|
|
607
|
+
const { defaultValues = {} } = payload;
|
|
608
|
+
state.formValues = defaultValues ? structuredClone(defaultValues) : {};
|
|
609
|
+
state.errors = {};
|
|
610
|
+
state.reactiveMode = false;
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
export const setErrors = ({ state }, payload = {}) => {
|
|
614
|
+
state.errors = payload.errors || {};
|
|
615
|
+
};
|
|
616
|
+
|
|
617
|
+
export const clearFieldError = ({ state }, payload = {}) => {
|
|
618
|
+
const { name } = payload;
|
|
619
|
+
if (name && state.errors[name]) {
|
|
620
|
+
delete state.errors[name];
|
|
621
|
+
}
|
|
622
|
+
};
|
|
623
|
+
|
|
624
|
+
export const setReactiveMode = ({ state }) => {
|
|
625
|
+
state.reactiveMode = true;
|
|
186
626
|
};
|
|
187
627
|
|
|
188
628
|
export const showTooltip = ({ state }, payload = {}) => {
|
|
@@ -198,6 +638,6 @@ export const showTooltip = ({ state }, payload = {}) => {
|
|
|
198
638
|
export const hideTooltip = ({ state }) => {
|
|
199
639
|
state.tooltipState = {
|
|
200
640
|
...state.tooltipState,
|
|
201
|
-
open: false
|
|
641
|
+
open: false,
|
|
202
642
|
};
|
|
203
643
|
};
|