@jsswift/ui 1.0.44 → 1.0.46
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/css/base.css +2 -137
- package/dist/css/fonts-material.css +7 -0
- package/dist/css/fonts-nunito.css +134 -0
- package/dist/css/min-ui.css +1 -1
- package/dist/css/ui.css +2 -137
- package/dist/css/ui.min.css +1 -1
- package/dist/min-ui.js +4 -4
- package/dist/tabler.js +7 -0
- package/dist/ui.js +10561 -14120
- package/dist/ui.min.js +4 -4
- package/package.json +6 -3
- package/src/01-foundation-helpers.js +0 -402
- package/src/10-primitives-layout.js +14 -1013
- package/src/20-display-content.js +484 -718
- package/src/21-list-content.js +246 -341
- package/src/22-banner-content.js +3 -116
- package/src/23-tooltip.js +276 -326
- package/src/31-form-advanced.js +2638 -3003
- package/src/40-navigation.js +1262 -1454
- package/src/41-tab-panel.js +896 -1090
- package/src/50-feedback.js +999 -1209
- package/src/60-shell-app.js +5 -265
- package/src/61-form-service.js +304 -386
- package/src/70-dialog.js +209 -277
- package/src/71-menu-overlays.js +1175 -1400
- package/src/80-data.js +433 -503
- package/src/css/base.css +2 -137
- package/src/css/fonts-material.css +7 -0
- package/src/css/fonts-nunito.css +134 -0
- package/src/modules.json +59 -23
- package/src/tabler.js +7 -0
package/src/61-form-service.js
CHANGED
|
@@ -1,322 +1,307 @@
|
|
|
1
|
-
// useForm + UI.Form
|
|
2
|
-
JSswift.form = JSswift.form || {};
|
|
3
|
-
|
|
4
|
-
JSswift.form._isPromise = (v) => v && typeof v.then === "function";
|
|
5
|
-
|
|
6
|
-
JSswift.form._normalizeRules = (rules) => {
|
|
7
|
-
// rules: { fieldName: [fn|{rule, message}] } or { fieldName: fn } etc.
|
|
8
|
-
const out = {};
|
|
9
|
-
for (const k in (rules || {})) {
|
|
10
|
-
const r = rules[k];
|
|
11
|
-
out[k] = Array.isArray(r) ? r : [r];
|
|
12
|
-
}
|
|
13
|
-
return out;
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
// rule returns: true (ok) | false (generic error) | string (error msg)
|
|
17
|
-
JSswift.form._runRule = async (rule, value, ctx) => {
|
|
18
|
-
let fn = rule;
|
|
19
|
-
let msg = null;
|
|
20
|
-
|
|
21
|
-
if (rule && typeof rule === "object" && typeof rule.rule === "function") {
|
|
22
|
-
fn = rule.rule;
|
|
23
|
-
msg = rule.message || null;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const res = fn ? fn(value, ctx) : true;
|
|
27
|
-
const v = JSswift.form._isPromise(res) ? await res : res;
|
|
28
|
-
|
|
29
|
-
if (v === true) return null;
|
|
30
|
-
if (typeof v === "string") return v;
|
|
31
|
-
if (v === false) return msg || "Valore non valido";
|
|
32
|
-
return v ? null : (msg || "Valore non valido");
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
JSswift.useForm = (options = {}) => {
|
|
36
|
-
const model = options.model || {};
|
|
37
|
-
const rules = JSswift.form._normalizeRules(options.rules || {});
|
|
38
|
-
const validateOn = options.validateOn || "submit"; // "input" | "blur" | "submit"
|
|
39
|
-
const initial = options.initial || null;
|
|
40
|
-
|
|
41
|
-
// errors: { field: string|null }
|
|
42
|
-
const errors = _.rod({});
|
|
43
|
-
const touched = _.rod({});
|
|
44
|
-
const submitting = _.rod(false);
|
|
45
|
-
const submitError = _.rod(null);
|
|
46
|
-
|
|
47
|
-
const getModelObj = () => {
|
|
48
|
-
// allow rod model (model.value)
|
|
49
|
-
if (model && typeof model === "object" && "value" in model) return model.value || {};
|
|
50
|
-
return model;
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
const setModelObj = (next) => {
|
|
54
|
-
if (model && typeof model === "object" && "value" in model) model.value = next;
|
|
55
|
-
else {
|
|
56
|
-
// mutate existing
|
|
57
|
-
const obj = model || {};
|
|
58
|
-
for (const k in obj) delete obj[k];
|
|
59
|
-
Object.assign(obj, next);
|
|
60
|
-
}
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
const getFieldValue = (name) => {
|
|
64
|
-
const obj = getModelObj();
|
|
65
|
-
return obj ? obj[name] : undefined;
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
const setFieldValue = (name, value) => {
|
|
69
|
-
const obj = { ...(getModelObj() || {}) };
|
|
70
|
-
obj[name] = value;
|
|
71
|
-
setModelObj(obj);
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
const setError = (name, msg) => {
|
|
75
|
-
const e = { ...(errors.value || {}) };
|
|
76
|
-
if (msg) e[name] = msg;
|
|
77
|
-
else delete e[name];
|
|
78
|
-
errors.value = e;
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
const getError = (name) => (errors.value || {})[name] || null;
|
|
82
|
-
|
|
83
|
-
const touch = (name) => {
|
|
84
|
-
touched.value = { ...(touched.value || {}), [name]: true };
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
const isTouched = (name) => !!(touched.value || {})[name];
|
|
88
|
-
|
|
89
|
-
const validateField = async (name) => {
|
|
90
|
-
const list = rules[name] || [];
|
|
91
|
-
if (!list.length) {
|
|
92
|
-
setError(name, null);
|
|
93
|
-
return true;
|
|
94
|
-
}
|
|
95
|
-
const value = getFieldValue(name);
|
|
96
|
-
const ctx = { name, value, model: getModelObj(), form: api };
|
|
97
|
-
|
|
98
|
-
for (const rule of list) {
|
|
99
|
-
if (!rule) continue;
|
|
100
|
-
const msg = await JSswift.form._runRule(rule, value, ctx);
|
|
101
|
-
if (msg) {
|
|
102
|
-
setError(name, msg);
|
|
103
|
-
return false;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
setError(name, null);
|
|
107
|
-
return true;
|
|
108
|
-
};
|
|
109
|
-
|
|
110
|
-
const validate = async () => {
|
|
111
|
-
submitError.value = null;
|
|
112
|
-
const names = Object.keys(rules);
|
|
113
|
-
let ok = true;
|
|
114
|
-
for (const n of names) {
|
|
115
|
-
const r = await validateField(n);
|
|
116
|
-
if (!r) ok = false;
|
|
117
|
-
}
|
|
118
|
-
return ok;
|
|
119
|
-
};
|
|
120
|
-
|
|
121
|
-
const reset = () => {
|
|
122
|
-
submitError.value = null;
|
|
123
|
-
errors.value = {};
|
|
124
|
-
touched.value = {};
|
|
125
|
-
if (initial) setModelObj({ ...(initial || {}) });
|
|
126
|
-
};
|
|
127
|
-
|
|
128
|
-
const submit = async (fn) => {
|
|
129
|
-
submitting.value = true;
|
|
130
|
-
submitError.value = null;
|
|
131
|
-
try {
|
|
132
|
-
const ok = await validate();
|
|
133
|
-
if (!ok) return { ok: false, reason: "validation" };
|
|
134
|
-
const res = await fn?.(getModelObj(), api);
|
|
135
|
-
return { ok: true, result: res };
|
|
136
|
-
} catch (e) {
|
|
137
|
-
console.error("[useForm] submit error:", e);
|
|
138
|
-
submitError.value = (e && e.message) ? e.message : String(e);
|
|
139
|
-
return { ok: false, reason: "exception", error: e };
|
|
140
|
-
} finally {
|
|
141
|
-
submitting.value = false;
|
|
142
|
-
}
|
|
143
|
-
};
|
|
144
|
-
|
|
145
|
-
// Field binding factory for UI components
|
|
146
|
-
const field = (name, fieldOpts = {}) => {
|
|
147
|
-
const r = _.rod("");
|
|
148
|
-
// keep rod synced with form model
|
|
149
|
-
// when r changes -> update model
|
|
150
|
-
r.action?.((v) => {
|
|
151
|
-
setFieldValue(name, v)
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
const shouldShowError = () => {
|
|
155
|
-
if (validateOn === "submit" || validateOn === "input" || validateOn === "blur") return true;
|
|
156
|
-
return isTouched(name);
|
|
157
|
-
};
|
|
158
|
-
|
|
159
|
-
let activeEl = null;
|
|
160
|
-
let blurHandled = false;
|
|
161
|
-
let watchingOutside = false;
|
|
162
|
-
|
|
163
|
-
const runBlur = async () => {
|
|
164
|
-
if (blurHandled) return;
|
|
165
|
-
blurHandled = true;
|
|
166
|
-
touch(name);
|
|
167
|
-
if (validateOn === "blur" || validateOn === "input") await validateField(name);
|
|
168
|
-
setTimeout(() => { blurHandled = false; }, 0);
|
|
169
|
-
};
|
|
170
|
-
|
|
171
|
-
const onDocPointerDown = (e) => {
|
|
172
|
-
if (!activeEl) return;
|
|
173
|
-
if (activeEl === e.target || (activeEl.contains && activeEl.contains(e.target))) return;
|
|
174
|
-
runBlur();
|
|
175
|
-
activeEl = null;
|
|
176
|
-
if (watchingOutside) {
|
|
177
|
-
watchingOutside = false;
|
|
178
|
-
document.removeEventListener("pointerdown", onDocPointerDown, true);
|
|
179
|
-
}
|
|
180
|
-
};
|
|
181
|
-
|
|
182
|
-
const ensureOutsideWatch = () => {
|
|
183
|
-
if (watchingOutside) return;
|
|
184
|
-
watchingOutside = true;
|
|
185
|
-
document.addEventListener("pointerdown", onDocPointerDown, true);
|
|
186
|
-
};
|
|
187
|
-
|
|
188
|
-
const onInput = async () => {
|
|
189
|
-
activeEl = document.activeElement;
|
|
190
|
-
blurHandled = false;
|
|
191
|
-
ensureOutsideWatch();
|
|
192
|
-
if (validateOn === "input") await validateField(name);
|
|
193
|
-
};
|
|
194
|
-
|
|
195
|
-
const onBlur = async () => {
|
|
196
|
-
await runBlur();
|
|
197
|
-
activeEl = null;
|
|
198
|
-
if (watchingOutside) {
|
|
199
|
-
watchingOutside = false;
|
|
200
|
-
document.removeEventListener("pointerdown", onDocPointerDown, true);
|
|
201
|
-
}
|
|
202
|
-
};
|
|
203
|
-
return {
|
|
204
|
-
name,
|
|
205
|
-
model: r,
|
|
206
|
-
get value() { return r.value; },
|
|
207
|
-
set value(v) { r.value = v; },
|
|
208
|
-
error: () => (shouldShowError() ? getError(name) : null),
|
|
209
|
-
success: fieldOpts.success || null,
|
|
210
|
-
warning: fieldOpts.warning || null,
|
|
211
|
-
note: fieldOpts.note || null,
|
|
212
|
-
touch: () => touch(name),
|
|
213
|
-
validate: () => validateField(name),
|
|
214
|
-
onInput,
|
|
215
|
-
onBlur,
|
|
216
|
-
// allow passing through custom hints
|
|
217
|
-
hint: fieldOpts.hint || null
|
|
218
|
-
};
|
|
219
|
-
};
|
|
220
|
-
|
|
221
|
-
const api = {
|
|
222
|
-
model,
|
|
223
|
-
errors,
|
|
224
|
-
touched,
|
|
225
|
-
submitting,
|
|
226
|
-
submitError,
|
|
227
|
-
|
|
228
|
-
getValue: getFieldValue,
|
|
229
|
-
setValue: setFieldValue,
|
|
230
|
-
getError,
|
|
231
|
-
setError,
|
|
232
|
-
|
|
233
|
-
touch,
|
|
234
|
-
isTouched,
|
|
235
|
-
|
|
236
|
-
field,
|
|
237
|
-
validateField,
|
|
238
|
-
validate,
|
|
239
|
-
reset,
|
|
240
|
-
submit
|
|
241
|
-
};
|
|
242
|
-
|
|
243
|
-
return api;
|
|
244
|
-
};
|
|
245
|
-
|
|
246
|
-
UI.Form = (...args) => {
|
|
247
|
-
const { props, children } = JSswift.uiNormalizeArgs(args);
|
|
248
|
-
const form = props.form; // required-ish
|
|
249
|
-
const onSubmit = props.onSubmit; // async (model, form) => ...
|
|
250
|
-
const cls = uiClass(["cms-form", props.class]);
|
|
251
|
-
|
|
252
|
-
const p = JSswift.omit(props, ["form", "onSubmit"]);
|
|
253
|
-
p.class = cls;
|
|
254
|
-
|
|
255
|
-
const el = _.form({
|
|
256
|
-
...p,
|
|
257
|
-
onSubmit: async (e) => {
|
|
258
|
-
e.preventDefault();
|
|
259
|
-
if (!form) return;
|
|
260
|
-
await form.submit(onSubmit || (async () => { }));
|
|
261
|
-
}
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
// children can be nodes or function(form)->nodes
|
|
265
|
-
const content = [];
|
|
266
|
-
for (const ch of (children || [])) {
|
|
267
|
-
const v = (typeof ch === "function") ? ch(form) : ch;
|
|
268
|
-
const out = JSswift.ui.slot(v);
|
|
269
|
-
if (!out) continue;
|
|
270
|
-
if (Array.isArray(out)) content.push(...out);
|
|
271
|
-
else content.push(out);
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
content.forEach(n => el.appendChild(n));
|
|
275
|
-
|
|
276
|
-
// disable fields/buttons while submitting (optional UX)
|
|
277
|
-
if (form && form.submitting) {
|
|
278
|
-
JSswift.reactive.effect(() => {
|
|
279
|
-
el.classList.toggle("is-submitting", !!form.submitting.value);
|
|
280
|
-
}, "UI.Form:submitting");
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
return el;
|
|
284
|
-
};
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
app.services = app.services || {};
|
|
306
|
-
app.services.dialog = app.services.dialog || {};
|
|
307
|
-
app.dialog = app.services.dialog;
|
|
308
|
-
|
|
309
|
-
let modalRoot = null;
|
|
310
|
-
|
|
311
|
-
function ensureModalRoot() {
|
|
312
|
-
if (modalRoot) return modalRoot;
|
|
313
|
-
|
|
314
|
-
modalRoot = document.createElement("div");
|
|
315
|
-
modalRoot.setAttribute("data-cms-modal-root", "true");
|
|
316
|
-
document.body.appendChild(modalRoot);
|
|
317
|
-
return modalRoot;
|
|
318
|
-
}
|
|
319
|
-
|
|
1
|
+
// useForm + UI.Form
|
|
2
|
+
JSswift.form = JSswift.form || {};
|
|
3
|
+
|
|
4
|
+
JSswift.form._isPromise = (v) => v && typeof v.then === "function";
|
|
5
|
+
|
|
6
|
+
JSswift.form._normalizeRules = (rules) => {
|
|
7
|
+
// rules: { fieldName: [fn|{rule, message}] } or { fieldName: fn } etc.
|
|
8
|
+
const out = {};
|
|
9
|
+
for (const k in (rules || {})) {
|
|
10
|
+
const r = rules[k];
|
|
11
|
+
out[k] = Array.isArray(r) ? r : [r];
|
|
12
|
+
}
|
|
13
|
+
return out;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// rule returns: true (ok) | false (generic error) | string (error msg)
|
|
17
|
+
JSswift.form._runRule = async (rule, value, ctx) => {
|
|
18
|
+
let fn = rule;
|
|
19
|
+
let msg = null;
|
|
20
|
+
|
|
21
|
+
if (rule && typeof rule === "object" && typeof rule.rule === "function") {
|
|
22
|
+
fn = rule.rule;
|
|
23
|
+
msg = rule.message || null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const res = fn ? fn(value, ctx) : true;
|
|
27
|
+
const v = JSswift.form._isPromise(res) ? await res : res;
|
|
28
|
+
|
|
29
|
+
if (v === true) return null;
|
|
30
|
+
if (typeof v === "string") return v;
|
|
31
|
+
if (v === false) return msg || "Valore non valido";
|
|
32
|
+
return v ? null : (msg || "Valore non valido");
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
JSswift.useForm = (options = {}) => {
|
|
36
|
+
const model = options.model || {};
|
|
37
|
+
const rules = JSswift.form._normalizeRules(options.rules || {});
|
|
38
|
+
const validateOn = options.validateOn || "submit"; // "input" | "blur" | "submit"
|
|
39
|
+
const initial = options.initial || null;
|
|
40
|
+
|
|
41
|
+
// errors: { field: string|null }
|
|
42
|
+
const errors = _.rod({});
|
|
43
|
+
const touched = _.rod({});
|
|
44
|
+
const submitting = _.rod(false);
|
|
45
|
+
const submitError = _.rod(null);
|
|
46
|
+
|
|
47
|
+
const getModelObj = () => {
|
|
48
|
+
// allow rod model (model.value)
|
|
49
|
+
if (model && typeof model === "object" && "value" in model) return model.value || {};
|
|
50
|
+
return model;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const setModelObj = (next) => {
|
|
54
|
+
if (model && typeof model === "object" && "value" in model) model.value = next;
|
|
55
|
+
else {
|
|
56
|
+
// mutate existing
|
|
57
|
+
const obj = model || {};
|
|
58
|
+
for (const k in obj) delete obj[k];
|
|
59
|
+
Object.assign(obj, next);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const getFieldValue = (name) => {
|
|
64
|
+
const obj = getModelObj();
|
|
65
|
+
return obj ? obj[name] : undefined;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const setFieldValue = (name, value) => {
|
|
69
|
+
const obj = { ...(getModelObj() || {}) };
|
|
70
|
+
obj[name] = value;
|
|
71
|
+
setModelObj(obj);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const setError = (name, msg) => {
|
|
75
|
+
const e = { ...(errors.value || {}) };
|
|
76
|
+
if (msg) e[name] = msg;
|
|
77
|
+
else delete e[name];
|
|
78
|
+
errors.value = e;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const getError = (name) => (errors.value || {})[name] || null;
|
|
82
|
+
|
|
83
|
+
const touch = (name) => {
|
|
84
|
+
touched.value = { ...(touched.value || {}), [name]: true };
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const isTouched = (name) => !!(touched.value || {})[name];
|
|
88
|
+
|
|
89
|
+
const validateField = async (name) => {
|
|
90
|
+
const list = rules[name] || [];
|
|
91
|
+
if (!list.length) {
|
|
92
|
+
setError(name, null);
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
const value = getFieldValue(name);
|
|
96
|
+
const ctx = { name, value, model: getModelObj(), form: api };
|
|
97
|
+
|
|
98
|
+
for (const rule of list) {
|
|
99
|
+
if (!rule) continue;
|
|
100
|
+
const msg = await JSswift.form._runRule(rule, value, ctx);
|
|
101
|
+
if (msg) {
|
|
102
|
+
setError(name, msg);
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
setError(name, null);
|
|
107
|
+
return true;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const validate = async () => {
|
|
111
|
+
submitError.value = null;
|
|
112
|
+
const names = Object.keys(rules);
|
|
113
|
+
let ok = true;
|
|
114
|
+
for (const n of names) {
|
|
115
|
+
const r = await validateField(n);
|
|
116
|
+
if (!r) ok = false;
|
|
117
|
+
}
|
|
118
|
+
return ok;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const reset = () => {
|
|
122
|
+
submitError.value = null;
|
|
123
|
+
errors.value = {};
|
|
124
|
+
touched.value = {};
|
|
125
|
+
if (initial) setModelObj({ ...(initial || {}) });
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const submit = async (fn) => {
|
|
129
|
+
submitting.value = true;
|
|
130
|
+
submitError.value = null;
|
|
131
|
+
try {
|
|
132
|
+
const ok = await validate();
|
|
133
|
+
if (!ok) return { ok: false, reason: "validation" };
|
|
134
|
+
const res = await fn?.(getModelObj(), api);
|
|
135
|
+
return { ok: true, result: res };
|
|
136
|
+
} catch (e) {
|
|
137
|
+
console.error("[useForm] submit error:", e);
|
|
138
|
+
submitError.value = (e && e.message) ? e.message : String(e);
|
|
139
|
+
return { ok: false, reason: "exception", error: e };
|
|
140
|
+
} finally {
|
|
141
|
+
submitting.value = false;
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
// Field binding factory for UI components
|
|
146
|
+
const field = (name, fieldOpts = {}) => {
|
|
147
|
+
const r = _.rod("");
|
|
148
|
+
// keep rod synced with form model
|
|
149
|
+
// when r changes -> update model
|
|
150
|
+
r.action?.((v) => {
|
|
151
|
+
setFieldValue(name, v)
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const shouldShowError = () => {
|
|
155
|
+
if (validateOn === "submit" || validateOn === "input" || validateOn === "blur") return true;
|
|
156
|
+
return isTouched(name);
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
let activeEl = null;
|
|
160
|
+
let blurHandled = false;
|
|
161
|
+
let watchingOutside = false;
|
|
162
|
+
|
|
163
|
+
const runBlur = async () => {
|
|
164
|
+
if (blurHandled) return;
|
|
165
|
+
blurHandled = true;
|
|
166
|
+
touch(name);
|
|
167
|
+
if (validateOn === "blur" || validateOn === "input") await validateField(name);
|
|
168
|
+
setTimeout(() => { blurHandled = false; }, 0);
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const onDocPointerDown = (e) => {
|
|
172
|
+
if (!activeEl) return;
|
|
173
|
+
if (activeEl === e.target || (activeEl.contains && activeEl.contains(e.target))) return;
|
|
174
|
+
runBlur();
|
|
175
|
+
activeEl = null;
|
|
176
|
+
if (watchingOutside) {
|
|
177
|
+
watchingOutside = false;
|
|
178
|
+
document.removeEventListener("pointerdown", onDocPointerDown, true);
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const ensureOutsideWatch = () => {
|
|
183
|
+
if (watchingOutside) return;
|
|
184
|
+
watchingOutside = true;
|
|
185
|
+
document.addEventListener("pointerdown", onDocPointerDown, true);
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const onInput = async () => {
|
|
189
|
+
activeEl = document.activeElement;
|
|
190
|
+
blurHandled = false;
|
|
191
|
+
ensureOutsideWatch();
|
|
192
|
+
if (validateOn === "input") await validateField(name);
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const onBlur = async () => {
|
|
196
|
+
await runBlur();
|
|
197
|
+
activeEl = null;
|
|
198
|
+
if (watchingOutside) {
|
|
199
|
+
watchingOutside = false;
|
|
200
|
+
document.removeEventListener("pointerdown", onDocPointerDown, true);
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
return {
|
|
204
|
+
name,
|
|
205
|
+
model: r,
|
|
206
|
+
get value() { return r.value; },
|
|
207
|
+
set value(v) { r.value = v; },
|
|
208
|
+
error: () => (shouldShowError() ? getError(name) : null),
|
|
209
|
+
success: fieldOpts.success || null,
|
|
210
|
+
warning: fieldOpts.warning || null,
|
|
211
|
+
note: fieldOpts.note || null,
|
|
212
|
+
touch: () => touch(name),
|
|
213
|
+
validate: () => validateField(name),
|
|
214
|
+
onInput,
|
|
215
|
+
onBlur,
|
|
216
|
+
// allow passing through custom hints
|
|
217
|
+
hint: fieldOpts.hint || null
|
|
218
|
+
};
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const api = {
|
|
222
|
+
model,
|
|
223
|
+
errors,
|
|
224
|
+
touched,
|
|
225
|
+
submitting,
|
|
226
|
+
submitError,
|
|
227
|
+
|
|
228
|
+
getValue: getFieldValue,
|
|
229
|
+
setValue: setFieldValue,
|
|
230
|
+
getError,
|
|
231
|
+
setError,
|
|
232
|
+
|
|
233
|
+
touch,
|
|
234
|
+
isTouched,
|
|
235
|
+
|
|
236
|
+
field,
|
|
237
|
+
validateField,
|
|
238
|
+
validate,
|
|
239
|
+
reset,
|
|
240
|
+
submit
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
return api;
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
UI.Form = (...args) => {
|
|
247
|
+
const { props, children } = JSswift.uiNormalizeArgs(args);
|
|
248
|
+
const form = props.form; // required-ish
|
|
249
|
+
const onSubmit = props.onSubmit; // async (model, form) => ...
|
|
250
|
+
const cls = uiClass(["cms-form", props.class]);
|
|
251
|
+
|
|
252
|
+
const p = JSswift.omit(props, ["form", "onSubmit"]);
|
|
253
|
+
p.class = cls;
|
|
254
|
+
|
|
255
|
+
const el = _.form({
|
|
256
|
+
...p,
|
|
257
|
+
onSubmit: async (e) => {
|
|
258
|
+
e.preventDefault();
|
|
259
|
+
if (!form) return;
|
|
260
|
+
await form.submit(onSubmit || (async () => { }));
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// children can be nodes or function(form)->nodes
|
|
265
|
+
const content = [];
|
|
266
|
+
for (const ch of (children || [])) {
|
|
267
|
+
const v = (typeof ch === "function") ? ch(form) : ch;
|
|
268
|
+
const out = JSswift.ui.slot(v);
|
|
269
|
+
if (!out) continue;
|
|
270
|
+
if (Array.isArray(out)) content.push(...out);
|
|
271
|
+
else content.push(out);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
content.forEach(n => el.appendChild(n));
|
|
275
|
+
|
|
276
|
+
// disable fields/buttons while submitting (optional UX)
|
|
277
|
+
if (form && form.submitting) {
|
|
278
|
+
JSswift.reactive.effect(() => {
|
|
279
|
+
el.classList.toggle("is-submitting", !!form.submitting.value);
|
|
280
|
+
}, "UI.Form:submitting");
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return el;
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
// ===============================
|
|
288
|
+
// Dialog Service (Modal)
|
|
289
|
+
// ===============================
|
|
290
|
+
app.services = app.services || {};
|
|
291
|
+
app.services.dialog = app.services.dialog || {};
|
|
292
|
+
app.dialog = app.services.dialog;
|
|
293
|
+
|
|
294
|
+
let modalRoot = null;
|
|
295
|
+
|
|
296
|
+
function ensureModalRoot() {
|
|
297
|
+
if (modalRoot) return modalRoot;
|
|
298
|
+
|
|
299
|
+
modalRoot = document.createElement("div");
|
|
300
|
+
modalRoot.setAttribute("data-cms-modal-root", "true");
|
|
301
|
+
document.body.appendChild(modalRoot);
|
|
302
|
+
return modalRoot;
|
|
303
|
+
}
|
|
304
|
+
|
|
320
305
|
UI.cardHeader = (...args) => {
|
|
321
306
|
const { props, children } = JSswift.uiNormalizeArgs(args);
|
|
322
307
|
const slots = props.slots || {};
|
|
@@ -381,70 +366,3 @@
|
|
|
381
366
|
setPropertyProps(el, props);
|
|
382
367
|
return el;
|
|
383
368
|
};
|
|
384
|
-
if (JSswift.isDev?.()) {
|
|
385
|
-
UI.meta = UI.meta || {};
|
|
386
|
-
UI.meta.cardHeader = {
|
|
387
|
-
signature: "UI.cardHeader(...children) | UI.cardHeader(props, ...children)",
|
|
388
|
-
props: {
|
|
389
|
-
divider: "boolean",
|
|
390
|
-
align: `stretch|flex-start|center|flex-end|baseline`,
|
|
391
|
-
justify: `flex-start|center|flex-end|space-between|space-around|space-evenly`,
|
|
392
|
-
wrap: "boolean",
|
|
393
|
-
direction: "row|column|string",
|
|
394
|
-
gap: "string|number",
|
|
395
|
-
mobile: "{ gap?, align?, justify?, wrap?, direction? }",
|
|
396
|
-
tablet: "{ gap?, align?, justify?, wrap?, direction? }",
|
|
397
|
-
pc: "{ gap?, align?, justify?, wrap?, direction? }",
|
|
398
|
-
slots: "{ default? }",
|
|
399
|
-
class: "string",
|
|
400
|
-
style: "object"
|
|
401
|
-
},
|
|
402
|
-
slots: {
|
|
403
|
-
default: "Card header content"
|
|
404
|
-
},
|
|
405
|
-
returns: "HTMLDivElement",
|
|
406
|
-
description: "Header della card con supporto layout flex."
|
|
407
|
-
};
|
|
408
|
-
UI.meta.cardBody = {
|
|
409
|
-
signature: "UI.cardBody(...children) | UI.cardBody(props, ...children)",
|
|
410
|
-
props: {
|
|
411
|
-
padding: "string|number",
|
|
412
|
-
gap: "string|number",
|
|
413
|
-
direction: "row|column|string",
|
|
414
|
-
mobile: "{ padding?, gap?, direction?, width? }",
|
|
415
|
-
tablet: "{ padding?, gap?, direction?, width? }",
|
|
416
|
-
pc: "{ padding?, gap?, direction?, width? }",
|
|
417
|
-
slots: "{ default? }",
|
|
418
|
-
class: "string",
|
|
419
|
-
style: "object"
|
|
420
|
-
},
|
|
421
|
-
slots: {
|
|
422
|
-
default: "Card body content"
|
|
423
|
-
},
|
|
424
|
-
returns: "HTMLDivElement",
|
|
425
|
-
description: "Body della card."
|
|
426
|
-
};
|
|
427
|
-
UI.meta.cardFooter = {
|
|
428
|
-
signature: "UI.cardFooter(...children) | UI.cardFooter(props, ...children)",
|
|
429
|
-
props: {
|
|
430
|
-
divider: "boolean",
|
|
431
|
-
align: `stretch|flex-start|center|flex-end|baseline`,
|
|
432
|
-
justify: `flex-start|center|flex-end|space-between|space-around|space-evenly`,
|
|
433
|
-
wrap: "boolean",
|
|
434
|
-
direction: "row|column|string",
|
|
435
|
-
gap: "string|number",
|
|
436
|
-
mobile: "{ gap?, align?, justify?, wrap?, direction? }",
|
|
437
|
-
tablet: "{ gap?, align?, justify?, wrap?, direction? }",
|
|
438
|
-
pc: "{ gap?, align?, justify?, wrap?, direction? }",
|
|
439
|
-
slots: "{ default? }",
|
|
440
|
-
class: "string",
|
|
441
|
-
style: "object"
|
|
442
|
-
},
|
|
443
|
-
slots: {
|
|
444
|
-
default: "Card footer content"
|
|
445
|
-
},
|
|
446
|
-
returns: "HTMLDivElement",
|
|
447
|
-
description: "Footer della card con supporto layout flex."
|
|
448
|
-
};
|
|
449
|
-
}
|
|
450
|
-
|