domma-cms 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/admin/css/admin.css +1 -1200
- package/admin/js/api.js +1 -242
- package/admin/js/app.js +5 -279
- package/admin/js/config/sidebar-config.js +1 -115
- package/admin/js/lib/card.js +1 -63
- package/admin/js/lib/image-editor.js +1 -869
- package/admin/js/lib/markdown-toolbar.js +46 -421
- package/admin/js/templates/layouts.html +44 -7
- package/admin/js/templates/page-editor.html +9 -0
- package/admin/js/templates/settings.html +18 -1
- package/admin/js/templates/users.html +29 -4
- package/admin/js/views/collection-editor.js +3 -487
- package/admin/js/views/collection-entries.js +1 -484
- package/admin/js/views/collections.js +1 -153
- package/admin/js/views/dashboard.js +1 -56
- package/admin/js/views/documentation.js +1 -12
- package/admin/js/views/index.js +1 -39
- package/admin/js/views/layouts.js +9 -42
- package/admin/js/views/login.js +7 -251
- package/admin/js/views/media.js +1 -240
- package/admin/js/views/navigation.js +14 -212
- package/admin/js/views/page-editor.js +53 -661
- package/admin/js/views/pages.js +5 -72
- package/admin/js/views/plugins.js +13 -90
- package/admin/js/views/settings.js +1 -199
- package/admin/js/views/tutorials.js +1 -12
- package/admin/js/views/user-editor.js +1 -88
- package/admin/js/views/users.js +7 -76
- package/config/auth.json +1 -17
- package/config/navigation.json +15 -0
- package/config/site.json +5 -4
- package/package.json +1 -1
- package/plugins/domma-effects/public/celebrations/core/canvas.js +2 -104
- package/plugins/domma-effects/public/celebrations/core/particles.js +1 -144
- package/plugins/domma-effects/public/celebrations/core/physics.js +1 -166
- package/plugins/domma-effects/public/celebrations/index.js +1 -535
- package/plugins/domma-effects/public/celebrations/themes/christmas.js +1 -1805
- package/plugins/domma-effects/public/celebrations/themes/guy-fawkes.js +1 -1477
- package/plugins/domma-effects/public/celebrations/themes/halloween.js +1 -1837
- package/plugins/domma-effects/public/celebrations/themes/st-andrews.js +1 -1175
- package/plugins/domma-effects/public/celebrations/themes/st-davids.js +1 -1258
- package/plugins/domma-effects/public/celebrations/themes/st-georges.js +1 -1754
- package/plugins/domma-effects/public/celebrations/themes/st-patricks.js +1 -1290
- package/plugins/domma-effects/public/celebrations/themes/valentines.js +1 -1361
- package/plugins/example-analytics/stats.json +16 -12
- package/plugins/form-builder/admin/templates/form-editor.html +158 -130
- package/plugins/form-builder/admin/views/form-editor.js +3 -1
- package/plugins/form-builder/data/forms/contact-details.json +71 -35
- package/plugins/form-builder/data/forms/feedback.json +130 -0
- package/plugins/form-builder/data/submissions/feedback.json +1 -0
- package/plugins/form-builder/public/form-logic-engine.js +1 -568
- package/public/css/site.css +1 -302
- package/public/js/btt.js +1 -90
- package/public/js/cookie-consent.js +1 -61
- package/public/js/site.js +1 -204
- package/scripts/setup.js +4 -4
- package/server/middleware/auth.js +44 -21
- package/server/routes/api/auth.js +38 -8
- package/server/routes/api/collections.js +18 -5
- package/server/routes/api/layouts.js +18 -4
- package/server/routes/api/media.js +2 -3
- package/server/routes/api/navigation.js +2 -3
- package/server/routes/api/pages.js +3 -3
- package/server/routes/api/settings.js +2 -3
- package/server/routes/api/users.js +4 -6
- package/server/routes/public.js +3 -3
- package/server/server.js +8 -0
- package/server/services/markdown.js +102 -3
- package/server/services/userTypes.js +167 -0
- package/plugins/form-builder/email.js +0 -103
|
@@ -1,568 +1 @@
|
|
|
1
|
-
|
|
2
|
-
* Form Builder — Conditional Logic Engine
|
|
3
|
-
*
|
|
4
|
-
* Pure evaluation functions (universal: browser + Node) + browser runtime class.
|
|
5
|
-
* UMD module: sets window.FormLogicEngine in browser, exports for Node import.
|
|
6
|
-
*
|
|
7
|
-
* Data model:
|
|
8
|
-
* field.logic = {
|
|
9
|
-
* visibility: { default: "visible"|"hidden", conditions: [{ when: ConditionGroup, then: "visible"|"hidden" }] }
|
|
10
|
-
* requirement: { default: true|false, conditions: [{ when: ConditionGroup, then: true|false }] }
|
|
11
|
-
* validation: [{ type: "regex"|"match", pattern?, flags?, field?, message }]
|
|
12
|
-
* cascade: { sourceField: string, mapping: { [value]: [{value,label}] }, defaultOptions: [{value,label}] }
|
|
13
|
-
* }
|
|
14
|
-
*
|
|
15
|
-
* ConditionGroup = { all: [...] } (AND) | { any: [...] } (OR)
|
|
16
|
-
* Condition = { field, operator, value }
|
|
17
|
-
*
|
|
18
|
-
* Operators: equals, not_equals, contains, not_contains, starts_with, ends_with,
|
|
19
|
-
* greater_than, less_than, is_empty, is_not_empty, in, not_in, matches_regex
|
|
20
|
-
*/
|
|
21
|
-
|
|
22
|
-
(function (root, factory) {
|
|
23
|
-
if (typeof module !== 'undefined' && module.exports) {
|
|
24
|
-
// Node / ESM host (Fastify side uses named import)
|
|
25
|
-
module.exports = factory();
|
|
26
|
-
} else {
|
|
27
|
-
// Browser global
|
|
28
|
-
root.FormLogicEngine = factory();
|
|
29
|
-
}
|
|
30
|
-
}(typeof globalThis !== 'undefined' ? globalThis : this, function () {
|
|
31
|
-
'use strict';
|
|
32
|
-
|
|
33
|
-
// -------------------------------------------------------------------------
|
|
34
|
-
// Helpers
|
|
35
|
-
// -------------------------------------------------------------------------
|
|
36
|
-
|
|
37
|
-
function coerce(a) {
|
|
38
|
-
if (a === '' || a === null || a === undefined) return '';
|
|
39
|
-
return String(a);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function numericCoerce(a) {
|
|
43
|
-
const n = parseFloat(a);
|
|
44
|
-
return isNaN(n) ? null : n;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function isEmpty(value) {
|
|
48
|
-
if (value === null || value === undefined) return true;
|
|
49
|
-
if (typeof value === 'string') return value.trim() === '';
|
|
50
|
-
if (Array.isArray(value)) return value.length === 0;
|
|
51
|
-
return false;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// -------------------------------------------------------------------------
|
|
55
|
-
// evaluateCondition — single Condition against form values
|
|
56
|
-
// -------------------------------------------------------------------------
|
|
57
|
-
|
|
58
|
-
function evaluateCondition(condition, values) {
|
|
59
|
-
if (!condition || !condition.field || !condition.operator) return true;
|
|
60
|
-
|
|
61
|
-
const raw = values[condition.field];
|
|
62
|
-
const actual = coerce(raw);
|
|
63
|
-
const expected = coerce(condition.value);
|
|
64
|
-
|
|
65
|
-
switch (condition.operator) {
|
|
66
|
-
case 'equals':
|
|
67
|
-
return actual === expected;
|
|
68
|
-
case 'not_equals':
|
|
69
|
-
return actual !== expected;
|
|
70
|
-
case 'contains':
|
|
71
|
-
return actual.toLowerCase().includes(expected.toLowerCase());
|
|
72
|
-
case 'not_contains':
|
|
73
|
-
return !actual.toLowerCase().includes(expected.toLowerCase());
|
|
74
|
-
case 'starts_with':
|
|
75
|
-
return actual.toLowerCase().startsWith(expected.toLowerCase());
|
|
76
|
-
case 'ends_with':
|
|
77
|
-
return actual.toLowerCase().endsWith(expected.toLowerCase());
|
|
78
|
-
case 'greater_than': {
|
|
79
|
-
const a = numericCoerce(raw);
|
|
80
|
-
const b = numericCoerce(condition.value);
|
|
81
|
-
return a !== null && b !== null && a > b;
|
|
82
|
-
}
|
|
83
|
-
case 'less_than': {
|
|
84
|
-
const a = numericCoerce(raw);
|
|
85
|
-
const b = numericCoerce(condition.value);
|
|
86
|
-
return a !== null && b !== null && a < b;
|
|
87
|
-
}
|
|
88
|
-
case 'is_empty':
|
|
89
|
-
return isEmpty(raw);
|
|
90
|
-
case 'is_not_empty':
|
|
91
|
-
return !isEmpty(raw);
|
|
92
|
-
case 'in': {
|
|
93
|
-
const list = Array.isArray(condition.value)
|
|
94
|
-
? condition.value.map(coerce)
|
|
95
|
-
: String(condition.value).split(',').map(s => s.trim());
|
|
96
|
-
return list.includes(actual);
|
|
97
|
-
}
|
|
98
|
-
case 'not_in': {
|
|
99
|
-
const list = Array.isArray(condition.value)
|
|
100
|
-
? condition.value.map(coerce)
|
|
101
|
-
: String(condition.value).split(',').map(s => s.trim());
|
|
102
|
-
return !list.includes(actual);
|
|
103
|
-
}
|
|
104
|
-
case 'matches_regex': {
|
|
105
|
-
try {
|
|
106
|
-
const flags = condition.flags || '';
|
|
107
|
-
const rx = new RegExp(condition.value, flags);
|
|
108
|
-
return rx.test(actual);
|
|
109
|
-
} catch {
|
|
110
|
-
return false;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
default:
|
|
114
|
-
return true;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// -------------------------------------------------------------------------
|
|
119
|
-
// evaluateConditionGroup — AND/OR recursive evaluation
|
|
120
|
-
// -------------------------------------------------------------------------
|
|
121
|
-
|
|
122
|
-
function evaluateConditionGroup(group, values) {
|
|
123
|
-
if (!group) return true;
|
|
124
|
-
|
|
125
|
-
if (Array.isArray(group.all)) {
|
|
126
|
-
return group.all.every(item => {
|
|
127
|
-
if (item.all || item.any) return evaluateConditionGroup(item, values);
|
|
128
|
-
return evaluateCondition(item, values);
|
|
129
|
-
});
|
|
130
|
-
}
|
|
131
|
-
if (Array.isArray(group.any)) {
|
|
132
|
-
return group.any.some(item => {
|
|
133
|
-
if (item.all || item.any) return evaluateConditionGroup(item, values);
|
|
134
|
-
return evaluateCondition(item, values);
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
|
-
// Plain condition (backward compat)
|
|
138
|
-
if (group.field && group.operator) return evaluateCondition(group, values);
|
|
139
|
-
return true;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// -------------------------------------------------------------------------
|
|
143
|
-
// evaluateFieldVisibility — returns "visible" | "hidden"
|
|
144
|
-
// -------------------------------------------------------------------------
|
|
145
|
-
|
|
146
|
-
function evaluateFieldVisibility(field, values) {
|
|
147
|
-
const logic = field.logic;
|
|
148
|
-
if (!logic || !logic.visibility) return 'visible';
|
|
149
|
-
|
|
150
|
-
const vis = logic.visibility;
|
|
151
|
-
const conditions = vis.conditions || [];
|
|
152
|
-
|
|
153
|
-
for (const rule of conditions) {
|
|
154
|
-
if (rule.when && evaluateConditionGroup(rule.when, values)) {
|
|
155
|
-
return rule.then === 'hidden' ? 'hidden' : 'visible';
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
return vis.default === 'hidden' ? 'hidden' : 'visible';
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// -------------------------------------------------------------------------
|
|
162
|
-
// evaluateFieldRequirement — returns true | false
|
|
163
|
-
// -------------------------------------------------------------------------
|
|
164
|
-
|
|
165
|
-
function evaluateFieldRequirement(field, values) {
|
|
166
|
-
const logic = field.logic;
|
|
167
|
-
if (!logic || !logic.requirement) return field.required || false;
|
|
168
|
-
|
|
169
|
-
const req = logic.requirement;
|
|
170
|
-
const conditions = req.conditions || [];
|
|
171
|
-
|
|
172
|
-
for (const rule of conditions) {
|
|
173
|
-
if (rule.when && evaluateConditionGroup(rule.when, values)) {
|
|
174
|
-
return rule.then === true;
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
return req.default === true;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// -------------------------------------------------------------------------
|
|
181
|
-
// validateField — returns array of { message } (empty = valid)
|
|
182
|
-
// -------------------------------------------------------------------------
|
|
183
|
-
|
|
184
|
-
function validateField(field, value, values) {
|
|
185
|
-
const logic = field.logic;
|
|
186
|
-
if (!logic || !Array.isArray(logic.validation)) return [];
|
|
187
|
-
|
|
188
|
-
const errors = [];
|
|
189
|
-
for (const rule of logic.validation) {
|
|
190
|
-
switch (rule.type) {
|
|
191
|
-
case 'regex': {
|
|
192
|
-
if (!isEmpty(value)) {
|
|
193
|
-
try {
|
|
194
|
-
const rx = new RegExp(rule.pattern || '', rule.flags || '');
|
|
195
|
-
if (!rx.test(coerce(value))) {
|
|
196
|
-
errors.push({ message: rule.message || 'Invalid format.' });
|
|
197
|
-
}
|
|
198
|
-
} catch {
|
|
199
|
-
// Invalid regex — skip
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
break;
|
|
203
|
-
}
|
|
204
|
-
case 'match': {
|
|
205
|
-
const other = values[rule.field];
|
|
206
|
-
if (!isEmpty(value) && coerce(value) !== coerce(other)) {
|
|
207
|
-
errors.push({ message: rule.message || 'Fields do not match.' });
|
|
208
|
-
}
|
|
209
|
-
break;
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
return errors;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// -------------------------------------------------------------------------
|
|
217
|
-
// getCascadeOptions — returns options array or null
|
|
218
|
-
// -------------------------------------------------------------------------
|
|
219
|
-
|
|
220
|
-
function getCascadeOptions(field, values) {
|
|
221
|
-
const logic = field.logic;
|
|
222
|
-
if (!logic || !logic.cascade || !logic.cascade.sourceField) return null;
|
|
223
|
-
|
|
224
|
-
const cascade = logic.cascade;
|
|
225
|
-
const sourceValue = coerce(values[cascade.sourceField]);
|
|
226
|
-
const mapping = cascade.mapping || {};
|
|
227
|
-
|
|
228
|
-
if (Object.prototype.hasOwnProperty.call(mapping, sourceValue)) {
|
|
229
|
-
return mapping[sourceValue] || [];
|
|
230
|
-
}
|
|
231
|
-
return cascade.defaultOptions || null;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
// =========================================================================
|
|
235
|
-
// Transition helpers (browser-only)
|
|
236
|
-
// =========================================================================
|
|
237
|
-
|
|
238
|
-
function _fbHide(el, transition) {
|
|
239
|
-
if (transition === 'fade') {
|
|
240
|
-
el.style.transition = 'opacity 0.25s ease';
|
|
241
|
-
el.style.opacity = '1';
|
|
242
|
-
requestAnimationFrame(function () {
|
|
243
|
-
el.style.opacity = '0';
|
|
244
|
-
setTimeout(function () {
|
|
245
|
-
el.classList.add('fb-field-hidden');
|
|
246
|
-
el.style.transition = '';
|
|
247
|
-
el.style.opacity = '';
|
|
248
|
-
}, 260);
|
|
249
|
-
});
|
|
250
|
-
} else if (transition === 'slide') {
|
|
251
|
-
el.style.overflow = 'hidden';
|
|
252
|
-
el.style.maxHeight = el.scrollHeight + 'px';
|
|
253
|
-
el.style.transition = 'max-height 0.3s ease, opacity 0.2s ease';
|
|
254
|
-
requestAnimationFrame(function () {
|
|
255
|
-
el.style.maxHeight = '0';
|
|
256
|
-
el.style.opacity = '0';
|
|
257
|
-
setTimeout(function () {
|
|
258
|
-
el.classList.add('fb-field-hidden');
|
|
259
|
-
el.style.transition = '';
|
|
260
|
-
el.style.maxHeight = '';
|
|
261
|
-
el.style.opacity = '';
|
|
262
|
-
el.style.overflow = '';
|
|
263
|
-
}, 320);
|
|
264
|
-
});
|
|
265
|
-
} else if (transition === 'scale') {
|
|
266
|
-
el.style.transition = 'opacity 0.2s ease, transform 0.2s ease';
|
|
267
|
-
el.style.transform = 'scale(1)';
|
|
268
|
-
el.style.opacity = '1';
|
|
269
|
-
requestAnimationFrame(function () {
|
|
270
|
-
el.style.transform = 'scale(0.95)';
|
|
271
|
-
el.style.opacity = '0';
|
|
272
|
-
setTimeout(function () {
|
|
273
|
-
el.classList.add('fb-field-hidden');
|
|
274
|
-
el.style.transition = '';
|
|
275
|
-
el.style.transform = '';
|
|
276
|
-
el.style.opacity = '';
|
|
277
|
-
}, 220);
|
|
278
|
-
});
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
function _fbShow(el, transition) {
|
|
283
|
-
el.classList.remove('fb-field-hidden');
|
|
284
|
-
if (transition === 'fade') {
|
|
285
|
-
el.style.opacity = '0';
|
|
286
|
-
el.style.transition = 'opacity 0.25s ease';
|
|
287
|
-
requestAnimationFrame(function () {
|
|
288
|
-
el.style.opacity = '1';
|
|
289
|
-
setTimeout(function () {
|
|
290
|
-
el.style.transition = '';
|
|
291
|
-
el.style.opacity = '';
|
|
292
|
-
}, 260);
|
|
293
|
-
});
|
|
294
|
-
} else if (transition === 'slide') {
|
|
295
|
-
el.style.overflow = 'hidden';
|
|
296
|
-
el.style.maxHeight = '0';
|
|
297
|
-
el.style.opacity = '0';
|
|
298
|
-
el.style.transition = 'max-height 0.3s ease, opacity 0.2s ease';
|
|
299
|
-
requestAnimationFrame(function () {
|
|
300
|
-
el.style.maxHeight = el.scrollHeight + 'px';
|
|
301
|
-
el.style.opacity = '1';
|
|
302
|
-
setTimeout(function () {
|
|
303
|
-
el.style.transition = '';
|
|
304
|
-
el.style.maxHeight = '';
|
|
305
|
-
el.style.opacity = '';
|
|
306
|
-
el.style.overflow = '';
|
|
307
|
-
}, 320);
|
|
308
|
-
});
|
|
309
|
-
} else if (transition === 'scale') {
|
|
310
|
-
el.style.transform = 'scale(0.95)';
|
|
311
|
-
el.style.opacity = '0';
|
|
312
|
-
el.style.transition = 'opacity 0.2s ease, transform 0.2s ease';
|
|
313
|
-
requestAnimationFrame(function () {
|
|
314
|
-
el.style.transform = 'scale(1)';
|
|
315
|
-
el.style.opacity = '1';
|
|
316
|
-
setTimeout(function () {
|
|
317
|
-
el.style.transition = '';
|
|
318
|
-
el.style.transform = '';
|
|
319
|
-
el.style.opacity = '';
|
|
320
|
-
}, 220);
|
|
321
|
-
});
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
// =========================================================================
|
|
326
|
-
// Browser Runtime Class
|
|
327
|
-
// =========================================================================
|
|
328
|
-
|
|
329
|
-
/**
|
|
330
|
-
* FormLogicRuntime — attaches to a rendered form element and drives
|
|
331
|
-
* conditional logic reactively as the user interacts with fields.
|
|
332
|
-
*
|
|
333
|
-
* @param {Object} formDefinition — form JSON with fields array
|
|
334
|
-
* @param {Element} formElement — the wrapper DOM element
|
|
335
|
-
*/
|
|
336
|
-
function FormLogicRuntime(formDefinition, formElement) {
|
|
337
|
-
this._form = formDefinition;
|
|
338
|
-
this._wrapper = formElement;
|
|
339
|
-
this._fields = (formDefinition.fields || []).filter(f => f.type !== 'page-break');
|
|
340
|
-
this._listeners = []; // [{ el, event, fn }] for cleanup
|
|
341
|
-
this._depMap = new Map(); // sourceField → Set<targetField.name>
|
|
342
|
-
this._initialEval = false; // true during first evaluate() — skips transitions
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
// Build dependency map: which fields depend on which source fields
|
|
346
|
-
FormLogicRuntime.prototype._buildDepMap = function () {
|
|
347
|
-
this._depMap.clear();
|
|
348
|
-
const self = this;
|
|
349
|
-
|
|
350
|
-
self._fields.forEach(function (field) {
|
|
351
|
-
const logic = field.logic;
|
|
352
|
-
if (!logic) return;
|
|
353
|
-
|
|
354
|
-
function scanGroup(group) {
|
|
355
|
-
if (!group) return;
|
|
356
|
-
const items = group.all || group.any || [];
|
|
357
|
-
items.forEach(function (item) {
|
|
358
|
-
if (item.field) {
|
|
359
|
-
if (!self._depMap.has(item.field)) self._depMap.set(item.field, new Set());
|
|
360
|
-
self._depMap.get(item.field).add(field.name);
|
|
361
|
-
} else {
|
|
362
|
-
scanGroup(item);
|
|
363
|
-
}
|
|
364
|
-
});
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
// Visibility
|
|
368
|
-
if (logic.visibility) {
|
|
369
|
-
(logic.visibility.conditions || []).forEach(function (rule) { scanGroup(rule.when); });
|
|
370
|
-
}
|
|
371
|
-
// Requirement
|
|
372
|
-
if (logic.requirement) {
|
|
373
|
-
(logic.requirement.conditions || []).forEach(function (rule) { scanGroup(rule.when); });
|
|
374
|
-
}
|
|
375
|
-
// Cascade
|
|
376
|
-
if (logic.cascade && logic.cascade.sourceField) {
|
|
377
|
-
const src = logic.cascade.sourceField;
|
|
378
|
-
if (!self._depMap.has(src)) self._depMap.set(src, new Set());
|
|
379
|
-
self._depMap.get(src).add(field.name);
|
|
380
|
-
}
|
|
381
|
-
});
|
|
382
|
-
};
|
|
383
|
-
|
|
384
|
-
// Collect current form values from the DOM
|
|
385
|
-
FormLogicRuntime.prototype._getValues = function () {
|
|
386
|
-
const values = {};
|
|
387
|
-
this._fields.forEach(function (field) {
|
|
388
|
-
const el = this._wrapper.querySelector('[name="' + field.name + '"]');
|
|
389
|
-
if (!el) return;
|
|
390
|
-
if (el.type === 'radio') {
|
|
391
|
-
// querySelector returns the first radio — must find the checked one
|
|
392
|
-
const checked = this._wrapper.querySelector('[name="' + field.name + '"]:checked');
|
|
393
|
-
values[field.name] = checked ? checked.value : '';
|
|
394
|
-
} else if (el.type === 'checkbox') {
|
|
395
|
-
values[field.name] = el.checked ? el.value || 'true' : '';
|
|
396
|
-
} else {
|
|
397
|
-
values[field.name] = el.value;
|
|
398
|
-
}
|
|
399
|
-
}, this);
|
|
400
|
-
return values;
|
|
401
|
-
};
|
|
402
|
-
|
|
403
|
-
// Apply visibility to a field's wrapper element
|
|
404
|
-
FormLogicRuntime.prototype._applyVisibility = function (field, values) {
|
|
405
|
-
const vis = evaluateFieldVisibility(field, values);
|
|
406
|
-
const hidden = vis === 'hidden';
|
|
407
|
-
const wrapper = this._findFieldWrapper(field.name);
|
|
408
|
-
if (!wrapper) return;
|
|
409
|
-
|
|
410
|
-
wrapper.setAttribute('aria-hidden', String(hidden));
|
|
411
|
-
|
|
412
|
-
const transition = this._initialEval ? 'none' : (field.logic?.visibility?.transition || 'none');
|
|
413
|
-
if (transition === 'none') {
|
|
414
|
-
wrapper.classList.toggle('fb-field-hidden', hidden);
|
|
415
|
-
return;
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
// Cancel any pending transition by resetting inline styles
|
|
419
|
-
wrapper.style.transition = '';
|
|
420
|
-
if (hidden) {
|
|
421
|
-
_fbHide(wrapper, transition);
|
|
422
|
-
} else {
|
|
423
|
-
_fbShow(wrapper, transition);
|
|
424
|
-
}
|
|
425
|
-
};
|
|
426
|
-
|
|
427
|
-
// Apply requirement to the input element
|
|
428
|
-
FormLogicRuntime.prototype._applyRequirement = function (field, values) {
|
|
429
|
-
const required = evaluateFieldRequirement(field, values);
|
|
430
|
-
const el = this._wrapper.querySelector('[name="' + field.name + '"]');
|
|
431
|
-
if (!el) return;
|
|
432
|
-
if (required) {
|
|
433
|
-
el.setAttribute('required', '');
|
|
434
|
-
} else {
|
|
435
|
-
el.removeAttribute('required');
|
|
436
|
-
}
|
|
437
|
-
};
|
|
438
|
-
|
|
439
|
-
// Apply cascade options to a select element
|
|
440
|
-
FormLogicRuntime.prototype._applyCascade = function (field, values) {
|
|
441
|
-
const options = getCascadeOptions(field, values);
|
|
442
|
-
if (options === null) return;
|
|
443
|
-
const sel = this._wrapper.querySelector('select[name="' + field.name + '"]');
|
|
444
|
-
if (!sel) return;
|
|
445
|
-
const current = sel.value;
|
|
446
|
-
sel.textContent = '';
|
|
447
|
-
options.forEach(function (opt) {
|
|
448
|
-
const o = document.createElement('option');
|
|
449
|
-
o.value = opt.value;
|
|
450
|
-
o.textContent = opt.label || opt.value;
|
|
451
|
-
if (opt.value === current) o.selected = true;
|
|
452
|
-
sel.appendChild(o);
|
|
453
|
-
});
|
|
454
|
-
};
|
|
455
|
-
|
|
456
|
-
// Show inline validation errors for a field (on blur)
|
|
457
|
-
FormLogicRuntime.prototype._applyValidation = function (field, values) {
|
|
458
|
-
const el = this._wrapper.querySelector('[name="' + field.name + '"]');
|
|
459
|
-
if (!el) return;
|
|
460
|
-
const value = el.type === 'checkbox' ? (el.checked ? el.value || 'true' : '') : el.value;
|
|
461
|
-
const errors = validateField(field, value, values);
|
|
462
|
-
|
|
463
|
-
let errEl = el.parentNode.querySelector('.fb-validation-error');
|
|
464
|
-
if (errors.length) {
|
|
465
|
-
if (!errEl) {
|
|
466
|
-
errEl = document.createElement('div');
|
|
467
|
-
errEl.className = 'fb-validation-error';
|
|
468
|
-
el.parentNode.appendChild(errEl);
|
|
469
|
-
}
|
|
470
|
-
errEl.textContent = errors[0].message;
|
|
471
|
-
} else {
|
|
472
|
-
if (errEl) errEl.remove();
|
|
473
|
-
}
|
|
474
|
-
};
|
|
475
|
-
|
|
476
|
-
// Find the block-level wrapper for a field — direct child of the form element.
|
|
477
|
-
// This is robust across rendering strategies: Domma, manual, wizard.
|
|
478
|
-
FormLogicRuntime.prototype._findFieldWrapper = function (fieldName) {
|
|
479
|
-
const el = this._wrapper.querySelector('[name="' + fieldName + '"]');
|
|
480
|
-
if (!el) return null;
|
|
481
|
-
// Use the nearest <form> as the boundary, falling back to the outer wrapper
|
|
482
|
-
const boundary = el.closest('form') || this._wrapper;
|
|
483
|
-
// Walk up to find the direct child of the boundary
|
|
484
|
-
let node = el;
|
|
485
|
-
while (node.parentNode && node.parentNode !== boundary) {
|
|
486
|
-
node = node.parentNode;
|
|
487
|
-
}
|
|
488
|
-
return node;
|
|
489
|
-
};
|
|
490
|
-
|
|
491
|
-
// Evaluate all fields (or just those depending on changedField)
|
|
492
|
-
FormLogicRuntime.prototype.evaluate = function (changedFieldName) {
|
|
493
|
-
const values = this._getValues();
|
|
494
|
-
const self = this;
|
|
495
|
-
|
|
496
|
-
let targets;
|
|
497
|
-
if (changedFieldName && this._depMap.has(changedFieldName)) {
|
|
498
|
-
const depNames = this._depMap.get(changedFieldName);
|
|
499
|
-
targets = this._fields.filter(function (f) { return depNames.has(f.name); });
|
|
500
|
-
} else {
|
|
501
|
-
targets = this._fields;
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
targets.forEach(function (field) {
|
|
505
|
-
if (!field.logic) return;
|
|
506
|
-
self._applyVisibility(field, values);
|
|
507
|
-
self._applyRequirement(field, values);
|
|
508
|
-
self._applyCascade(field, values);
|
|
509
|
-
});
|
|
510
|
-
};
|
|
511
|
-
|
|
512
|
-
// Attach listeners and run initial evaluation
|
|
513
|
-
FormLogicRuntime.prototype.init = function () {
|
|
514
|
-
this._buildDepMap();
|
|
515
|
-
const self = this;
|
|
516
|
-
|
|
517
|
-
// Listen for changes on all form inputs
|
|
518
|
-
const allInputs = this._wrapper.querySelectorAll('input, select, textarea');
|
|
519
|
-
allInputs.forEach(function (el) {
|
|
520
|
-
const name = el.getAttribute('name');
|
|
521
|
-
if (!name) return;
|
|
522
|
-
|
|
523
|
-
const changeFn = function () { self.evaluate(name); };
|
|
524
|
-
el.addEventListener('input', changeFn);
|
|
525
|
-
el.addEventListener('change', changeFn);
|
|
526
|
-
self._listeners.push({ el: el, event: 'input', fn: changeFn });
|
|
527
|
-
self._listeners.push({ el: el, event: 'change', fn: changeFn });
|
|
528
|
-
|
|
529
|
-
// Validation on blur (only if field has validation rules)
|
|
530
|
-
const field = self._fields.find(function (f) { return f.name === name; });
|
|
531
|
-
if (field && field.logic && Array.isArray(field.logic.validation) && field.logic.validation.length) {
|
|
532
|
-
const blurFn = function () {
|
|
533
|
-
const values = self._getValues();
|
|
534
|
-
self._applyValidation(field, values);
|
|
535
|
-
};
|
|
536
|
-
el.addEventListener('blur', blurFn);
|
|
537
|
-
self._listeners.push({ el: el, event: 'blur', fn: blurFn });
|
|
538
|
-
}
|
|
539
|
-
});
|
|
540
|
-
|
|
541
|
-
// Initial pass — no transitions on first render
|
|
542
|
-
this._initialEval = true;
|
|
543
|
-
this.evaluate();
|
|
544
|
-
this._initialEval = false;
|
|
545
|
-
};
|
|
546
|
-
|
|
547
|
-
// Remove all attached listeners
|
|
548
|
-
FormLogicRuntime.prototype.destroy = function () {
|
|
549
|
-
this._listeners.forEach(function (item) {
|
|
550
|
-
item.el.removeEventListener(item.event, item.fn);
|
|
551
|
-
});
|
|
552
|
-
this._listeners = [];
|
|
553
|
-
};
|
|
554
|
-
|
|
555
|
-
// =========================================================================
|
|
556
|
-
// Public API
|
|
557
|
-
// =========================================================================
|
|
558
|
-
|
|
559
|
-
return {
|
|
560
|
-
evaluateCondition,
|
|
561
|
-
evaluateConditionGroup,
|
|
562
|
-
evaluateFieldVisibility,
|
|
563
|
-
evaluateFieldRequirement,
|
|
564
|
-
validateField,
|
|
565
|
-
getCascadeOptions,
|
|
566
|
-
FormLogicRuntime
|
|
567
|
-
};
|
|
568
|
-
}));
|
|
1
|
+
(function(c,l){typeof module<"u"&&module.exports?module.exports=l():c.FormLogicEngine=l()})(typeof globalThis<"u"?globalThis:this,function(){"use strict";function c(e){return e===""||e===null||e===void 0?"":String(e)}function l(e){const s=parseFloat(e);return isNaN(s)?null:s}function f(e){return e==null?!0:typeof e=="string"?e.trim()==="":Array.isArray(e)?e.length===0:!1}function p(e,s){if(!e||!e.field||!e.operator)return!0;const t=s[e.field],n=c(t),i=c(e.value);switch(e.operator){case"equals":return n===i;case"not_equals":return n!==i;case"contains":return n.toLowerCase().includes(i.toLowerCase());case"not_contains":return!n.toLowerCase().includes(i.toLowerCase());case"starts_with":return n.toLowerCase().startsWith(i.toLowerCase());case"ends_with":return n.toLowerCase().endsWith(i.toLowerCase());case"greater_than":{const r=l(t),a=l(e.value);return r!==null&&a!==null&&r>a}case"less_than":{const r=l(t),a=l(e.value);return r!==null&&a!==null&&r<a}case"is_empty":return f(t);case"is_not_empty":return!f(t);case"in":return(Array.isArray(e.value)?e.value.map(c):String(e.value).split(",").map(a=>a.trim())).includes(n);case"not_in":return!(Array.isArray(e.value)?e.value.map(c):String(e.value).split(",").map(a=>a.trim())).includes(n);case"matches_regex":try{const r=e.flags||"";return new RegExp(e.value,r).test(n)}catch{return!1}default:return!0}}function u(e,s){return e?Array.isArray(e.all)?e.all.every(t=>t.all||t.any?u(t,s):p(t,s)):Array.isArray(e.any)?e.any.some(t=>t.all||t.any?u(t,s):p(t,s)):e.field&&e.operator?p(e,s):!0:!0}function y(e,s){const t=e.logic;if(!t||!t.visibility)return"visible";const n=t.visibility,i=n.conditions||[];for(const r of i)if(r.when&&u(r.when,s))return r.then==="hidden"?"hidden":"visible";return n.default==="hidden"?"hidden":"visible"}function d(e,s){const t=e.logic;if(!t||!t.requirement)return e.required||!1;const n=t.requirement,i=n.conditions||[];for(const r of i)if(r.when&&u(r.when,s))return r.then===!0;return n.default===!0}function h(e,s,t){const n=e.logic;if(!n||!Array.isArray(n.validation))return[];const i=[];for(const r of n.validation)switch(r.type){case"regex":{if(!f(s))try{new RegExp(r.pattern||"",r.flags||"").test(c(s))||i.push({message:r.message||"Invalid format."})}catch{}break}case"match":{const a=t[r.field];!f(s)&&c(s)!==c(a)&&i.push({message:r.message||"Fields do not match."});break}}return i}function m(e,s){const t=e.logic;if(!t||!t.cascade||!t.cascade.sourceField)return null;const n=t.cascade,i=c(s[n.sourceField]),r=n.mapping||{};return Object.prototype.hasOwnProperty.call(r,i)?r[i]||[]:n.defaultOptions||null}function _(e,s){s==="fade"?(e.style.transition="opacity 0.25s ease",e.style.opacity="1",requestAnimationFrame(function(){e.style.opacity="0",setTimeout(function(){e.classList.add("fb-field-hidden"),e.style.transition="",e.style.opacity=""},260)})):s==="slide"?(e.style.overflow="hidden",e.style.maxHeight=e.scrollHeight+"px",e.style.transition="max-height 0.3s ease, opacity 0.2s ease",requestAnimationFrame(function(){e.style.maxHeight="0",e.style.opacity="0",setTimeout(function(){e.classList.add("fb-field-hidden"),e.style.transition="",e.style.maxHeight="",e.style.opacity="",e.style.overflow=""},320)})):s==="scale"&&(e.style.transition="opacity 0.2s ease, transform 0.2s ease",e.style.transform="scale(1)",e.style.opacity="1",requestAnimationFrame(function(){e.style.transform="scale(0.95)",e.style.opacity="0",setTimeout(function(){e.classList.add("fb-field-hidden"),e.style.transition="",e.style.transform="",e.style.opacity=""},220)}))}function v(e,s){e.classList.remove("fb-field-hidden"),s==="fade"?(e.style.opacity="0",e.style.transition="opacity 0.25s ease",requestAnimationFrame(function(){e.style.opacity="1",setTimeout(function(){e.style.transition="",e.style.opacity=""},260)})):s==="slide"?(e.style.overflow="hidden",e.style.maxHeight="0",e.style.opacity="0",e.style.transition="max-height 0.3s ease, opacity 0.2s ease",requestAnimationFrame(function(){e.style.maxHeight=e.scrollHeight+"px",e.style.opacity="1",setTimeout(function(){e.style.transition="",e.style.maxHeight="",e.style.opacity="",e.style.overflow=""},320)})):s==="scale"&&(e.style.transform="scale(0.95)",e.style.opacity="0",e.style.transition="opacity 0.2s ease, transform 0.2s ease",requestAnimationFrame(function(){e.style.transform="scale(1)",e.style.opacity="1",setTimeout(function(){e.style.transition="",e.style.transform="",e.style.opacity=""},220)}))}function o(e,s){this._form=e,this._wrapper=s,this._fields=(e.fields||[]).filter(t=>t.type!=="page-break"),this._listeners=[],this._depMap=new Map,this._initialEval=!1}return o.prototype._buildDepMap=function(){this._depMap.clear();const e=this;e._fields.forEach(function(s){const t=s.logic;if(!t)return;function n(i){if(!i)return;(i.all||i.any||[]).forEach(function(a){a.field?(e._depMap.has(a.field)||e._depMap.set(a.field,new Set),e._depMap.get(a.field).add(s.name)):n(a)})}if(t.visibility&&(t.visibility.conditions||[]).forEach(function(i){n(i.when)}),t.requirement&&(t.requirement.conditions||[]).forEach(function(i){n(i.when)}),t.cascade&&t.cascade.sourceField){const i=t.cascade.sourceField;e._depMap.has(i)||e._depMap.set(i,new Set),e._depMap.get(i).add(s.name)}})},o.prototype._getValues=function(){const e={};return this._fields.forEach(function(s){const t=this._wrapper.querySelector('[name="'+s.name+'"]');if(t)if(t.type==="radio"){const n=this._wrapper.querySelector('[name="'+s.name+'"]:checked');e[s.name]=n?n.value:""}else t.type==="checkbox"?e[s.name]=t.checked?t.value||"true":"":e[s.name]=t.value},this),e},o.prototype._applyVisibility=function(e,s){const n=y(e,s)==="hidden",i=this._findFieldWrapper(e.name);if(!i)return;i.setAttribute("aria-hidden",String(n));const r=this._initialEval?"none":e.logic?.visibility?.transition||"none";if(r==="none"){i.classList.toggle("fb-field-hidden",n);return}i.style.transition="",n?_(i,r):v(i,r)},o.prototype._applyRequirement=function(e,s){const t=d(e,s),n=this._wrapper.querySelector('[name="'+e.name+'"]');n&&(t?n.setAttribute("required",""):n.removeAttribute("required"))},o.prototype._applyCascade=function(e,s){const t=m(e,s);if(t===null)return;const n=this._wrapper.querySelector('select[name="'+e.name+'"]');if(!n)return;const i=n.value;n.textContent="",t.forEach(function(r){const a=document.createElement("option");a.value=r.value,a.textContent=r.label||r.value,r.value===i&&(a.selected=!0),n.appendChild(a)})},o.prototype._applyValidation=function(e,s){const t=this._wrapper.querySelector('[name="'+e.name+'"]');if(!t)return;const n=t.type==="checkbox"?t.checked?t.value||"true":"":t.value,i=h(e,n,s);let r=t.parentNode.querySelector(".fb-validation-error");i.length?(r||(r=document.createElement("div"),r.className="fb-validation-error",t.parentNode.appendChild(r)),r.textContent=i[0].message):r&&r.remove()},o.prototype._findFieldWrapper=function(e){const s=this._wrapper.querySelector('[name="'+e+'"]');if(!s)return null;const t=s.closest("form")||this._wrapper;let n=s;for(;n.parentNode&&n.parentNode!==t;)n=n.parentNode;return n},o.prototype.evaluate=function(e){const s=this._getValues(),t=this;let n;if(e&&this._depMap.has(e)){const i=this._depMap.get(e);n=this._fields.filter(function(r){return i.has(r.name)})}else n=this._fields;n.forEach(function(i){i.logic&&(t._applyVisibility(i,s),t._applyRequirement(i,s),t._applyCascade(i,s))})},o.prototype.init=function(){this._buildDepMap();const e=this;this._wrapper.querySelectorAll("input, select, textarea").forEach(function(t){const n=t.getAttribute("name");if(!n)return;const i=function(){e.evaluate(n)};t.addEventListener("input",i),t.addEventListener("change",i),e._listeners.push({el:t,event:"input",fn:i}),e._listeners.push({el:t,event:"change",fn:i});const r=e._fields.find(function(a){return a.name===n});if(r&&r.logic&&Array.isArray(r.logic.validation)&&r.logic.validation.length){const a=function(){const g=e._getValues();e._applyValidation(r,g)};t.addEventListener("blur",a),e._listeners.push({el:t,event:"blur",fn:a})}}),this._initialEval=!0,this.evaluate(),this._initialEval=!1},o.prototype.destroy=function(){this._listeners.forEach(function(e){e.el.removeEventListener(e.event,e.fn)}),this._listeners=[]},{evaluateCondition:p,evaluateConditionGroup:u,evaluateFieldVisibility:y,evaluateFieldRequirement:d,validateField:h,getCascadeOptions:m,FormLogicRuntime:o}});
|