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