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.
Files changed (70) hide show
  1. package/admin/css/admin.css +1 -1200
  2. package/admin/js/api.js +1 -242
  3. package/admin/js/app.js +5 -279
  4. package/admin/js/config/sidebar-config.js +1 -115
  5. package/admin/js/lib/card.js +1 -63
  6. package/admin/js/lib/image-editor.js +1 -869
  7. package/admin/js/lib/markdown-toolbar.js +46 -421
  8. package/admin/js/templates/layouts.html +44 -7
  9. package/admin/js/templates/page-editor.html +9 -0
  10. package/admin/js/templates/settings.html +18 -1
  11. package/admin/js/templates/users.html +29 -4
  12. package/admin/js/views/collection-editor.js +3 -487
  13. package/admin/js/views/collection-entries.js +1 -484
  14. package/admin/js/views/collections.js +1 -153
  15. package/admin/js/views/dashboard.js +1 -56
  16. package/admin/js/views/documentation.js +1 -12
  17. package/admin/js/views/index.js +1 -39
  18. package/admin/js/views/layouts.js +9 -42
  19. package/admin/js/views/login.js +7 -251
  20. package/admin/js/views/media.js +1 -240
  21. package/admin/js/views/navigation.js +14 -212
  22. package/admin/js/views/page-editor.js +53 -661
  23. package/admin/js/views/pages.js +5 -72
  24. package/admin/js/views/plugins.js +13 -90
  25. package/admin/js/views/settings.js +1 -199
  26. package/admin/js/views/tutorials.js +1 -12
  27. package/admin/js/views/user-editor.js +1 -88
  28. package/admin/js/views/users.js +7 -76
  29. package/config/auth.json +1 -17
  30. package/config/navigation.json +15 -0
  31. package/config/site.json +5 -4
  32. package/package.json +1 -1
  33. package/plugins/domma-effects/public/celebrations/core/canvas.js +2 -104
  34. package/plugins/domma-effects/public/celebrations/core/particles.js +1 -144
  35. package/plugins/domma-effects/public/celebrations/core/physics.js +1 -166
  36. package/plugins/domma-effects/public/celebrations/index.js +1 -535
  37. package/plugins/domma-effects/public/celebrations/themes/christmas.js +1 -1805
  38. package/plugins/domma-effects/public/celebrations/themes/guy-fawkes.js +1 -1477
  39. package/plugins/domma-effects/public/celebrations/themes/halloween.js +1 -1837
  40. package/plugins/domma-effects/public/celebrations/themes/st-andrews.js +1 -1175
  41. package/plugins/domma-effects/public/celebrations/themes/st-davids.js +1 -1258
  42. package/plugins/domma-effects/public/celebrations/themes/st-georges.js +1 -1754
  43. package/plugins/domma-effects/public/celebrations/themes/st-patricks.js +1 -1290
  44. package/plugins/domma-effects/public/celebrations/themes/valentines.js +1 -1361
  45. package/plugins/example-analytics/stats.json +16 -12
  46. package/plugins/form-builder/admin/templates/form-editor.html +158 -130
  47. package/plugins/form-builder/admin/views/form-editor.js +3 -1
  48. package/plugins/form-builder/data/forms/contact-details.json +71 -35
  49. package/plugins/form-builder/data/forms/feedback.json +130 -0
  50. package/plugins/form-builder/data/submissions/feedback.json +1 -0
  51. package/plugins/form-builder/public/form-logic-engine.js +1 -568
  52. package/public/css/site.css +1 -302
  53. package/public/js/btt.js +1 -90
  54. package/public/js/cookie-consent.js +1 -61
  55. package/public/js/site.js +1 -204
  56. package/scripts/setup.js +4 -4
  57. package/server/middleware/auth.js +44 -21
  58. package/server/routes/api/auth.js +38 -8
  59. package/server/routes/api/collections.js +18 -5
  60. package/server/routes/api/layouts.js +18 -4
  61. package/server/routes/api/media.js +2 -3
  62. package/server/routes/api/navigation.js +2 -3
  63. package/server/routes/api/pages.js +3 -3
  64. package/server/routes/api/settings.js +2 -3
  65. package/server/routes/api/users.js +4 -6
  66. package/server/routes/public.js +3 -3
  67. package/server/server.js +8 -0
  68. package/server/services/markdown.js +102 -3
  69. package/server/services/userTypes.js +167 -0
  70. 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}});