domma-cms 0.1.0 → 0.2.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 +78 -1
- package/admin/js/api.js +32 -0
- package/admin/js/app.js +24 -7
- package/admin/js/config/sidebar-config.js +8 -0
- package/admin/js/templates/collection-editor.html +80 -0
- package/admin/js/templates/collection-entries.html +36 -0
- package/admin/js/templates/collections.html +12 -0
- package/admin/js/templates/documentation.html +136 -0
- package/admin/js/templates/navigation.html +26 -4
- package/admin/js/templates/page-editor.html +91 -85
- package/admin/js/templates/settings.html +433 -172
- package/admin/js/views/collection-editor.js +487 -0
- package/admin/js/views/collection-entries.js +484 -0
- package/admin/js/views/collections.js +153 -0
- package/admin/js/views/dashboard.js +14 -6
- package/admin/js/views/index.js +9 -3
- package/admin/js/views/login.js +3 -2
- package/admin/js/views/navigation.js +77 -11
- package/admin/js/views/page-editor.js +207 -25
- package/admin/js/views/pages.js +14 -6
- package/admin/js/views/settings.js +137 -2
- package/admin/js/views/users.js +10 -7
- package/bin/cli.js +37 -10
- package/config/auth.json +2 -1
- package/config/content.json +1 -0
- package/config/navigation.json +14 -4
- package/config/plugins.json +0 -18
- package/config/presets.json +4 -8
- package/config/site.json +44 -3
- package/package.json +6 -2
- package/plugins/domma-effects/admin/templates/domma-effects.html +92 -3
- package/plugins/domma-effects/plugin.js +125 -0
- package/plugins/domma-effects/public/inject-body.html +19 -0
- package/plugins/example-analytics/admin/views/analytics.js +2 -2
- package/plugins/example-analytics/plugin.json +8 -0
- package/plugins/example-analytics/stats.json +15 -1
- package/plugins/form-builder/admin/templates/form-editor.html +19 -6
- package/plugins/form-builder/admin/views/form-editor.js +634 -9
- package/plugins/form-builder/admin/views/form-submissions.js +4 -4
- package/plugins/form-builder/admin/views/forms-list.js +5 -5
- package/plugins/form-builder/data/forms/consent.json +104 -0
- package/plugins/form-builder/data/forms/contacts.json +66 -0
- package/plugins/form-builder/data/submissions/consent.json +13 -0
- package/plugins/form-builder/data/submissions/contacts.json +26 -0
- package/plugins/form-builder/plugin.js +62 -11
- package/plugins/form-builder/plugin.json +12 -16
- package/plugins/form-builder/public/form-logic-engine.js +568 -0
- package/plugins/form-builder/public/inject-body.html +88 -6
- package/plugins/form-builder/public/inject-head.html +16 -0
- package/plugins/form-builder/public/package.json +1 -0
- package/public/css/site.css +113 -0
- package/public/js/btt.js +90 -0
- package/public/js/cookie-consent.js +61 -0
- package/public/js/site.js +129 -34
- package/scripts/build.js +129 -0
- package/scripts/seed.js +517 -7
- package/server/routes/api/collections.js +301 -0
- package/server/routes/api/settings.js +66 -2
- package/server/server.js +19 -15
- package/server/services/collections.js +430 -0
- package/server/services/content.js +11 -2
- package/server/services/hooks.js +109 -0
- package/server/services/markdown.js +500 -149
- package/server/services/plugins.js +6 -1
- package/server/services/renderer.js +73 -7
- package/server/templates/page.html +38 -3
- package/plugins/back-to-top/admin/templates/back-to-top-settings.html +0 -55
- package/plugins/back-to-top/admin/views/back-to-top-settings.js +0 -44
- package/plugins/back-to-top/config.js +0 -10
- package/plugins/back-to-top/plugin.js +0 -24
- package/plugins/back-to-top/plugin.json +0 -36
- package/plugins/back-to-top/public/inject-body.html +0 -105
- package/plugins/cookie-consent/admin/templates/cookie-consent-settings.html +0 -113
- package/plugins/cookie-consent/admin/views/cookie-consent-settings.js +0 -73
- package/plugins/cookie-consent/config.js +0 -30
- package/plugins/cookie-consent/plugin.js +0 -24
- package/plugins/cookie-consent/plugin.json +0 -36
- package/plugins/cookie-consent/public/inject-body.html +0 -69
- package/plugins/custom-css/admin/templates/custom-css.html +0 -17
- package/plugins/custom-css/admin/views/custom-css.js +0 -35
- package/plugins/custom-css/config.js +0 -1
- package/plugins/custom-css/data/custom.css +0 -0
- package/plugins/custom-css/plugin.js +0 -63
- package/plugins/custom-css/plugin.json +0 -32
- package/plugins/custom-css/public/inject-head.html +0 -1
- package/plugins/form-builder/data/forms/contact.json +0 -52
- package/plugins/form-builder/data/submissions/contact.json +0 -14
|
@@ -36,6 +36,15 @@ function getTypeLabel(type) {
|
|
|
36
36
|
return FIELD_TYPES.find(t => t.value === type)?.label || type;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
function hasLogic(logic) {
|
|
40
|
+
if (!logic) return false;
|
|
41
|
+
if (logic.visibility?.conditions?.length) return true;
|
|
42
|
+
if (logic.requirement?.conditions?.length) return true;
|
|
43
|
+
if (logic.validation?.length) return true;
|
|
44
|
+
if (logic.cascade?.sourceField) return true;
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
|
|
39
48
|
// ---------------------------------------------------------------------------
|
|
40
49
|
// Read field values from DOM back into array
|
|
41
50
|
// ---------------------------------------------------------------------------
|
|
@@ -43,6 +52,8 @@ function getTypeLabel(type) {
|
|
|
43
52
|
function collectFieldFromDOM(idx) {
|
|
44
53
|
const field = { ...fields[idx] };
|
|
45
54
|
|
|
55
|
+
if (field.type === 'spacer') return field;
|
|
56
|
+
|
|
46
57
|
// Page-break cards only have label + description inputs
|
|
47
58
|
if (field.type === 'page-break') {
|
|
48
59
|
const labelEl = document.getElementById(`fb-pb-label-${idx}`);
|
|
@@ -91,9 +102,119 @@ function collectFieldFromDOM(idx) {
|
|
|
91
102
|
const maxNum = document.getElementById(`fb-max-${idx}`)?.value;
|
|
92
103
|
if (maxNum !== '' && maxNum !== undefined) field.max = parseFloat(maxNum);
|
|
93
104
|
|
|
105
|
+
// Collect conditional logic from the logic builder UI
|
|
106
|
+
const collectedLogic = collectLogicFromCard(idx);
|
|
107
|
+
if (collectedLogic) {
|
|
108
|
+
field.logic = collectedLogic;
|
|
109
|
+
} else {
|
|
110
|
+
delete field.logic;
|
|
111
|
+
}
|
|
112
|
+
|
|
94
113
|
return field;
|
|
95
114
|
}
|
|
96
115
|
|
|
116
|
+
function collectLogicFromCard(idx) {
|
|
117
|
+
const card = document.querySelector(`.fb-field-card[data-index="${idx}"]`);
|
|
118
|
+
if (!card) return undefined;
|
|
119
|
+
const logicSection = card.querySelector('.fb-field-logic');
|
|
120
|
+
if (!logicSection) return undefined;
|
|
121
|
+
|
|
122
|
+
const logic = {};
|
|
123
|
+
let hasAnyLogic = false;
|
|
124
|
+
|
|
125
|
+
// Visibility
|
|
126
|
+
const visSection = logicSection.querySelector('[data-logic-section="visibility"]');
|
|
127
|
+
if (visSection) {
|
|
128
|
+
const defaultSel = visSection.querySelector('.fb-logic-vis-default');
|
|
129
|
+
const transSel = visSection.querySelector('.fb-logic-vis-transition');
|
|
130
|
+
const visRules = Array.from(visSection.querySelectorAll('.fb-logic-cond-row')).map(row => {
|
|
131
|
+
const fieldSel = row.querySelector('.fb-logic-cond-field');
|
|
132
|
+
const opSel = row.querySelector('.fb-logic-cond-op');
|
|
133
|
+
const valInp = row.querySelector('.fb-logic-cond-val');
|
|
134
|
+
const thenSel = row.querySelector('.fb-logic-vis-then');
|
|
135
|
+
if (!fieldSel?.value) return null;
|
|
136
|
+
return {
|
|
137
|
+
when: { all: [{ field: fieldSel.value, operator: opSel.value, value: valInp.value }] },
|
|
138
|
+
then: thenSel.value
|
|
139
|
+
};
|
|
140
|
+
}).filter(Boolean);
|
|
141
|
+
const visDefault = defaultSel?.value || 'visible';
|
|
142
|
+
const transition = transSel?.value || 'none';
|
|
143
|
+
if (visDefault !== 'visible' || visRules.length > 0 || transition !== 'none') {
|
|
144
|
+
logic.visibility = { default: visDefault, conditions: visRules };
|
|
145
|
+
if (transition !== 'none') logic.visibility.transition = transition;
|
|
146
|
+
hasAnyLogic = true;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Requirement
|
|
151
|
+
const reqSection = logicSection.querySelector('[data-logic-section="requirement"]');
|
|
152
|
+
if (reqSection) {
|
|
153
|
+
const defaultCb = reqSection.querySelector('.fb-logic-req-default');
|
|
154
|
+
const reqRules = Array.from(reqSection.querySelectorAll('.fb-logic-cond-row')).map(row => {
|
|
155
|
+
const fieldSel = row.querySelector('.fb-logic-cond-field');
|
|
156
|
+
const opSel = row.querySelector('.fb-logic-cond-op');
|
|
157
|
+
const valInp = row.querySelector('.fb-logic-cond-val');
|
|
158
|
+
const thenSel = row.querySelector('.fb-logic-req-then');
|
|
159
|
+
if (!fieldSel?.value) return null;
|
|
160
|
+
return {
|
|
161
|
+
when: { all: [{ field: fieldSel.value, operator: opSel.value, value: valInp.value }] },
|
|
162
|
+
then: thenSel.value === 'true'
|
|
163
|
+
};
|
|
164
|
+
}).filter(Boolean);
|
|
165
|
+
if (reqRules.length > 0) {
|
|
166
|
+
logic.requirement = { default: defaultCb?.checked === true, conditions: reqRules };
|
|
167
|
+
hasAnyLogic = true;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Validation
|
|
172
|
+
const valSection = logicSection.querySelector('[data-logic-section="validation"]');
|
|
173
|
+
if (valSection) {
|
|
174
|
+
const valRules = Array.from(valSection.querySelectorAll('.fb-logic-val-rule')).map(row => {
|
|
175
|
+
const typeSel = row.querySelector('.fb-logic-val-type');
|
|
176
|
+
const patternInp = row.querySelector('.fb-logic-val-pattern');
|
|
177
|
+
const flagsInp = row.querySelector('.fb-logic-val-flags');
|
|
178
|
+
const msgInp = row.querySelector('.fb-logic-val-message');
|
|
179
|
+
if (!patternInp?.value.trim()) return null;
|
|
180
|
+
const type = typeSel?.value || 'regex';
|
|
181
|
+
const rule = { type, message: msgInp?.value.trim() || 'Invalid value.' };
|
|
182
|
+
if (type === 'regex') {
|
|
183
|
+
rule.pattern = patternInp.value.trim();
|
|
184
|
+
if (flagsInp?.value.trim()) rule.flags = flagsInp.value.trim();
|
|
185
|
+
} else {
|
|
186
|
+
rule.field = patternInp.value.trim();
|
|
187
|
+
}
|
|
188
|
+
return rule;
|
|
189
|
+
}).filter(Boolean);
|
|
190
|
+
if (valRules.length > 0) {
|
|
191
|
+
logic.validation = valRules;
|
|
192
|
+
hasAnyLogic = true;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Cascade
|
|
197
|
+
const cascSection = logicSection.querySelector('[data-logic-section="cascade"]');
|
|
198
|
+
if (cascSection) {
|
|
199
|
+
const srcSel = cascSection.querySelector('.fb-logic-cascade-source');
|
|
200
|
+
const mapTa = cascSection.querySelector('.fb-logic-cascade-mapping');
|
|
201
|
+
const defTa = cascSection.querySelector('.fb-logic-cascade-defaults');
|
|
202
|
+
const sourceField = srcSel?.value?.trim();
|
|
203
|
+
if (sourceField) {
|
|
204
|
+
let mapping = {};
|
|
205
|
+
try { mapping = JSON.parse(mapTa?.value || '{}'); } catch { /* invalid json */ }
|
|
206
|
+
const defaultOptions = (defTa?.value || '').split('\n').filter(l => l.trim()).map(line => {
|
|
207
|
+
const [v, ...rest] = line.split(':');
|
|
208
|
+
return { value: v.trim(), label: rest.join(':').trim() || v.trim() };
|
|
209
|
+
});
|
|
210
|
+
logic.cascade = { sourceField, mapping, defaultOptions };
|
|
211
|
+
hasAnyLogic = true;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return hasAnyLogic ? logic : undefined;
|
|
216
|
+
}
|
|
217
|
+
|
|
97
218
|
function syncAllFieldsFromDOM() {
|
|
98
219
|
fields = fields.map((_, idx) => collectFieldFromDOM(idx));
|
|
99
220
|
}
|
|
@@ -119,6 +240,8 @@ function renderFieldList($container) {
|
|
|
119
240
|
fields.forEach((field, idx) => {
|
|
120
241
|
const card = field.type === 'page-break'
|
|
121
242
|
? buildPageBreakCard(field, idx, $container)
|
|
243
|
+
: field.type === 'spacer'
|
|
244
|
+
? buildSpacerCard(field, idx, $container)
|
|
122
245
|
: buildFieldCard(field, idx, $container);
|
|
123
246
|
listEl.appendChild(card);
|
|
124
247
|
});
|
|
@@ -231,6 +354,79 @@ function buildPageBreakCard(field, idx, $container) {
|
|
|
231
354
|
return card;
|
|
232
355
|
}
|
|
233
356
|
|
|
357
|
+
// ---------------------------------------------------------------------------
|
|
358
|
+
// Spacer card
|
|
359
|
+
// ---------------------------------------------------------------------------
|
|
360
|
+
|
|
361
|
+
function buildSpacerCard(field, idx, $container) {
|
|
362
|
+
const card = document.createElement('div');
|
|
363
|
+
card.className = 'fb-field-card';
|
|
364
|
+
card.dataset.index = idx;
|
|
365
|
+
card.style.cssText = 'border:1px dashed var(--border-color,#444);border-radius:6px;margin-bottom:.5rem;background:transparent;';
|
|
366
|
+
|
|
367
|
+
const header = document.createElement('div');
|
|
368
|
+
header.style.cssText = 'display:flex;align-items:center;gap:.6rem;padding:.4rem .8rem;';
|
|
369
|
+
|
|
370
|
+
const line = document.createElement('div');
|
|
371
|
+
line.style.cssText = 'flex:1;height:1px;background:var(--border-color,#444);';
|
|
372
|
+
|
|
373
|
+
const label = document.createElement('span');
|
|
374
|
+
label.textContent = 'Spacer';
|
|
375
|
+
label.style.cssText = 'font-size:.7rem;color:var(--text-muted,#888);white-space:nowrap;padding:0 .4rem;font-style:italic;';
|
|
376
|
+
|
|
377
|
+
const line2 = document.createElement('div');
|
|
378
|
+
line2.style.cssText = 'flex:1;height:1px;background:var(--border-color,#444);';
|
|
379
|
+
|
|
380
|
+
const controls = document.createElement('div');
|
|
381
|
+
controls.style.cssText = 'display:flex;gap:.25rem;flex-shrink:0;';
|
|
382
|
+
|
|
383
|
+
if (idx > 0) {
|
|
384
|
+
const upBtn = document.createElement('button');
|
|
385
|
+
upBtn.className = 'btn btn-xs btn-ghost';
|
|
386
|
+
upBtn.title = 'Move up';
|
|
387
|
+
upBtn.textContent = '↑';
|
|
388
|
+
upBtn.addEventListener('click', (e) => {
|
|
389
|
+
e.stopPropagation();
|
|
390
|
+
syncAllFieldsFromDOM();
|
|
391
|
+
[fields[idx - 1], fields[idx]] = [fields[idx], fields[idx - 1]];
|
|
392
|
+
renderFieldList($container);
|
|
393
|
+
});
|
|
394
|
+
controls.appendChild(upBtn);
|
|
395
|
+
}
|
|
396
|
+
if (idx < fields.length - 1) {
|
|
397
|
+
const downBtn = document.createElement('button');
|
|
398
|
+
downBtn.className = 'btn btn-xs btn-ghost';
|
|
399
|
+
downBtn.title = 'Move down';
|
|
400
|
+
downBtn.textContent = '↓';
|
|
401
|
+
downBtn.addEventListener('click', (e) => {
|
|
402
|
+
e.stopPropagation();
|
|
403
|
+
syncAllFieldsFromDOM();
|
|
404
|
+
[fields[idx], fields[idx + 1]] = [fields[idx + 1], fields[idx]];
|
|
405
|
+
renderFieldList($container);
|
|
406
|
+
});
|
|
407
|
+
controls.appendChild(downBtn);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const removeBtn = document.createElement('button');
|
|
411
|
+
removeBtn.className = 'btn btn-xs btn-danger';
|
|
412
|
+
removeBtn.title = 'Remove spacer';
|
|
413
|
+
removeBtn.textContent = '✕';
|
|
414
|
+
removeBtn.addEventListener('click', async (e) => {
|
|
415
|
+
e.stopPropagation();
|
|
416
|
+
syncAllFieldsFromDOM();
|
|
417
|
+
fields.splice(idx, 1);
|
|
418
|
+
renderFieldList($container);
|
|
419
|
+
});
|
|
420
|
+
controls.appendChild(removeBtn);
|
|
421
|
+
|
|
422
|
+
header.appendChild(line);
|
|
423
|
+
header.appendChild(label);
|
|
424
|
+
header.appendChild(line2);
|
|
425
|
+
header.appendChild(controls);
|
|
426
|
+
card.appendChild(header);
|
|
427
|
+
return card;
|
|
428
|
+
}
|
|
429
|
+
|
|
234
430
|
// ---------------------------------------------------------------------------
|
|
235
431
|
// Regular field card
|
|
236
432
|
// ---------------------------------------------------------------------------
|
|
@@ -315,6 +511,13 @@ function buildFieldCard(field, idx, $container) {
|
|
|
315
511
|
req.style.cssText = 'font-size:.7rem;color:var(--danger,#ef4444);flex-shrink:0;';
|
|
316
512
|
header.appendChild(req);
|
|
317
513
|
}
|
|
514
|
+
if (hasLogic(field.logic)) {
|
|
515
|
+
const logicBadge = document.createElement('span');
|
|
516
|
+
logicBadge.textContent = '⚡';
|
|
517
|
+
logicBadge.title = 'Has conditional logic';
|
|
518
|
+
logicBadge.style.cssText = 'font-size:.75rem;color:var(--primary,#6366f1);flex-shrink:0;';
|
|
519
|
+
header.appendChild(logicBadge);
|
|
520
|
+
}
|
|
318
521
|
header.appendChild(controls);
|
|
319
522
|
|
|
320
523
|
// Click header to toggle body
|
|
@@ -390,9 +593,408 @@ function buildFieldBody(field, idx, labelSpan) {
|
|
|
390
593
|
const extras = buildFieldExtras(field.type, field, idx);
|
|
391
594
|
if (extras) body.appendChild(extras);
|
|
392
595
|
|
|
596
|
+
// Conditional logic builder
|
|
597
|
+
body.appendChild(buildFieldLogic(field, idx));
|
|
598
|
+
|
|
393
599
|
return body;
|
|
394
600
|
}
|
|
395
601
|
|
|
602
|
+
// ---------------------------------------------------------------------------
|
|
603
|
+
// Conditional Logic Builder UI
|
|
604
|
+
// ---------------------------------------------------------------------------
|
|
605
|
+
|
|
606
|
+
const CONDITION_OPERATORS = [
|
|
607
|
+
{ value: 'equals', label: 'equals' },
|
|
608
|
+
{ value: 'not_equals', label: 'does not equal' },
|
|
609
|
+
{ value: 'contains', label: 'contains' },
|
|
610
|
+
{ value: 'not_contains', label: 'does not contain' },
|
|
611
|
+
{ value: 'starts_with', label: 'starts with' },
|
|
612
|
+
{ value: 'ends_with', label: 'ends with' },
|
|
613
|
+
{ value: 'greater_than', label: 'is greater than' },
|
|
614
|
+
{ value: 'less_than', label: 'is less than' },
|
|
615
|
+
{ value: 'is_empty', label: 'is empty' },
|
|
616
|
+
{ value: 'is_not_empty', label: 'is not empty' },
|
|
617
|
+
{ value: 'in', label: 'is one of (comma sep)' },
|
|
618
|
+
{ value: 'not_in', label: 'is not one of (comma sep)' },
|
|
619
|
+
{ value: 'matches_regex', label: 'matches regex' },
|
|
620
|
+
];
|
|
621
|
+
|
|
622
|
+
const NO_VALUE_OPS = new Set(['is_empty', 'is_not_empty']);
|
|
623
|
+
|
|
624
|
+
function buildLogicSectionHeader(text) {
|
|
625
|
+
const p = document.createElement('p');
|
|
626
|
+
p.textContent = text;
|
|
627
|
+
p.style.cssText = 'font-size:.75rem;font-weight:700;color:var(--text-muted,#888);margin:.6rem 0 .3rem;text-transform:uppercase;letter-spacing:.04em;';
|
|
628
|
+
return p;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
function buildConditionRow(condition, fieldOptions, thenOptions, thenClass, thenSelected) {
|
|
632
|
+
const row = document.createElement('div');
|
|
633
|
+
row.className = 'fb-logic-cond-row';
|
|
634
|
+
row.style.cssText = 'display:flex;gap:.35rem;align-items:center;margin-bottom:.35rem;flex-wrap:wrap;';
|
|
635
|
+
|
|
636
|
+
const whenLabel = document.createElement('span');
|
|
637
|
+
whenLabel.textContent = 'When';
|
|
638
|
+
whenLabel.style.cssText = 'font-size:.73rem;color:var(--text-muted,#888);flex-shrink:0;';
|
|
639
|
+
|
|
640
|
+
const fieldSel = document.createElement('select');
|
|
641
|
+
fieldSel.className = 'form-input fb-logic-cond-field';
|
|
642
|
+
fieldSel.style.cssText = 'flex:2;min-width:80px;font-size:.78rem;padding:.2rem .35rem;';
|
|
643
|
+
fieldOptions.forEach(opt => {
|
|
644
|
+
const o = document.createElement('option');
|
|
645
|
+
o.value = opt.value;
|
|
646
|
+
o.textContent = opt.label;
|
|
647
|
+
if (condition && opt.value === condition.field) o.selected = true;
|
|
648
|
+
fieldSel.appendChild(o);
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
const opSel = document.createElement('select');
|
|
652
|
+
opSel.className = 'form-input fb-logic-cond-op';
|
|
653
|
+
opSel.style.cssText = 'flex:2;min-width:80px;font-size:.78rem;padding:.2rem .35rem;';
|
|
654
|
+
CONDITION_OPERATORS.forEach(op => {
|
|
655
|
+
const o = document.createElement('option');
|
|
656
|
+
o.value = op.value;
|
|
657
|
+
o.textContent = op.label;
|
|
658
|
+
if (condition && op.value === condition.operator) o.selected = true;
|
|
659
|
+
opSel.appendChild(o);
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
const valInp = document.createElement('input');
|
|
663
|
+
valInp.type = 'text';
|
|
664
|
+
valInp.className = 'form-input fb-logic-cond-val';
|
|
665
|
+
valInp.placeholder = 'value';
|
|
666
|
+
valInp.style.cssText = 'flex:2;min-width:60px;font-size:.78rem;padding:.2rem .35rem;';
|
|
667
|
+
valInp.value = condition?.value || '';
|
|
668
|
+
if (condition && NO_VALUE_OPS.has(condition.operator)) valInp.style.display = 'none';
|
|
669
|
+
opSel.addEventListener('change', () => {
|
|
670
|
+
valInp.style.display = NO_VALUE_OPS.has(opSel.value) ? 'none' : '';
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
const arrow = document.createElement('span');
|
|
674
|
+
arrow.textContent = '→';
|
|
675
|
+
arrow.style.cssText = 'font-size:.73rem;color:var(--text-muted,#888);flex-shrink:0;';
|
|
676
|
+
|
|
677
|
+
const thenSel = document.createElement('select');
|
|
678
|
+
thenSel.className = `form-input ${thenClass}`;
|
|
679
|
+
thenSel.style.cssText = 'flex:2;min-width:80px;font-size:.78rem;padding:.2rem .35rem;';
|
|
680
|
+
thenOptions.forEach(opt => {
|
|
681
|
+
const o = document.createElement('option');
|
|
682
|
+
o.value = opt.value;
|
|
683
|
+
o.textContent = opt.label;
|
|
684
|
+
if (opt.value === thenSelected) o.selected = true;
|
|
685
|
+
thenSel.appendChild(o);
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
const removeBtn = document.createElement('button');
|
|
689
|
+
removeBtn.type = 'button';
|
|
690
|
+
removeBtn.className = 'btn btn-xs btn-danger';
|
|
691
|
+
removeBtn.textContent = '✕';
|
|
692
|
+
removeBtn.style.flexShrink = '0';
|
|
693
|
+
removeBtn.addEventListener('click', () => row.remove());
|
|
694
|
+
|
|
695
|
+
row.appendChild(whenLabel);
|
|
696
|
+
row.appendChild(fieldSel);
|
|
697
|
+
row.appendChild(opSel);
|
|
698
|
+
row.appendChild(valInp);
|
|
699
|
+
row.appendChild(arrow);
|
|
700
|
+
row.appendChild(thenSel);
|
|
701
|
+
row.appendChild(removeBtn);
|
|
702
|
+
return row;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
function buildVisibilitySection(vis, idx, otherFields) {
|
|
706
|
+
const section = document.createElement('div');
|
|
707
|
+
section.dataset.logicSection = 'visibility';
|
|
708
|
+
section.appendChild(buildLogicSectionHeader('Visibility'));
|
|
709
|
+
|
|
710
|
+
const defaultRow = document.createElement('div');
|
|
711
|
+
defaultRow.style.cssText = 'display:flex;align-items:center;gap:.5rem;margin-bottom:.4rem;';
|
|
712
|
+
const defaultLabel = document.createElement('span');
|
|
713
|
+
defaultLabel.textContent = 'Default:';
|
|
714
|
+
defaultLabel.style.cssText = 'font-size:.8rem;flex-shrink:0;';
|
|
715
|
+
const defaultSel = document.createElement('select');
|
|
716
|
+
defaultSel.className = 'form-input fb-logic-vis-default';
|
|
717
|
+
defaultSel.style.cssText = 'font-size:.8rem;padding:.25rem .4rem;';
|
|
718
|
+
[{ value: 'visible', label: 'Visible' }, { value: 'hidden', label: 'Hidden' }].forEach(opt => {
|
|
719
|
+
const o = document.createElement('option');
|
|
720
|
+
o.value = opt.value;
|
|
721
|
+
o.textContent = opt.label;
|
|
722
|
+
if (opt.value === (vis.default || 'visible')) o.selected = true;
|
|
723
|
+
defaultSel.appendChild(o);
|
|
724
|
+
});
|
|
725
|
+
defaultRow.appendChild(defaultLabel);
|
|
726
|
+
defaultRow.appendChild(defaultSel);
|
|
727
|
+
section.appendChild(defaultRow);
|
|
728
|
+
|
|
729
|
+
// Transition type dropdown
|
|
730
|
+
const transRow = document.createElement('div');
|
|
731
|
+
transRow.style.cssText = 'display:flex;align-items:center;gap:.5rem;margin-bottom:.4rem;';
|
|
732
|
+
const transLabel = document.createElement('span');
|
|
733
|
+
transLabel.textContent = 'Transition:';
|
|
734
|
+
transLabel.style.cssText = 'font-size:.8rem;flex-shrink:0;';
|
|
735
|
+
const transSel = document.createElement('select');
|
|
736
|
+
transSel.className = 'form-input fb-logic-vis-transition';
|
|
737
|
+
transSel.style.cssText = 'font-size:.8rem;padding:.25rem .4rem;';
|
|
738
|
+
[
|
|
739
|
+
{value: 'none', label: 'None (instant)'},
|
|
740
|
+
{value: 'fade', label: 'Fade'},
|
|
741
|
+
{value: 'slide', label: 'Slide'},
|
|
742
|
+
{value: 'scale', label: 'Scale'},
|
|
743
|
+
].forEach(opt => {
|
|
744
|
+
const o = document.createElement('option');
|
|
745
|
+
o.value = opt.value;
|
|
746
|
+
o.textContent = opt.label;
|
|
747
|
+
if (opt.value === (vis.transition || 'none')) o.selected = true;
|
|
748
|
+
transSel.appendChild(o);
|
|
749
|
+
});
|
|
750
|
+
transRow.appendChild(transLabel);
|
|
751
|
+
transRow.appendChild(transSel);
|
|
752
|
+
section.appendChild(transRow);
|
|
753
|
+
|
|
754
|
+
const rulesContainer = document.createElement('div');
|
|
755
|
+
rulesContainer.className = 'fb-logic-vis-rules';
|
|
756
|
+
const fieldOpts = otherFields.map(f => ({ value: f.name, label: f.label || f.name }));
|
|
757
|
+
const thenOpts = [{ value: 'visible', label: 'Show' }, { value: 'hidden', label: 'Hide' }];
|
|
758
|
+
|
|
759
|
+
(vis.conditions || []).forEach(rule => {
|
|
760
|
+
const cond = (rule.when?.all || rule.when?.any || [])[0];
|
|
761
|
+
const thenVal = rule.then === 'hidden' ? 'hidden' : 'visible';
|
|
762
|
+
if (fieldOpts.length > 0) rulesContainer.appendChild(buildConditionRow(cond, fieldOpts, thenOpts, 'fb-logic-vis-then', thenVal));
|
|
763
|
+
});
|
|
764
|
+
section.appendChild(rulesContainer);
|
|
765
|
+
|
|
766
|
+
if (fieldOpts.length > 0) {
|
|
767
|
+
const addBtn = document.createElement('button');
|
|
768
|
+
addBtn.type = 'button';
|
|
769
|
+
addBtn.className = 'btn btn-xs btn-ghost';
|
|
770
|
+
addBtn.style.cssText = 'font-size:.73rem;margin-top:.2rem;';
|
|
771
|
+
addBtn.textContent = '+ Add visibility rule';
|
|
772
|
+
addBtn.addEventListener('click', () => rulesContainer.appendChild(buildConditionRow(null, fieldOpts, thenOpts, 'fb-logic-vis-then', 'visible')));
|
|
773
|
+
section.appendChild(addBtn);
|
|
774
|
+
}
|
|
775
|
+
return section;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
function buildRequirementSection(req, idx, otherFields) {
|
|
779
|
+
const section = document.createElement('div');
|
|
780
|
+
section.dataset.logicSection = 'requirement';
|
|
781
|
+
section.appendChild(buildLogicSectionHeader('Conditional Requirement'));
|
|
782
|
+
|
|
783
|
+
const cbLabel = document.createElement('label');
|
|
784
|
+
cbLabel.style.cssText = 'display:flex;align-items:center;gap:.4rem;font-size:.8rem;cursor:pointer;margin-bottom:.4rem;';
|
|
785
|
+
const cb = document.createElement('input');
|
|
786
|
+
cb.type = 'checkbox';
|
|
787
|
+
cb.className = 'fb-logic-req-default';
|
|
788
|
+
cb.checked = req.default === true;
|
|
789
|
+
cbLabel.appendChild(cb);
|
|
790
|
+
cbLabel.appendChild(document.createTextNode('Required by default'));
|
|
791
|
+
section.appendChild(cbLabel);
|
|
792
|
+
|
|
793
|
+
const rulesContainer = document.createElement('div');
|
|
794
|
+
rulesContainer.className = 'fb-logic-req-rules';
|
|
795
|
+
const fieldOpts = otherFields.map(f => ({ value: f.name, label: f.label || f.name }));
|
|
796
|
+
const thenOpts = [{ value: 'true', label: 'Make required' }, { value: 'false', label: 'Make optional' }];
|
|
797
|
+
|
|
798
|
+
(req.conditions || []).forEach(rule => {
|
|
799
|
+
const cond = (rule.when?.all || rule.when?.any || [])[0];
|
|
800
|
+
const thenVal = rule.then === true ? 'true' : 'false';
|
|
801
|
+
if (fieldOpts.length > 0) rulesContainer.appendChild(buildConditionRow(cond, fieldOpts, thenOpts, 'fb-logic-req-then', thenVal));
|
|
802
|
+
});
|
|
803
|
+
section.appendChild(rulesContainer);
|
|
804
|
+
|
|
805
|
+
if (fieldOpts.length > 0) {
|
|
806
|
+
const addBtn = document.createElement('button');
|
|
807
|
+
addBtn.type = 'button';
|
|
808
|
+
addBtn.className = 'btn btn-xs btn-ghost';
|
|
809
|
+
addBtn.style.cssText = 'font-size:.73rem;margin-top:.2rem;';
|
|
810
|
+
addBtn.textContent = '+ Add requirement rule';
|
|
811
|
+
addBtn.addEventListener('click', () => rulesContainer.appendChild(buildConditionRow(null, fieldOpts, thenOpts, 'fb-logic-req-then', 'true')));
|
|
812
|
+
section.appendChild(addBtn);
|
|
813
|
+
}
|
|
814
|
+
return section;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
function buildValidationRuleRow(rule) {
|
|
818
|
+
const row = document.createElement('div');
|
|
819
|
+
row.className = 'fb-logic-val-rule';
|
|
820
|
+
row.style.cssText = 'display:flex;gap:.35rem;align-items:center;margin-bottom:.35rem;flex-wrap:wrap;';
|
|
821
|
+
|
|
822
|
+
const typeSel = document.createElement('select');
|
|
823
|
+
typeSel.className = 'form-input fb-logic-val-type';
|
|
824
|
+
typeSel.style.cssText = 'flex:0 0 auto;font-size:.78rem;padding:.2rem .35rem;';
|
|
825
|
+
[{ value: 'regex', label: 'Regex' }, { value: 'match', label: 'Match field' }].forEach(opt => {
|
|
826
|
+
const o = document.createElement('option');
|
|
827
|
+
o.value = opt.value;
|
|
828
|
+
o.textContent = opt.label;
|
|
829
|
+
if (opt.value === (rule?.type || 'regex')) o.selected = true;
|
|
830
|
+
typeSel.appendChild(o);
|
|
831
|
+
});
|
|
832
|
+
|
|
833
|
+
const patternInp = document.createElement('input');
|
|
834
|
+
patternInp.type = 'text';
|
|
835
|
+
patternInp.className = 'form-input fb-logic-val-pattern';
|
|
836
|
+
patternInp.placeholder = rule?.type === 'match' ? 'field name' : 'pattern';
|
|
837
|
+
patternInp.value = rule?.pattern || rule?.field || '';
|
|
838
|
+
patternInp.style.cssText = 'flex:3;font-size:.78rem;padding:.2rem .35rem;';
|
|
839
|
+
|
|
840
|
+
const flagsInp = document.createElement('input');
|
|
841
|
+
flagsInp.type = 'text';
|
|
842
|
+
flagsInp.className = 'form-input fb-logic-val-flags';
|
|
843
|
+
flagsInp.placeholder = 'flags';
|
|
844
|
+
flagsInp.value = rule?.flags || '';
|
|
845
|
+
flagsInp.style.cssText = 'flex:0 0 55px;font-size:.78rem;padding:.2rem .35rem;';
|
|
846
|
+
if (rule?.type === 'match') flagsInp.style.display = 'none';
|
|
847
|
+
|
|
848
|
+
typeSel.addEventListener('change', () => {
|
|
849
|
+
flagsInp.style.display = typeSel.value === 'match' ? 'none' : '';
|
|
850
|
+
patternInp.placeholder = typeSel.value === 'match' ? 'field name' : 'pattern';
|
|
851
|
+
});
|
|
852
|
+
|
|
853
|
+
const msgInp = document.createElement('input');
|
|
854
|
+
msgInp.type = 'text';
|
|
855
|
+
msgInp.className = 'form-input fb-logic-val-message';
|
|
856
|
+
msgInp.placeholder = 'Error message';
|
|
857
|
+
msgInp.value = rule?.message || '';
|
|
858
|
+
msgInp.style.cssText = 'flex:4;font-size:.78rem;padding:.2rem .35rem;';
|
|
859
|
+
|
|
860
|
+
const removeBtn = document.createElement('button');
|
|
861
|
+
removeBtn.type = 'button';
|
|
862
|
+
removeBtn.className = 'btn btn-xs btn-danger';
|
|
863
|
+
removeBtn.textContent = '✕';
|
|
864
|
+
removeBtn.style.flexShrink = '0';
|
|
865
|
+
removeBtn.addEventListener('click', () => row.remove());
|
|
866
|
+
|
|
867
|
+
row.appendChild(typeSel);
|
|
868
|
+
row.appendChild(patternInp);
|
|
869
|
+
row.appendChild(flagsInp);
|
|
870
|
+
row.appendChild(msgInp);
|
|
871
|
+
row.appendChild(removeBtn);
|
|
872
|
+
return row;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
function buildValidationSection(validationRules, idx, otherFields) {
|
|
876
|
+
const section = document.createElement('div');
|
|
877
|
+
section.dataset.logicSection = 'validation';
|
|
878
|
+
section.appendChild(buildLogicSectionHeader('Custom Validation'));
|
|
879
|
+
|
|
880
|
+
const rulesContainer = document.createElement('div');
|
|
881
|
+
rulesContainer.className = 'fb-logic-val-rules';
|
|
882
|
+
(validationRules || []).forEach(rule => rulesContainer.appendChild(buildValidationRuleRow(rule)));
|
|
883
|
+
section.appendChild(rulesContainer);
|
|
884
|
+
|
|
885
|
+
const addBtn = document.createElement('button');
|
|
886
|
+
addBtn.type = 'button';
|
|
887
|
+
addBtn.className = 'btn btn-xs btn-ghost';
|
|
888
|
+
addBtn.style.cssText = 'font-size:.73rem;margin-top:.2rem;';
|
|
889
|
+
addBtn.textContent = '+ Add validation rule';
|
|
890
|
+
addBtn.addEventListener('click', () => rulesContainer.appendChild(buildValidationRuleRow(null)));
|
|
891
|
+
section.appendChild(addBtn);
|
|
892
|
+
return section;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
function buildCascadeSection(cascade, idx, otherFields) {
|
|
896
|
+
const section = document.createElement('div');
|
|
897
|
+
section.dataset.logicSection = 'cascade';
|
|
898
|
+
section.appendChild(buildLogicSectionHeader('Cascade Options'));
|
|
899
|
+
|
|
900
|
+
const srcRow = document.createElement('div');
|
|
901
|
+
srcRow.style.cssText = 'display:flex;align-items:center;gap:.5rem;margin-bottom:.5rem;';
|
|
902
|
+
const srcLabel = document.createElement('span');
|
|
903
|
+
srcLabel.textContent = 'Source field:';
|
|
904
|
+
srcLabel.style.cssText = 'font-size:.8rem;flex-shrink:0;';
|
|
905
|
+
const srcSel = document.createElement('select');
|
|
906
|
+
srcSel.className = 'form-input fb-logic-cascade-source';
|
|
907
|
+
srcSel.style.cssText = 'flex:1;font-size:.8rem;padding:.25rem .4rem;';
|
|
908
|
+
const emptyOpt = document.createElement('option');
|
|
909
|
+
emptyOpt.value = '';
|
|
910
|
+
emptyOpt.textContent = '— none —';
|
|
911
|
+
srcSel.appendChild(emptyOpt);
|
|
912
|
+
otherFields.forEach(f => {
|
|
913
|
+
const o = document.createElement('option');
|
|
914
|
+
o.value = f.name;
|
|
915
|
+
o.textContent = f.label || f.name;
|
|
916
|
+
if (f.name === cascade.sourceField) o.selected = true;
|
|
917
|
+
srcSel.appendChild(o);
|
|
918
|
+
});
|
|
919
|
+
srcRow.appendChild(srcLabel);
|
|
920
|
+
srcRow.appendChild(srcSel);
|
|
921
|
+
section.appendChild(srcRow);
|
|
922
|
+
|
|
923
|
+
const mapLabel = document.createElement('p');
|
|
924
|
+
mapLabel.textContent = 'Mapping JSON — {"value":[{"value":"...","label":"..."}]}';
|
|
925
|
+
mapLabel.style.cssText = 'font-size:.73rem;color:var(--text-muted,#888);margin:.3rem 0 .2rem;';
|
|
926
|
+
const mapTa = document.createElement('textarea');
|
|
927
|
+
mapTa.className = 'form-input fb-logic-cascade-mapping';
|
|
928
|
+
mapTa.rows = 4;
|
|
929
|
+
mapTa.style.cssText = 'font-family:monospace;font-size:.78rem;';
|
|
930
|
+
mapTa.placeholder = '{"uk": [{"value": "london", "label": "London"}]}';
|
|
931
|
+
mapTa.value = cascade.mapping ? JSON.stringify(cascade.mapping, null, 2) : '';
|
|
932
|
+
section.appendChild(mapLabel);
|
|
933
|
+
section.appendChild(mapTa);
|
|
934
|
+
|
|
935
|
+
const defLabel = document.createElement('p');
|
|
936
|
+
defLabel.textContent = 'Default options (one per line: value:Label)';
|
|
937
|
+
defLabel.style.cssText = 'font-size:.73rem;color:var(--text-muted,#888);margin:.3rem 0 .2rem;';
|
|
938
|
+
const defTa = document.createElement('textarea');
|
|
939
|
+
defTa.className = 'form-input fb-logic-cascade-defaults';
|
|
940
|
+
defTa.rows = 3;
|
|
941
|
+
defTa.style.cssText = 'font-family:monospace;font-size:.78rem;';
|
|
942
|
+
defTa.placeholder = 'option1:Option 1\noption2:Option 2';
|
|
943
|
+
defTa.value = (cascade.defaultOptions || []).map(o => o.value === o.label ? o.value : `${o.value}:${o.label}`).join('\n');
|
|
944
|
+
section.appendChild(defLabel);
|
|
945
|
+
section.appendChild(defTa);
|
|
946
|
+
return section;
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
function buildFieldLogic(field, idx) {
|
|
950
|
+
const logic = field.logic || {};
|
|
951
|
+
const otherFields = fields.filter((f, i) => i !== idx && f.type !== 'page-break');
|
|
952
|
+
|
|
953
|
+
const section = document.createElement('div');
|
|
954
|
+
section.className = 'fb-field-logic';
|
|
955
|
+
section.style.cssText = 'margin-top:.75rem;border-top:1px solid var(--border-color,#333);padding-top:.5rem;';
|
|
956
|
+
|
|
957
|
+
const toggleHeader = document.createElement('div');
|
|
958
|
+
toggleHeader.style.cssText = 'display:flex;align-items:center;justify-content:space-between;cursor:pointer;padding:.15rem 0;';
|
|
959
|
+
const toggleLabel = document.createElement('span');
|
|
960
|
+
toggleLabel.style.cssText = 'font-size:.8rem;font-weight:600;color:var(--text-muted,#888);';
|
|
961
|
+
toggleLabel.textContent = '⚡ Conditional Logic';
|
|
962
|
+
const isOpen = hasLogic(logic);
|
|
963
|
+
const toggleBtn = document.createElement('button');
|
|
964
|
+
toggleBtn.type = 'button';
|
|
965
|
+
toggleBtn.className = 'btn btn-xs btn-ghost';
|
|
966
|
+
toggleBtn.textContent = isOpen ? '▾' : '▸';
|
|
967
|
+
|
|
968
|
+
const logicBody = document.createElement('div');
|
|
969
|
+
logicBody.className = 'fb-logic-body';
|
|
970
|
+
logicBody.style.cssText = 'padding:.25rem 0 .25rem;' + (isOpen ? '' : 'display:none;');
|
|
971
|
+
|
|
972
|
+
toggleHeader.addEventListener('click', () => {
|
|
973
|
+
const collapsed = logicBody.style.display === 'none';
|
|
974
|
+
logicBody.style.display = collapsed ? '' : 'none';
|
|
975
|
+
toggleBtn.textContent = collapsed ? '▾' : '▸';
|
|
976
|
+
});
|
|
977
|
+
toggleHeader.appendChild(toggleLabel);
|
|
978
|
+
toggleHeader.appendChild(toggleBtn);
|
|
979
|
+
section.appendChild(toggleHeader);
|
|
980
|
+
|
|
981
|
+
logicBody.appendChild(buildVisibilitySection(logic.visibility || {}, idx, otherFields));
|
|
982
|
+
logicBody.appendChild(buildRequirementSection(logic.requirement || {}, idx, otherFields));
|
|
983
|
+
logicBody.appendChild(buildValidationSection(logic.validation || [], idx, otherFields));
|
|
984
|
+
|
|
985
|
+
const currentType = document.getElementById(`fb-type-${idx}`)?.value || field.type;
|
|
986
|
+
if (OPTION_TYPES.has(currentType)) {
|
|
987
|
+
logicBody.appendChild(buildCascadeSection(logic.cascade || {}, idx, otherFields));
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
section.appendChild(logicBody);
|
|
991
|
+
return section;
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
// ---------------------------------------------------------------------------
|
|
995
|
+
// Field type-specific extras
|
|
996
|
+
// ---------------------------------------------------------------------------
|
|
997
|
+
|
|
396
998
|
function buildFieldExtras(type, field, idx) {
|
|
397
999
|
const wrap = document.createElement('div');
|
|
398
1000
|
wrap.className = 'fb-field-extras';
|
|
@@ -575,7 +1177,7 @@ function buildWizardSteps(formFields) {
|
|
|
575
1177
|
currentGroup = [];
|
|
576
1178
|
stepTitle = f.label || `Step ${steps.length + 1}`;
|
|
577
1179
|
stepDesc = f.description || '';
|
|
578
|
-
} else {
|
|
1180
|
+
} else if (f.type !== 'spacer') {
|
|
579
1181
|
currentGroup.push(f);
|
|
580
1182
|
}
|
|
581
1183
|
});
|
|
@@ -588,7 +1190,7 @@ function buildWizardSteps(formFields) {
|
|
|
588
1190
|
function buildBlueprintFromFields(fieldList) {
|
|
589
1191
|
const blueprint = {};
|
|
590
1192
|
fieldList.forEach(field => {
|
|
591
|
-
if (field.type === 'page-break') return;
|
|
1193
|
+
if (field.type === 'page-break' || field.type === 'spacer') return;
|
|
592
1194
|
blueprint[field.name] = {
|
|
593
1195
|
type: field.type,
|
|
594
1196
|
label: field.label,
|
|
@@ -662,8 +1264,19 @@ export const formEditorView = {
|
|
|
662
1264
|
|
|
663
1265
|
renderFieldList($container);
|
|
664
1266
|
|
|
1267
|
+
// --- Add dropdown toggle ---
|
|
1268
|
+
const addMenuEl = $container.find('#add-element-menu').get(0);
|
|
1269
|
+
$container.find('#add-element-btn').get(0).addEventListener('click', (e) => {
|
|
1270
|
+
e.stopPropagation();
|
|
1271
|
+
addMenuEl.style.display = addMenuEl.style.display === 'none' ? '' : 'none';
|
|
1272
|
+
});
|
|
1273
|
+
document.addEventListener('click', () => {
|
|
1274
|
+
if (addMenuEl) addMenuEl.style.display = 'none';
|
|
1275
|
+
});
|
|
1276
|
+
|
|
665
1277
|
// --- Add field ---
|
|
666
1278
|
$container.find('#add-field-btn').get(0).addEventListener('click', () => {
|
|
1279
|
+
if (addMenuEl) addMenuEl.style.display = 'none';
|
|
667
1280
|
syncAllFieldsFromDOM();
|
|
668
1281
|
const newIdx = fields.length;
|
|
669
1282
|
fields.push({
|
|
@@ -683,8 +1296,17 @@ export const formEditorView = {
|
|
|
683
1296
|
}
|
|
684
1297
|
});
|
|
685
1298
|
|
|
1299
|
+
// --- Add spacer ---
|
|
1300
|
+
$container.find('#add-spacer-btn').get(0).addEventListener('click', () => {
|
|
1301
|
+
if (addMenuEl) addMenuEl.style.display = 'none';
|
|
1302
|
+
syncAllFieldsFromDOM();
|
|
1303
|
+
fields.push({type: 'spacer'});
|
|
1304
|
+
renderFieldList($container);
|
|
1305
|
+
});
|
|
1306
|
+
|
|
686
1307
|
// --- Add page break ---
|
|
687
1308
|
$container.find('#add-page-break-btn').get(0).addEventListener('click', () => {
|
|
1309
|
+
if (addMenuEl) addMenuEl.style.display = 'none';
|
|
688
1310
|
syncAllFieldsFromDOM();
|
|
689
1311
|
const stepNum = fields.filter(f => f.type === 'page-break').length + 2;
|
|
690
1312
|
fields.push({
|
|
@@ -693,13 +1315,6 @@ export const formEditorView = {
|
|
|
693
1315
|
description: ''
|
|
694
1316
|
});
|
|
695
1317
|
renderFieldList($container);
|
|
696
|
-
// Auto-expand the newly added page break card
|
|
697
|
-
const listEl = $container.find('#fields-list').get(0);
|
|
698
|
-
const newCard = listEl?.lastElementChild;
|
|
699
|
-
if (newCard) {
|
|
700
|
-
const body = newCard.querySelector('.fb-field-body');
|
|
701
|
-
if (body) body.style.display = '';
|
|
702
|
-
}
|
|
703
1318
|
});
|
|
704
1319
|
|
|
705
1320
|
// --- Save ---
|
|
@@ -804,6 +1419,16 @@ export const formEditorView = {
|
|
|
804
1419
|
onSubmit
|
|
805
1420
|
});
|
|
806
1421
|
}
|
|
1422
|
+
// Init conditional logic engine on preview form
|
|
1423
|
+
if (window.FormLogicEngine && data.fields.some(f => f.logic)) {
|
|
1424
|
+
requestAnimationFrame(() => {
|
|
1425
|
+
const runtime = new window.FormLogicEngine.FormLogicRuntime(
|
|
1426
|
+
{ fields: data.fields },
|
|
1427
|
+
formEl
|
|
1428
|
+
);
|
|
1429
|
+
runtime.init();
|
|
1430
|
+
});
|
|
1431
|
+
}
|
|
807
1432
|
} else {
|
|
808
1433
|
const msg = document.createElement('p');
|
|
809
1434
|
msg.textContent = `${data.fields.filter(f => f.type !== 'page-break').length} field(s): ${data.fields.filter(f => f.type !== 'page-break').map(f => f.label).join(', ')}`;
|