domma-cms 0.2.1 → 0.5.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 (166) hide show
  1. package/README.md +3 -3
  2. package/admin/css/admin.css +1 -1200
  3. package/admin/dist/domma/domma-tools.css +2313 -0
  4. package/admin/dist/domma/domma-tools.min.js +10 -0
  5. package/admin/index.html +4 -0
  6. package/admin/js/api.js +1 -242
  7. package/admin/js/app.js +9 -279
  8. package/admin/js/config/sidebar-config.js +1 -115
  9. package/admin/js/lib/card.js +1 -63
  10. package/admin/js/lib/image-editor.js +1 -869
  11. package/admin/js/lib/markdown-toolbar.js +54 -421
  12. package/admin/js/templates/action-editor.html +171 -0
  13. package/admin/js/templates/actions-list.html +19 -0
  14. package/admin/js/templates/api-reference.html +1411 -0
  15. package/admin/js/templates/block-editor.html +158 -0
  16. package/admin/js/templates/blocks.html +8 -0
  17. package/admin/js/templates/collection-editor.html +47 -0
  18. package/admin/js/templates/collection-entries.html +3 -0
  19. package/admin/js/templates/collections.html +51 -4
  20. package/admin/js/templates/documentation.html +258 -0
  21. package/admin/js/templates/form-editor.html +238 -0
  22. package/{plugins/form-builder/admin → admin/js}/templates/form-submissions.html +30 -30
  23. package/{plugins/form-builder/admin/templates/forms-list.html → admin/js/templates/forms.html} +17 -17
  24. package/admin/js/templates/layouts.html +44 -7
  25. package/admin/js/templates/login.html +29 -4
  26. package/admin/js/templates/my-profile.html +17 -0
  27. package/admin/js/templates/page-editor.html +48 -0
  28. package/admin/js/templates/pages.html +6 -1
  29. package/admin/js/templates/pro-docs.html +259 -0
  30. package/admin/js/templates/role-editor.html +59 -0
  31. package/admin/js/templates/roles.html +10 -0
  32. package/admin/js/templates/settings.html +137 -18
  33. package/admin/js/templates/tutorials.html +81 -0
  34. package/admin/js/templates/user-editor.html +7 -0
  35. package/admin/js/templates/users.html +3 -1
  36. package/admin/js/templates/view-editor.html +201 -0
  37. package/admin/js/templates/view-preview.html +51 -0
  38. package/admin/js/templates/views-list.html +19 -0
  39. package/admin/js/views/action-editor.js +1 -0
  40. package/admin/js/views/actions-list.js +1 -0
  41. package/admin/js/views/api-reference.js +1 -0
  42. package/admin/js/views/block-editor.js +8 -0
  43. package/admin/js/views/blocks.js +4 -0
  44. package/admin/js/views/collection-editor.js +3 -487
  45. package/admin/js/views/collection-entries.js +1 -484
  46. package/admin/js/views/collections.js +1 -153
  47. package/admin/js/views/dashboard.js +1 -56
  48. package/admin/js/views/documentation.js +1 -12
  49. package/admin/js/views/form-editor.js +8 -0
  50. package/admin/js/views/form-submissions.js +1 -0
  51. package/admin/js/views/forms.js +1 -0
  52. package/admin/js/views/index.js +1 -39
  53. package/admin/js/views/layouts.js +9 -42
  54. package/admin/js/views/login.js +7 -251
  55. package/admin/js/views/media.js +1 -240
  56. package/admin/js/views/my-profile.js +1 -0
  57. package/admin/js/views/navigation.js +14 -212
  58. package/admin/js/views/page-editor.js +72 -661
  59. package/admin/js/views/pages.js +5 -72
  60. package/admin/js/views/plugins.js +13 -90
  61. package/admin/js/views/pro-docs.js +1 -0
  62. package/admin/js/views/role-editor.js +1 -0
  63. package/admin/js/views/roles.js +4 -0
  64. package/admin/js/views/settings.js +3 -199
  65. package/admin/js/views/tutorials.js +1 -12
  66. package/admin/js/views/user-editor.js +1 -88
  67. package/admin/js/views/users.js +4 -76
  68. package/admin/js/views/view-editor.js +1 -0
  69. package/admin/js/views/view-preview.js +1 -0
  70. package/admin/js/views/views-list.js +1 -0
  71. package/bin/cli.js +1 -1
  72. package/config/auth.json +2 -17
  73. package/config/connections.json.bak +9 -0
  74. package/config/connections.json.example +9 -0
  75. package/config/navigation.json +15 -0
  76. package/config/plugins.json +19 -29
  77. package/config/server.json +6 -6
  78. package/config/site.json +17 -6
  79. package/package.json +24 -10
  80. package/plugins/domma-effects/public/celebrations/core/canvas.js +2 -104
  81. package/plugins/domma-effects/public/celebrations/core/particles.js +1 -144
  82. package/plugins/domma-effects/public/celebrations/core/physics.js +1 -166
  83. package/plugins/domma-effects/public/celebrations/index.js +1 -535
  84. package/plugins/domma-effects/public/celebrations/themes/christmas.js +1 -1805
  85. package/plugins/domma-effects/public/celebrations/themes/guy-fawkes.js +1 -1477
  86. package/plugins/domma-effects/public/celebrations/themes/halloween.js +1 -1837
  87. package/plugins/domma-effects/public/celebrations/themes/st-andrews.js +1 -1175
  88. package/plugins/domma-effects/public/celebrations/themes/st-davids.js +1 -1258
  89. package/plugins/domma-effects/public/celebrations/themes/st-georges.js +1 -1754
  90. package/plugins/domma-effects/public/celebrations/themes/st-patricks.js +1 -1290
  91. package/plugins/domma-effects/public/celebrations/themes/valentines.js +1 -1361
  92. package/plugins/example-analytics/stats.json +21 -12
  93. package/plugins/theme-roller/admin/templates/theme-roller.html +71 -0
  94. package/plugins/theme-roller/admin/views/theme-roller-view.js +403 -0
  95. package/plugins/theme-roller/config.js +1 -0
  96. package/plugins/theme-roller/plugin.js +233 -0
  97. package/plugins/theme-roller/plugin.json +31 -0
  98. package/plugins/theme-roller/public/active-theme.css +0 -0
  99. package/plugins/theme-roller/public/inject-head-late.html +1 -0
  100. package/public/css/forms.css +1 -0
  101. package/public/css/site.css +1 -302
  102. package/public/js/btt.js +1 -90
  103. package/public/js/cookie-consent.js +1 -61
  104. package/public/js/form-logic-engine.js +1 -0
  105. package/public/js/forms.js +1 -0
  106. package/public/js/site.js +1 -204
  107. package/scripts/build.js +194 -129
  108. package/scripts/pro.js +254 -0
  109. package/scripts/reset.js +33 -8
  110. package/scripts/seed.js +343 -78
  111. package/scripts/setup.js +5 -4
  112. package/server/middleware/auth.js +136 -97
  113. package/server/routes/api/actions.js +200 -0
  114. package/server/routes/api/auth.js +292 -116
  115. package/server/routes/api/blocks.js +84 -0
  116. package/server/routes/api/collections.js +88 -23
  117. package/{plugins/form-builder/plugin.js → server/routes/api/forms.js} +483 -505
  118. package/server/routes/api/layouts.js +49 -25
  119. package/server/routes/api/media.js +118 -93
  120. package/server/routes/api/navigation.js +40 -37
  121. package/server/routes/api/pages.js +132 -118
  122. package/server/routes/api/plugins.js +6 -3
  123. package/server/routes/api/settings.js +104 -89
  124. package/server/routes/api/users.js +27 -21
  125. package/server/routes/api/views.js +148 -0
  126. package/server/routes/public.js +124 -108
  127. package/server/server.js +269 -173
  128. package/server/services/actions.js +387 -0
  129. package/server/services/adapterRegistry.js +98 -0
  130. package/server/services/adapters/FileAdapter.js +192 -0
  131. package/server/services/adapters/MongoAdapter.js +220 -0
  132. package/server/services/blocks.js +162 -0
  133. package/server/services/collections.js +74 -86
  134. package/server/services/connectionManager.js +102 -0
  135. package/server/services/content.js +312 -307
  136. package/{plugins/form-builder → server/services}/email.js +126 -103
  137. package/server/services/forms.js +173 -0
  138. package/server/services/markdown.js +1378 -648
  139. package/server/services/permissionRegistry.js +173 -0
  140. package/server/services/presetCollections.js +251 -0
  141. package/server/services/renderer.js +75 -1
  142. package/server/services/roles.js +227 -0
  143. package/server/services/rowAccess.js +104 -0
  144. package/server/services/userProfiles.js +199 -0
  145. package/server/services/users.js +281 -212
  146. package/server/services/views.js +280 -0
  147. package/server/templates/page.html +119 -113
  148. package/plugins/form-builder/admin/templates/form-editor.html +0 -171
  149. package/plugins/form-builder/admin/templates/form-settings.html +0 -29
  150. package/plugins/form-builder/admin/views/form-editor.js +0 -1442
  151. package/plugins/form-builder/admin/views/form-settings.js +0 -38
  152. package/plugins/form-builder/admin/views/form-submissions.js +0 -295
  153. package/plugins/form-builder/admin/views/forms-list.js +0 -164
  154. package/plugins/form-builder/config.js +0 -9
  155. package/plugins/form-builder/data/forms/consent.json +0 -104
  156. package/plugins/form-builder/data/forms/contact-details.json +0 -63
  157. package/plugins/form-builder/data/forms/contacts.json +0 -66
  158. package/plugins/form-builder/data/submissions/consent.json +0 -13
  159. package/plugins/form-builder/data/submissions/contact-details.json +0 -1
  160. package/plugins/form-builder/data/submissions/contacts.json +0 -26
  161. package/plugins/form-builder/plugin.json +0 -52
  162. package/plugins/form-builder/public/form-logic-engine.js +0 -568
  163. package/plugins/form-builder/public/inject-body.html +0 -352
  164. package/plugins/form-builder/public/inject-head.html +0 -58
  165. package/plugins/form-builder/public/package.json +0 -1
  166. package/scripts/copy-domma.js +0 -48
@@ -1,1442 +0,0 @@
1
- /**
2
- * Form Builder Plugin — Form Editor View
3
- * List-based visual field editor with add/remove/reorder, live preview, and save.
4
- * Supports "Page Break" pseudo-fields for wizard forms.
5
- */
6
- import {apiRequest} from '/admin/js/api.js';
7
-
8
- const FIELD_TYPES = [
9
- { value: 'string', label: 'Text (single line)' },
10
- { value: 'email', label: 'Email' },
11
- { value: 'tel', label: 'Phone' },
12
- { value: 'number', label: 'Number' },
13
- { value: 'textarea', label: 'Textarea (multi-line)' },
14
- { value: 'select', label: 'Dropdown (select)' },
15
- { value: 'radio', label: 'Radio buttons' },
16
- { value: 'checkbox', label: 'Single checkbox' },
17
- { value: 'checkbox-group', label: 'Checkbox group' },
18
- { value: 'date', label: 'Date' },
19
- { value: 'time', label: 'Time' },
20
- { value: 'url', label: 'URL' },
21
- { value: 'password', label: 'Password' },
22
- { value: 'file', label: 'File upload' },
23
- { value: 'hidden', label: 'Hidden field' }
24
- ];
25
-
26
- const OPTION_TYPES = new Set(['select', 'radio', 'checkbox-group']);
27
-
28
- let fields = [];
29
- let formSlug = null;
30
-
31
- function slugify(str) {
32
- return str.toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_|_$/g, '');
33
- }
34
-
35
- function getTypeLabel(type) {
36
- return FIELD_TYPES.find(t => t.value === type)?.label || type;
37
- }
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
-
48
- // ---------------------------------------------------------------------------
49
- // Read field values from DOM back into array
50
- // ---------------------------------------------------------------------------
51
-
52
- function collectFieldFromDOM(idx) {
53
- const field = { ...fields[idx] };
54
-
55
- if (field.type === 'spacer') return field;
56
-
57
- // Page-break cards only have label + description inputs
58
- if (field.type === 'page-break') {
59
- const labelEl = document.getElementById(`fb-pb-label-${idx}`);
60
- const descEl = document.getElementById(`fb-pb-desc-${idx}`);
61
- if (labelEl) field.label = labelEl.value.trim() || field.label;
62
- if (descEl) field.description = descEl.value.trim();
63
- return field;
64
- }
65
-
66
- const label = document.getElementById(`fb-label-${idx}`);
67
- const name = document.getElementById(`fb-name-${idx}`);
68
- const type = document.getElementById(`fb-type-${idx}`);
69
- const required = document.getElementById(`fb-required-${idx}`);
70
- const placeholder = document.getElementById(`fb-placeholder-${idx}`);
71
- const helper = document.getElementById(`fb-helper-${idx}`);
72
-
73
- if (label) field.label = label.value.trim() || field.label;
74
- if (name) field.name = name.value.trim() || field.name;
75
- if (type) field.type = type.value || field.type;
76
- if (required) field.required = required.checked;
77
- if (placeholder) field.placeholder = placeholder.value.trim();
78
- if (helper) field.helper = helper.value.trim();
79
-
80
- if (OPTION_TYPES.has(field.type)) {
81
- const ta = document.getElementById(`fb-options-${idx}`);
82
- if (ta) {
83
- field.options = ta.value.split('\n').filter(l => l.trim()).map(line => {
84
- const [v, ...rest] = line.split(':');
85
- return { value: v.trim(), label: rest.join(':').trim() || v.trim() };
86
- });
87
- }
88
- }
89
-
90
- if (field.type === 'textarea') {
91
- const rows = parseInt(document.getElementById(`fb-rows-${idx}`)?.value, 10);
92
- if (rows > 0) field.formConfig = { rows };
93
- }
94
-
95
- const minLen = document.getElementById(`fb-minlength-${idx}`)?.value;
96
- if (minLen) field.minLength = parseInt(minLen, 10);
97
- const maxLen = document.getElementById(`fb-maxlength-${idx}`)?.value;
98
- if (maxLen) field.maxLength = parseInt(maxLen, 10);
99
-
100
- const minNum = document.getElementById(`fb-min-${idx}`)?.value;
101
- if (minNum !== '' && minNum !== undefined) field.min = parseFloat(minNum);
102
- const maxNum = document.getElementById(`fb-max-${idx}`)?.value;
103
- if (maxNum !== '' && maxNum !== undefined) field.max = parseFloat(maxNum);
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
-
113
- return field;
114
- }
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
-
218
- function syncAllFieldsFromDOM() {
219
- fields = fields.map((_, idx) => collectFieldFromDOM(idx));
220
- }
221
-
222
- // ---------------------------------------------------------------------------
223
- // Field list renderer — attaches all events directly to elements
224
- // ---------------------------------------------------------------------------
225
-
226
- function renderFieldList($container) {
227
- const listEl = $container.find('#fields-list').get(0);
228
- const emptyMsg = $container.find('#fields-empty-msg').get(0);
229
- if (!listEl) return;
230
-
231
- // Remove old field cards
232
- Array.from(listEl.querySelectorAll('.fb-field-card')).forEach(el => el.remove());
233
-
234
- if (fields.length === 0) {
235
- if (emptyMsg) emptyMsg.style.display = '';
236
- return;
237
- }
238
- if (emptyMsg) emptyMsg.style.display = 'none';
239
-
240
- fields.forEach((field, idx) => {
241
- const card = field.type === 'page-break'
242
- ? buildPageBreakCard(field, idx, $container)
243
- : field.type === 'spacer'
244
- ? buildSpacerCard(field, idx, $container)
245
- : buildFieldCard(field, idx, $container);
246
- listEl.appendChild(card);
247
- });
248
- }
249
-
250
- // ---------------------------------------------------------------------------
251
- // Page Break card
252
- // ---------------------------------------------------------------------------
253
-
254
- function buildPageBreakCard(field, idx, $container) {
255
- const card = document.createElement('div');
256
- card.className = 'fb-field-card';
257
- card.dataset.index = idx;
258
- card.style.cssText = 'border:2px dashed var(--border-color,#444);border-radius:6px;overflow:hidden;margin-bottom:.5rem;background:var(--card-bg,rgba(255,255,255,.02));';
259
-
260
- // Header
261
- const header = document.createElement('div');
262
- header.style.cssText = 'display:flex;align-items:center;gap:.6rem;padding:.6rem .8rem;cursor:pointer;user-select:none;';
263
-
264
- const badge = document.createElement('span');
265
- badge.textContent = '— Page Break —';
266
- badge.style.cssText = 'font-size:.7rem;padding:.15rem .5rem;border-radius:999px;background:rgba(120,120,120,.2);color:var(--text-muted,#888);white-space:nowrap;flex-shrink:0;font-style:italic;';
267
-
268
- const labelSpan = document.createElement('span');
269
- labelSpan.textContent = field.label || 'Untitled Step';
270
- labelSpan.style.cssText = 'flex:1;font-weight:600;font-size:.9rem;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text-muted,#888);';
271
-
272
- const controls = document.createElement('div');
273
- controls.style.cssText = 'display:flex;gap:.25rem;flex-shrink:0;margin-left:.5rem;';
274
-
275
- if (idx > 0) {
276
- const upBtn = document.createElement('button');
277
- upBtn.className = 'btn btn-xs btn-ghost';
278
- upBtn.title = 'Move up';
279
- upBtn.textContent = '↑';
280
- upBtn.addEventListener('click', (e) => {
281
- e.stopPropagation();
282
- syncAllFieldsFromDOM();
283
- [fields[idx - 1], fields[idx]] = [fields[idx], fields[idx - 1]];
284
- renderFieldList($container);
285
- });
286
- controls.appendChild(upBtn);
287
- }
288
- if (idx < fields.length - 1) {
289
- const downBtn = document.createElement('button');
290
- downBtn.className = 'btn btn-xs btn-ghost';
291
- downBtn.title = 'Move down';
292
- downBtn.textContent = '↓';
293
- downBtn.addEventListener('click', (e) => {
294
- e.stopPropagation();
295
- syncAllFieldsFromDOM();
296
- [fields[idx], fields[idx + 1]] = [fields[idx + 1], fields[idx]];
297
- renderFieldList($container);
298
- });
299
- controls.appendChild(downBtn);
300
- }
301
-
302
- const toggleBtn = document.createElement('button');
303
- toggleBtn.className = 'btn btn-xs btn-ghost';
304
- toggleBtn.title = 'Edit step';
305
- toggleBtn.textContent = '⋯';
306
- toggleBtn.addEventListener('click', (e) => {
307
- e.stopPropagation();
308
- body.style.display = body.style.display === 'none' ? '' : 'none';
309
- });
310
- controls.appendChild(toggleBtn);
311
-
312
- const removeBtn = document.createElement('button');
313
- removeBtn.className = 'btn btn-xs btn-danger';
314
- removeBtn.title = 'Remove page break';
315
- removeBtn.textContent = '✕';
316
- removeBtn.addEventListener('click', async (e) => {
317
- e.stopPropagation();
318
- const confirmed = await E.confirm('Remove this page break?');
319
- if (!confirmed) return;
320
- syncAllFieldsFromDOM();
321
- fields.splice(idx, 1);
322
- renderFieldList($container);
323
- });
324
- controls.appendChild(removeBtn);
325
-
326
- header.appendChild(badge);
327
- header.appendChild(labelSpan);
328
- header.appendChild(controls);
329
-
330
- header.addEventListener('click', () => {
331
- body.style.display = body.style.display === 'none' ? '' : 'none';
332
- });
333
-
334
- // Body
335
- const body = document.createElement('div');
336
- body.className = 'fb-field-body';
337
- body.style.cssText = 'padding:.8rem;border-top:1px dashed var(--border-color,#444);display:none;';
338
-
339
- const row1 = buildRow([
340
- buildInputGroup('Step Title', `fb-pb-label-${idx}`, 'text', field.label || '', 'Shown as the wizard step heading'),
341
- buildInputGroup('Step Description', `fb-pb-desc-${idx}`, 'text', field.description || '', 'Optional sub-heading')
342
- ]);
343
-
344
- const labelInp = row1.querySelector(`#fb-pb-label-${idx}`);
345
- if (labelInp) {
346
- labelInp.addEventListener('input', () => {
347
- labelSpan.textContent = labelInp.value || 'Untitled Step';
348
- });
349
- }
350
-
351
- body.appendChild(row1);
352
- card.appendChild(header);
353
- card.appendChild(body);
354
- return card;
355
- }
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
-
430
- // ---------------------------------------------------------------------------
431
- // Regular field card
432
- // ---------------------------------------------------------------------------
433
-
434
- function buildFieldCard(field, idx, $container) {
435
- const card = document.createElement('div');
436
- card.className = 'fb-field-card';
437
- card.dataset.index = idx;
438
- card.style.cssText = 'border:1px solid var(--border-color,#333);border-radius:6px;overflow:hidden;margin-bottom:.5rem;';
439
-
440
- // --- Header ---
441
- const header = document.createElement('div');
442
- header.style.cssText = 'display:flex;align-items:center;gap:.6rem;padding:.6rem .8rem;background:var(--card-header-bg,rgba(255,255,255,.04));cursor:pointer;user-select:none;';
443
-
444
- const typeBadge = document.createElement('span');
445
- typeBadge.textContent = getTypeLabel(field.type);
446
- typeBadge.style.cssText = 'font-size:.7rem;padding:.15rem .45rem;border-radius:999px;background:var(--primary-soft,rgba(99,102,241,.15));color:var(--primary,#6366f1);white-space:nowrap;flex-shrink:0;';
447
-
448
- const labelSpan = document.createElement('span');
449
- labelSpan.textContent = field.label || '(unlabelled)';
450
- labelSpan.style.cssText = 'flex:1;font-weight:600;font-size:.9rem;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;';
451
-
452
- const controls = document.createElement('div');
453
- controls.style.cssText = 'display:flex;gap:.25rem;flex-shrink:0;margin-left:.5rem;';
454
-
455
- if (idx > 0) {
456
- const upBtn = document.createElement('button');
457
- upBtn.className = 'btn btn-xs btn-ghost';
458
- upBtn.title = 'Move up';
459
- upBtn.textContent = '↑';
460
- upBtn.addEventListener('click', (e) => {
461
- e.stopPropagation();
462
- syncAllFieldsFromDOM();
463
- [fields[idx - 1], fields[idx]] = [fields[idx], fields[idx - 1]];
464
- renderFieldList($container);
465
- });
466
- controls.appendChild(upBtn);
467
- }
468
- if (idx < fields.length - 1) {
469
- const downBtn = document.createElement('button');
470
- downBtn.className = 'btn btn-xs btn-ghost';
471
- downBtn.title = 'Move down';
472
- downBtn.textContent = '↓';
473
- downBtn.addEventListener('click', (e) => {
474
- e.stopPropagation();
475
- syncAllFieldsFromDOM();
476
- [fields[idx], fields[idx + 1]] = [fields[idx + 1], fields[idx]];
477
- renderFieldList($container);
478
- });
479
- controls.appendChild(downBtn);
480
- }
481
-
482
- const toggleBtn = document.createElement('button');
483
- toggleBtn.className = 'btn btn-xs btn-ghost';
484
- toggleBtn.title = 'Edit field';
485
- toggleBtn.textContent = '⋯';
486
- toggleBtn.addEventListener('click', (e) => {
487
- e.stopPropagation();
488
- body.style.display = body.style.display === 'none' ? '' : 'none';
489
- });
490
- controls.appendChild(toggleBtn);
491
-
492
- const removeBtn = document.createElement('button');
493
- removeBtn.className = 'btn btn-xs btn-danger';
494
- removeBtn.title = 'Remove field';
495
- removeBtn.textContent = '✕';
496
- removeBtn.addEventListener('click', async (e) => {
497
- e.stopPropagation();
498
- const confirmed = await E.confirm('Remove this field?');
499
- if (!confirmed) return;
500
- syncAllFieldsFromDOM();
501
- fields.splice(idx, 1);
502
- renderFieldList($container);
503
- });
504
- controls.appendChild(removeBtn);
505
-
506
- header.appendChild(typeBadge);
507
- header.appendChild(labelSpan);
508
- if (field.required) {
509
- const req = document.createElement('span');
510
- req.textContent = 'required';
511
- req.style.cssText = 'font-size:.7rem;color:var(--danger,#ef4444);flex-shrink:0;';
512
- header.appendChild(req);
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
- }
521
- header.appendChild(controls);
522
-
523
- // Click header to toggle body
524
- header.addEventListener('click', () => {
525
- body.style.display = body.style.display === 'none' ? '' : 'none';
526
- });
527
-
528
- // --- Body (hidden by default, auto-expanded for new fields) ---
529
- const body = buildFieldBody(field, idx, labelSpan);
530
- body.style.display = 'none';
531
-
532
- card.appendChild(header);
533
- card.appendChild(body);
534
- return card;
535
- }
536
-
537
- function buildFieldBody(field, idx, labelSpan) {
538
- const body = document.createElement('div');
539
- body.className = 'fb-field-body';
540
- body.style.cssText = 'padding:.8rem;border-top:1px solid var(--border-color,#333);';
541
-
542
- const row1 = buildRow([
543
- buildInputGroup('Label', `fb-label-${idx}`, 'text', field.label || '', 'Shown above the field'),
544
- buildInputGroup('Field Name', `fb-name-${idx}`, 'text', field.name || '', 'Used as data key')
545
- ]);
546
- const row2 = buildRow([
547
- buildSelectGroup('Type', `fb-type-${idx}`, FIELD_TYPES, field.type || 'string'),
548
- buildCheckboxGroup('Required', `fb-required-${idx}`, field.required || false)
549
- ]);
550
- const row3 = buildRow([
551
- buildInputGroup('Placeholder', `fb-placeholder-${idx}`, 'text', field.placeholder || '', 'Hint text inside the field'),
552
- buildInputGroup('Helper Text', `fb-helper-${idx}`, 'text', field.helper || '', 'Shown below the field')
553
- ]);
554
-
555
- body.appendChild(row1);
556
- body.appendChild(row2);
557
- body.appendChild(row3);
558
-
559
- // Auto-update header label and field name as user types
560
- const labelInp = body.querySelector(`#fb-label-${idx}`);
561
- const nameInp = body.querySelector(`#fb-name-${idx}`);
562
- if (labelInp) {
563
- labelInp.addEventListener('input', () => {
564
- if (labelSpan) labelSpan.textContent = labelInp.value || '(unlabelled)';
565
- if (nameInp && !nameInp.dataset.manuallyEdited) {
566
- nameInp.value = slugify(labelInp.value);
567
- }
568
- });
569
- }
570
- if (nameInp) {
571
- nameInp.addEventListener('input', () => { nameInp.dataset.manuallyEdited = '1'; });
572
- }
573
-
574
- // Re-render body extras when type changes (options, rows, min/max)
575
- const typeSelect = body.querySelector(`#fb-type-${idx}`);
576
- if (typeSelect) {
577
- typeSelect.addEventListener('change', () => {
578
- // Update badge in header
579
- const card = body.closest('.fb-field-card');
580
- if (card) {
581
- const badge = card.querySelector('span');
582
- if (badge) badge.textContent = getTypeLabel(typeSelect.value);
583
- }
584
- // Rebuild extras section
585
- const extrasEl = body.querySelector('.fb-field-extras');
586
- if (extrasEl) extrasEl.remove();
587
- const extras = buildFieldExtras(typeSelect.value, field, idx);
588
- if (extras) body.appendChild(extras);
589
- });
590
- }
591
-
592
- // Type-specific extras
593
- const extras = buildFieldExtras(field.type, field, idx);
594
- if (extras) body.appendChild(extras);
595
-
596
- // Conditional logic builder
597
- body.appendChild(buildFieldLogic(field, idx));
598
-
599
- return body;
600
- }
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
-
998
- function buildFieldExtras(type, field, idx) {
999
- const wrap = document.createElement('div');
1000
- wrap.className = 'fb-field-extras';
1001
-
1002
- if (OPTION_TYPES.has(type)) {
1003
- wrap.appendChild(buildOptionsEditor(field.options || [], idx));
1004
- }
1005
- if (type === 'textarea') {
1006
- wrap.appendChild(buildRow([
1007
- buildInputGroup('Rows', `fb-rows-${idx}`, 'number', field.formConfig?.rows || 4, 'Height of textarea')
1008
- ]));
1009
- }
1010
- if (type === 'string' || type === 'textarea') {
1011
- wrap.appendChild(buildRow([
1012
- buildInputGroup('Min Length', `fb-minlength-${idx}`, 'number', field.minLength || '', ''),
1013
- buildInputGroup('Max Length', `fb-maxlength-${idx}`, 'number', field.maxLength || '', '')
1014
- ]));
1015
- }
1016
- if (type === 'number') {
1017
- wrap.appendChild(buildRow([
1018
- buildInputGroup('Min', `fb-min-${idx}`, 'number', field.min ?? '', ''),
1019
- buildInputGroup('Max', `fb-max-${idx}`, 'number', field.max ?? '', '')
1020
- ]));
1021
- }
1022
-
1023
- return wrap.children.length ? wrap : null;
1024
- }
1025
-
1026
- // ---------------------------------------------------------------------------
1027
- // DOM builders
1028
- // ---------------------------------------------------------------------------
1029
-
1030
- function buildRow(children) {
1031
- const row = document.createElement('div');
1032
- row.style.cssText = 'display:flex;gap:.75rem;margin-bottom:.6rem;';
1033
- children.forEach(c => { if (c) row.appendChild(c); });
1034
- return row;
1035
- }
1036
-
1037
- function buildInputGroup(label, id, type, value, hint) {
1038
- const wrap = document.createElement('div');
1039
- wrap.style.flex = '1';
1040
-
1041
- const lbl = document.createElement('label');
1042
- lbl.htmlFor = id;
1043
- lbl.className = 'form-label';
1044
- lbl.textContent = label;
1045
- lbl.style.fontSize = '.8rem';
1046
-
1047
- const inp = document.createElement('input');
1048
- inp.id = id;
1049
- inp.type = type || 'text';
1050
- inp.className = 'form-input';
1051
- inp.value = value ?? '';
1052
-
1053
- wrap.appendChild(lbl);
1054
- wrap.appendChild(inp);
1055
-
1056
- if (hint) {
1057
- const h = document.createElement('p');
1058
- h.className = 'form-hint text-muted';
1059
- h.textContent = hint;
1060
- h.style.cssText = 'font-size:.73rem;margin-top:.2rem;';
1061
- wrap.appendChild(h);
1062
- }
1063
- return wrap;
1064
- }
1065
-
1066
- function buildSelectGroup(label, id, options, selected) {
1067
- const wrap = document.createElement('div');
1068
- wrap.style.flex = '1';
1069
-
1070
- const lbl = document.createElement('label');
1071
- lbl.htmlFor = id;
1072
- lbl.className = 'form-label';
1073
- lbl.textContent = label;
1074
- lbl.style.fontSize = '.8rem';
1075
-
1076
- const sel = document.createElement('select');
1077
- sel.id = id;
1078
- sel.className = 'form-input';
1079
- options.forEach(opt => {
1080
- const o = document.createElement('option');
1081
- o.value = opt.value;
1082
- o.textContent = opt.label;
1083
- if (opt.value === selected) o.selected = true;
1084
- sel.appendChild(o);
1085
- });
1086
-
1087
- wrap.appendChild(lbl);
1088
- wrap.appendChild(sel);
1089
- return wrap;
1090
- }
1091
-
1092
- function buildCheckboxGroup(label, id, checked) {
1093
- const wrap = document.createElement('div');
1094
- wrap.style.cssText = 'flex:0;min-width:80px;display:flex;flex-direction:column;justify-content:flex-end;';
1095
-
1096
- const lbl = document.createElement('label');
1097
- lbl.style.cssText = 'display:flex;align-items:center;gap:.4rem;cursor:pointer;font-size:.8rem;white-space:nowrap;';
1098
-
1099
- const inp = document.createElement('input');
1100
- inp.id = id;
1101
- inp.type = 'checkbox';
1102
- inp.checked = checked;
1103
-
1104
- lbl.appendChild(inp);
1105
- lbl.appendChild(document.createTextNode(label));
1106
- wrap.appendChild(lbl);
1107
- return wrap;
1108
- }
1109
-
1110
- function buildOptionsEditor(options, idx) {
1111
- const wrap = document.createElement('div');
1112
- wrap.style.cssText = 'margin-top:.4rem;';
1113
-
1114
- const title = document.createElement('p');
1115
- title.textContent = 'Options (one per line: value or value:Label)';
1116
- title.style.cssText = 'font-size:.8rem;font-weight:600;margin-bottom:.3rem;';
1117
-
1118
- const ta = document.createElement('textarea');
1119
- ta.id = `fb-options-${idx}`;
1120
- ta.className = 'form-input';
1121
- ta.rows = 4;
1122
- ta.placeholder = 'yes:Yes\nno:No\nmaybe:Maybe';
1123
- ta.value = (options || []).map(o => o.value === o.label ? o.value : `${o.value}:${o.label}`).join('\n');
1124
- ta.style.fontFamily = 'monospace';
1125
-
1126
- wrap.appendChild(title);
1127
- wrap.appendChild(ta);
1128
- return wrap;
1129
- }
1130
-
1131
- // ---------------------------------------------------------------------------
1132
- // Collect full form data
1133
- // ---------------------------------------------------------------------------
1134
-
1135
- function collectFormData($container) {
1136
- syncAllFieldsFromDOM();
1137
- return {
1138
- title: $container.find('#field-title').val().trim(),
1139
- slug: $container.find('#field-slug').val().trim(),
1140
- description: $container.find('#field-description').val().trim(),
1141
- fields,
1142
- settings: {
1143
- submitText: $container.find('#setting-submit-text').val().trim() || 'Submit',
1144
- successMessage: $container.find('#setting-success-message').val().trim() || 'Thank you.',
1145
- layout: $container.find('#setting-layout').val() || 'stacked',
1146
- honeypot: $container.find('#setting-honeypot').prop('checked'),
1147
- rateLimitPerMinute: parseInt($container.find('#setting-rate-limit').val(), 10) || 3
1148
- },
1149
- actions: {
1150
- email: {
1151
- enabled: $container.find('#action-email-enabled').prop('checked'),
1152
- recipients: $container.find('#action-email-recipients').val().trim(),
1153
- subjectPrefix: $container.find('#action-email-subject-prefix').val().trim()
1154
- },
1155
- webhook: {
1156
- enabled: $container.find('#action-webhook-enabled').prop('checked'),
1157
- url: $container.find('#action-webhook-url').val().trim(),
1158
- method: $container.find('#action-webhook-method').val()
1159
- }
1160
- }
1161
- };
1162
- }
1163
-
1164
- // ---------------------------------------------------------------------------
1165
- // Wizard preview helper
1166
- // ---------------------------------------------------------------------------
1167
-
1168
- function buildWizardSteps(formFields) {
1169
- const steps = [];
1170
- let currentGroup = [];
1171
- let stepTitle = 'Step 1';
1172
- let stepDesc = '';
1173
-
1174
- formFields.forEach(f => {
1175
- if (f.type === 'page-break') {
1176
- steps.push({ title: stepTitle, description: stepDesc, fields: currentGroup });
1177
- currentGroup = [];
1178
- stepTitle = f.label || `Step ${steps.length + 1}`;
1179
- stepDesc = f.description || '';
1180
- } else if (f.type !== 'spacer') {
1181
- currentGroup.push(f);
1182
- }
1183
- });
1184
- if (currentGroup.length || steps.length === 0) {
1185
- steps.push({ title: stepTitle, description: stepDesc, fields: currentGroup });
1186
- }
1187
- return steps;
1188
- }
1189
-
1190
- function buildBlueprintFromFields(fieldList) {
1191
- const blueprint = {};
1192
- fieldList.forEach(field => {
1193
- if (field.type === 'page-break' || field.type === 'spacer') return;
1194
- blueprint[field.name] = {
1195
- type: field.type,
1196
- label: field.label,
1197
- required: field.required,
1198
- placeholder: field.placeholder,
1199
- helper: field.helper,
1200
- options: field.options,
1201
- ...(field.formConfig || {})
1202
- };
1203
- });
1204
- return blueprint;
1205
- }
1206
-
1207
- // ---------------------------------------------------------------------------
1208
- // View
1209
- // ---------------------------------------------------------------------------
1210
-
1211
- export const formEditorView = {
1212
- templateUrl: '/plugins/form-builder/admin/templates/form-editor.html',
1213
-
1214
- async onMount($container) {
1215
- fields = [];
1216
- formSlug = null;
1217
-
1218
- const hash = window.location.hash;
1219
- const editMatch = hash.match(/\/plugins\/form-builder\/edit\/([^/?#]+)/);
1220
- formSlug = editMatch ? editMatch[1] : null;
1221
-
1222
- let form = null;
1223
- if (formSlug) {
1224
- try {
1225
- form = await apiRequest(`/plugins/form-builder/forms/${formSlug}`);
1226
- fields = form.fields || [];
1227
- } catch {
1228
- E.toast('Could not load form.', { type: 'error' });
1229
- }
1230
- }
1231
-
1232
- if (form) {
1233
- $container.find('#editor-title').get(0).textContent = `Edit: ${form.title}`;
1234
- $container.find('#field-title').val(form.title);
1235
- $container.find('#field-slug').val(form.slug);
1236
- $container.find('#field-description').val(form.description || '');
1237
-
1238
- const s = form.settings || {};
1239
- $container.find('#setting-submit-text').val(s.submitText || 'Submit');
1240
- $container.find('#setting-success-message').val(s.successMessage || '');
1241
- $container.find('#setting-layout').val(s.layout || 'stacked');
1242
- $container.find('#setting-honeypot').prop('checked', s.honeypot !== false);
1243
- $container.find('#setting-rate-limit').val(s.rateLimitPerMinute || 3);
1244
-
1245
- const e = form.actions?.email || {};
1246
- $container.find('#action-email-enabled').prop('checked', e.enabled || false);
1247
- $container.find('#action-email-recipients').val(e.recipients || '');
1248
- $container.find('#action-email-subject-prefix').val(e.subjectPrefix || '');
1249
-
1250
- const w = form.actions?.webhook || {};
1251
- $container.find('#action-webhook-enabled').prop('checked', w.enabled || false);
1252
- $container.find('#action-webhook-url').val(w.url || '');
1253
- $container.find('#action-webhook-method').val(w.method || 'POST');
1254
- } else {
1255
- $container.find('#editor-title').get(0).textContent = 'New Form';
1256
- }
1257
-
1258
- // Auto-slug from title for new forms
1259
- if (!formSlug) {
1260
- $container.find('#field-title').get(0).addEventListener('input', function () {
1261
- $container.find('#field-slug').val(slugify(this.value));
1262
- });
1263
- }
1264
-
1265
- renderFieldList($container);
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
-
1277
- // --- Add field ---
1278
- $container.find('#add-field-btn').get(0).addEventListener('click', () => {
1279
- if (addMenuEl) addMenuEl.style.display = 'none';
1280
- syncAllFieldsFromDOM();
1281
- const newIdx = fields.length;
1282
- fields.push({
1283
- name: `field_${newIdx + 1}`,
1284
- type: 'string',
1285
- label: 'New Field',
1286
- required: false,
1287
- placeholder: ''
1288
- });
1289
- renderFieldList($container);
1290
- // Auto-expand the newly added field
1291
- const listEl = $container.find('#fields-list').get(0);
1292
- const newCard = listEl?.lastElementChild;
1293
- if (newCard) {
1294
- const body = newCard.querySelector('.fb-field-body');
1295
- if (body) body.style.display = '';
1296
- }
1297
- });
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
-
1307
- // --- Add page break ---
1308
- $container.find('#add-page-break-btn').get(0).addEventListener('click', () => {
1309
- if (addMenuEl) addMenuEl.style.display = 'none';
1310
- syncAllFieldsFromDOM();
1311
- const stepNum = fields.filter(f => f.type === 'page-break').length + 2;
1312
- fields.push({
1313
- type: 'page-break',
1314
- label: `Step ${stepNum}`,
1315
- description: ''
1316
- });
1317
- renderFieldList($container);
1318
- });
1319
-
1320
- // --- Save ---
1321
- $container.find('#save-form-btn').get(0).addEventListener('click', async () => {
1322
- const data = collectFormData($container);
1323
- if (!data.title) {
1324
- E.toast('Please enter a form title.', { type: 'error' });
1325
- return;
1326
- }
1327
- try {
1328
- if (formSlug) {
1329
- await apiRequest(`/plugins/form-builder/forms/${formSlug}`, {
1330
- method: 'PUT',
1331
- body: JSON.stringify(data)
1332
- });
1333
- E.toast('Form saved.', { type: 'success' });
1334
- } else {
1335
- const created = await apiRequest('/plugins/form-builder/forms', {
1336
- method: 'POST',
1337
- body: JSON.stringify({ title: data.title, slug: data.slug })
1338
- });
1339
- await apiRequest(`/plugins/form-builder/forms/${created.slug}`, {
1340
- method: 'PUT',
1341
- body: JSON.stringify({ ...data, slug: created.slug })
1342
- });
1343
- formSlug = created.slug;
1344
- R.navigate(`/plugins/form-builder/edit/${formSlug}`);
1345
- E.toast('Form created.', { type: 'success' });
1346
- }
1347
- } catch (err) {
1348
- E.toast(err.message || 'Failed to save form.', { type: 'error' });
1349
- }
1350
- });
1351
-
1352
- // --- Preview / Test ---
1353
- $container.find('#preview-btn').get(0).addEventListener('click', () => {
1354
- const data = collectFormData($container);
1355
- const previewEl = $container.find('#preview-container').get(0);
1356
- if (!previewEl) return;
1357
-
1358
- // Reset result area
1359
- const resultEl = $container.find('#preview-test-result').get(0);
1360
- const testBadge = $container.find('#preview-test-badge').get(0);
1361
- if (resultEl) {
1362
- resultEl.style.display = 'none';
1363
- resultEl.textContent = '';
1364
- }
1365
- if (testBadge) testBadge.style.display = formSlug ? '' : 'none';
1366
-
1367
- $container.find('#preview-card').get(0).style.display = '';
1368
- previewEl.textContent = '';
1369
-
1370
- const formEl = document.createElement('div');
1371
- formEl.id = 'fb-preview-form';
1372
- previewEl.appendChild(formEl);
1373
-
1374
- // If saved: wire up real submit; otherwise no-op
1375
- const onSubmit = formSlug
1376
- ? async (formData) => {
1377
- if (resultEl) {
1378
- resultEl.style.display = 'none';
1379
- resultEl.textContent = '';
1380
- }
1381
- try {
1382
- const res = await fetch(`/api/plugins/form-builder/submit/${formSlug}`, {
1383
- method: 'POST',
1384
- headers: {'Content-Type': 'application/json'},
1385
- body: JSON.stringify(formData)
1386
- });
1387
- const json = await res.json();
1388
- if (!res.ok) throw new Error(json.error || 'Submission failed.');
1389
- if (resultEl) {
1390
- resultEl.textContent = json.message || data.settings?.successMessage || 'Submitted successfully.';
1391
- resultEl.style.cssText = 'display:block;margin-top:.75rem;padding:.6rem .9rem;border-radius:6px;font-size:.9rem;background:rgba(34,197,94,.12);color:var(--success,#22c55e);';
1392
- }
1393
- E.toast('Test submission stored.', {type: 'success'});
1394
- } catch (err) {
1395
- if (resultEl) {
1396
- resultEl.textContent = err.message;
1397
- resultEl.style.cssText = 'display:block;margin-top:.75rem;padding:.6rem .9rem;border-radius:6px;font-size:.9rem;background:rgba(239,68,68,.12);color:var(--danger,#ef4444);';
1398
- }
1399
- E.toast(err.message, {type: 'error'});
1400
- }
1401
- return false;
1402
- }
1403
- : () => false;
1404
-
1405
- const hasPageBreaks = data.fields.some(f => f.type === 'page-break');
1406
-
1407
- if (typeof F !== 'undefined') {
1408
- if (hasPageBreaks && F.wizard) {
1409
- const steps = buildWizardSteps(data.fields).map(step => ({
1410
- title: step.title,
1411
- description: step.description,
1412
- fields: buildBlueprintFromFields(step.fields)
1413
- }));
1414
- F.wizard('#fb-preview-form', {schema: {steps}, onSubmit});
1415
- } else if (F.render) {
1416
- const blueprint = buildBlueprintFromFields(data.fields);
1417
- F.render('#fb-preview-form', blueprint, {}, {
1418
- submitText: data.settings?.submitText || 'Submit',
1419
- onSubmit
1420
- });
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
- }
1432
- } else {
1433
- const msg = document.createElement('p');
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(', ')}`;
1435
- msg.style.cssText = 'color:var(--muted);font-style:italic;';
1436
- formEl.appendChild(msg);
1437
- }
1438
-
1439
- $container.find('#preview-card').get(0).scrollIntoView({ behavior: 'smooth', block: 'start' });
1440
- });
1441
- }
1442
- };