@usefillo/dom 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/LICENSE +21 -0
- package/README.md +47 -0
- package/dist/index.d.ts +87 -0
- package/dist/index.js +920 -0
- package/dist/standalone.global.js +5741 -0
- package/dist/styles.css +382 -0
- package/package.json +54 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,920 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import {
|
|
3
|
+
FilloClient,
|
|
4
|
+
FilloError,
|
|
5
|
+
createClient,
|
|
6
|
+
createFormController,
|
|
7
|
+
defineForm,
|
|
8
|
+
isCodeForm,
|
|
9
|
+
isField,
|
|
10
|
+
pipeBlock,
|
|
11
|
+
syncCodeForm
|
|
12
|
+
} from "@usefillo/core";
|
|
13
|
+
var FIELD_INPUT_PREFIX = "fillo-";
|
|
14
|
+
function el(tag, options = {}) {
|
|
15
|
+
const node = document.createElement(tag);
|
|
16
|
+
if (options.className) node.className = options.className;
|
|
17
|
+
if (options.text !== void 0) node.textContent = options.text;
|
|
18
|
+
for (const [name, value] of Object.entries(options.attrs ?? {})) node.setAttribute(name, value);
|
|
19
|
+
return node;
|
|
20
|
+
}
|
|
21
|
+
function themeStyle(node, theme) {
|
|
22
|
+
if (!theme) return;
|
|
23
|
+
if (theme.primary) node.style.setProperty("--fillo-primary", theme.primary);
|
|
24
|
+
if (theme.background) node.style.setProperty("--fillo-bg", theme.background);
|
|
25
|
+
if (theme.text) node.style.setProperty("--fillo-text", theme.text);
|
|
26
|
+
if (theme.radius) node.style.setProperty("--fillo-radius", theme.radius);
|
|
27
|
+
if (theme.fontFamily) node.style.setProperty("--fillo-font", theme.fontFamily);
|
|
28
|
+
}
|
|
29
|
+
function inputValue(value) {
|
|
30
|
+
return typeof value === "string" || typeof value === "number" ? String(value) : "";
|
|
31
|
+
}
|
|
32
|
+
function appendChildren(parent, children) {
|
|
33
|
+
for (const child of children) {
|
|
34
|
+
if (child) parent.appendChild(child);
|
|
35
|
+
}
|
|
36
|
+
return parent;
|
|
37
|
+
}
|
|
38
|
+
function applyFieldAria(node, field, error) {
|
|
39
|
+
if (error) node.setAttribute("aria-invalid", "true");
|
|
40
|
+
if (field.required) node.setAttribute("aria-required", "true");
|
|
41
|
+
const describedBy = [
|
|
42
|
+
field.description ? `${FIELD_INPUT_PREFIX}${field.id}-desc` : null,
|
|
43
|
+
error ? `${FIELD_INPUT_PREFIX}${field.id}-error` : null
|
|
44
|
+
].filter(Boolean).join(" ");
|
|
45
|
+
if (describedBy) node.setAttribute("aria-describedby", describedBy);
|
|
46
|
+
}
|
|
47
|
+
function shell(field, error, child) {
|
|
48
|
+
const wrap = el("div", {
|
|
49
|
+
className: `fillo-field fillo-field--${field.kind}${error ? " fillo-field--error" : ""}`,
|
|
50
|
+
attrs: { "data-field": field.id }
|
|
51
|
+
});
|
|
52
|
+
const label = el("label", {
|
|
53
|
+
className: "fillo-label",
|
|
54
|
+
text: field.label,
|
|
55
|
+
attrs: { id: `${FIELD_INPUT_PREFIX}${field.id}-label`, for: `${FIELD_INPUT_PREFIX}${field.id}` }
|
|
56
|
+
});
|
|
57
|
+
if (field.required)
|
|
58
|
+
label.appendChild(el("span", { className: "fillo-required", text: " *", attrs: { "aria-hidden": "true" } }));
|
|
59
|
+
wrap.appendChild(label);
|
|
60
|
+
if (field.description)
|
|
61
|
+
wrap.appendChild(
|
|
62
|
+
el("p", {
|
|
63
|
+
className: "fillo-description",
|
|
64
|
+
text: field.description,
|
|
65
|
+
attrs: { id: `${FIELD_INPUT_PREFIX}${field.id}-desc` }
|
|
66
|
+
})
|
|
67
|
+
);
|
|
68
|
+
wrap.appendChild(child);
|
|
69
|
+
if (error)
|
|
70
|
+
wrap.appendChild(
|
|
71
|
+
el("p", {
|
|
72
|
+
className: "fillo-error",
|
|
73
|
+
text: error,
|
|
74
|
+
attrs: { role: "alert", id: `${FIELD_INPUT_PREFIX}${field.id}-error` }
|
|
75
|
+
})
|
|
76
|
+
);
|
|
77
|
+
return wrap;
|
|
78
|
+
}
|
|
79
|
+
function textInput(type, context) {
|
|
80
|
+
const input = el("input", {
|
|
81
|
+
className: "fillo-input",
|
|
82
|
+
attrs: { id: `${FIELD_INPUT_PREFIX}${context.field.id}`, type }
|
|
83
|
+
});
|
|
84
|
+
input.value = inputValue(context.value);
|
|
85
|
+
if (context.field.placeholder) input.placeholder = context.field.placeholder;
|
|
86
|
+
input.addEventListener("input", () => context.setValue(input.value, { render: false }));
|
|
87
|
+
input.addEventListener("change", () => context.setValue(input.value));
|
|
88
|
+
applyFieldAria(input, context.field, context.error);
|
|
89
|
+
return shell(context.field, context.error, input);
|
|
90
|
+
}
|
|
91
|
+
function longText(context) {
|
|
92
|
+
const textarea = el("textarea", {
|
|
93
|
+
className: "fillo-input fillo-textarea",
|
|
94
|
+
attrs: { id: `${FIELD_INPUT_PREFIX}${context.field.id}`, rows: "4" }
|
|
95
|
+
});
|
|
96
|
+
textarea.value = typeof context.value === "string" ? context.value : "";
|
|
97
|
+
if (context.field.placeholder) textarea.placeholder = context.field.placeholder;
|
|
98
|
+
textarea.addEventListener("input", () => context.setValue(textarea.value, { render: false }));
|
|
99
|
+
textarea.addEventListener("change", () => context.setValue(textarea.value));
|
|
100
|
+
applyFieldAria(textarea, context.field, context.error);
|
|
101
|
+
return shell(context.field, context.error, textarea);
|
|
102
|
+
}
|
|
103
|
+
function singleChoice(context) {
|
|
104
|
+
const field = context.field;
|
|
105
|
+
const options = displayOptions(field);
|
|
106
|
+
const value = typeof context.value === "string" ? context.value : "";
|
|
107
|
+
const isOther = value !== "" && !field.options.some((option) => option.id === value);
|
|
108
|
+
const wrap = el("div", {
|
|
109
|
+
className: "fillo-options",
|
|
110
|
+
attrs: { role: "radiogroup", "aria-labelledby": `${FIELD_INPUT_PREFIX}${field.id}-label` }
|
|
111
|
+
});
|
|
112
|
+
applyFieldAria(wrap, field, context.error);
|
|
113
|
+
for (const option of options) {
|
|
114
|
+
const label = el("label", {
|
|
115
|
+
className: `fillo-option${value === option.id ? " fillo-option--selected" : ""}`,
|
|
116
|
+
attrs: { "data-option": option.id }
|
|
117
|
+
});
|
|
118
|
+
const input = el("input", { className: "fillo-option-input", attrs: { type: "radio", name: `fillo-${field.id}` } });
|
|
119
|
+
input.checked = value === option.id;
|
|
120
|
+
input.addEventListener("change", () => context.setValue(option.id));
|
|
121
|
+
appendChildren(label, [input, el("span", { className: "fillo-option-label", text: option.label })]);
|
|
122
|
+
wrap.appendChild(label);
|
|
123
|
+
}
|
|
124
|
+
if (field.allowOther) {
|
|
125
|
+
const label = el("label", { className: `fillo-option${isOther ? " fillo-option--selected" : ""}` });
|
|
126
|
+
const input = el("input", { className: "fillo-option-input", attrs: { type: "radio", name: `fillo-${field.id}` } });
|
|
127
|
+
input.checked = isOther;
|
|
128
|
+
input.addEventListener("change", () => context.setValue("", { render: true }));
|
|
129
|
+
label.appendChild(input);
|
|
130
|
+
label.appendChild(el("span", { className: "fillo-option-label", text: "Other" }));
|
|
131
|
+
if (isOther) {
|
|
132
|
+
const other = el("input", { className: "fillo-input fillo-other-input", attrs: { type: "text" } });
|
|
133
|
+
other.value = value;
|
|
134
|
+
other.placeholder = "Your answer";
|
|
135
|
+
other.addEventListener("input", () => context.setValue(other.value, { render: false }));
|
|
136
|
+
other.addEventListener("change", () => context.setValue(other.value));
|
|
137
|
+
label.appendChild(other);
|
|
138
|
+
}
|
|
139
|
+
wrap.appendChild(label);
|
|
140
|
+
}
|
|
141
|
+
return shell(field, context.error, wrap);
|
|
142
|
+
}
|
|
143
|
+
function multiChoice(context) {
|
|
144
|
+
const field = context.field;
|
|
145
|
+
const options = displayOptions(field);
|
|
146
|
+
const selected = Array.isArray(context.value) ? context.value : [];
|
|
147
|
+
const knownIds = new Set(field.options.map((option) => option.id));
|
|
148
|
+
const otherText = selected.find((value) => !knownIds.has(value));
|
|
149
|
+
const wrap = el("div", {
|
|
150
|
+
className: "fillo-options",
|
|
151
|
+
attrs: { role: "group", "aria-labelledby": `${FIELD_INPUT_PREFIX}${field.id}-label` }
|
|
152
|
+
});
|
|
153
|
+
applyFieldAria(wrap, field, context.error);
|
|
154
|
+
const setSelected = (next) => context.setValue(next);
|
|
155
|
+
for (const option of options) {
|
|
156
|
+
const label = el("label", {
|
|
157
|
+
className: `fillo-option${selected.includes(option.id) ? " fillo-option--selected" : ""}`,
|
|
158
|
+
attrs: { "data-option": option.id }
|
|
159
|
+
});
|
|
160
|
+
const input = el("input", { className: "fillo-option-input", attrs: { type: "checkbox" } });
|
|
161
|
+
input.checked = selected.includes(option.id);
|
|
162
|
+
input.addEventListener("change", () => {
|
|
163
|
+
setSelected(input.checked ? [...selected, option.id] : selected.filter((id) => id !== option.id));
|
|
164
|
+
});
|
|
165
|
+
appendChildren(label, [input, el("span", { className: "fillo-option-label", text: option.label })]);
|
|
166
|
+
wrap.appendChild(label);
|
|
167
|
+
}
|
|
168
|
+
if (field.allowOther) {
|
|
169
|
+
const active = otherText !== void 0;
|
|
170
|
+
const label = el("label", { className: `fillo-option${active ? " fillo-option--selected" : ""}` });
|
|
171
|
+
const input = el("input", { className: "fillo-option-input", attrs: { type: "checkbox" } });
|
|
172
|
+
input.checked = active;
|
|
173
|
+
input.addEventListener("change", () => {
|
|
174
|
+
const rest = selected.filter((value) => knownIds.has(value));
|
|
175
|
+
setSelected(input.checked ? [...rest, otherText ?? ""] : rest);
|
|
176
|
+
});
|
|
177
|
+
appendChildren(label, [input, el("span", { className: "fillo-option-label", text: "Other" })]);
|
|
178
|
+
if (active) {
|
|
179
|
+
const other = el("input", { className: "fillo-input fillo-other-input", attrs: { type: "text" } });
|
|
180
|
+
other.value = otherText ?? "";
|
|
181
|
+
other.placeholder = "Your answer";
|
|
182
|
+
other.addEventListener("input", () => {
|
|
183
|
+
const rest = selected.filter((value) => knownIds.has(value));
|
|
184
|
+
context.setValue(other.value ? [...rest, other.value] : rest, { render: false });
|
|
185
|
+
});
|
|
186
|
+
other.addEventListener("change", () => {
|
|
187
|
+
const rest = selected.filter((value) => knownIds.has(value));
|
|
188
|
+
context.setValue(other.value ? [...rest, other.value] : rest);
|
|
189
|
+
});
|
|
190
|
+
label.appendChild(other);
|
|
191
|
+
}
|
|
192
|
+
wrap.appendChild(label);
|
|
193
|
+
}
|
|
194
|
+
return shell(field, context.error, wrap);
|
|
195
|
+
}
|
|
196
|
+
var OTHER_VALUE = "__fw_other__";
|
|
197
|
+
function dropdown(context) {
|
|
198
|
+
const field = context.field;
|
|
199
|
+
const value = typeof context.value === "string" ? context.value : "";
|
|
200
|
+
const isOther = value !== "" && !field.options.some((option) => option.id === value);
|
|
201
|
+
const wrap = el("div");
|
|
202
|
+
const select = el("select", {
|
|
203
|
+
className: "fillo-input fillo-select",
|
|
204
|
+
attrs: { id: `${FIELD_INPUT_PREFIX}${field.id}` }
|
|
205
|
+
});
|
|
206
|
+
select.appendChild(el("option", { text: field.placeholder ?? "Choose...", attrs: { value: "" } }));
|
|
207
|
+
for (const option of displayOptions(field)) {
|
|
208
|
+
select.appendChild(el("option", { text: option.label, attrs: { value: option.id } }));
|
|
209
|
+
}
|
|
210
|
+
if (field.allowOther) select.appendChild(el("option", { text: "Other...", attrs: { value: OTHER_VALUE } }));
|
|
211
|
+
select.value = isOther ? OTHER_VALUE : value;
|
|
212
|
+
select.addEventListener("change", () => {
|
|
213
|
+
context.setValue(select.value === OTHER_VALUE ? "" : select.value || null);
|
|
214
|
+
});
|
|
215
|
+
applyFieldAria(select, field, context.error);
|
|
216
|
+
wrap.appendChild(select);
|
|
217
|
+
if (isOther) {
|
|
218
|
+
const other = el("input", {
|
|
219
|
+
className: "fillo-input fillo-other-input fillo-other-input--block",
|
|
220
|
+
attrs: { type: "text" }
|
|
221
|
+
});
|
|
222
|
+
other.value = value;
|
|
223
|
+
other.placeholder = "Your answer";
|
|
224
|
+
other.addEventListener("input", () => context.setValue(other.value, { render: false }));
|
|
225
|
+
other.addEventListener("change", () => context.setValue(other.value));
|
|
226
|
+
wrap.appendChild(other);
|
|
227
|
+
}
|
|
228
|
+
return shell(field, context.error, wrap);
|
|
229
|
+
}
|
|
230
|
+
function checkbox(context) {
|
|
231
|
+
const wrap = el("div", {
|
|
232
|
+
className: `fillo-field fillo-field--checkbox${context.error ? " fillo-field--error" : ""}`,
|
|
233
|
+
attrs: { "data-field": context.field.id }
|
|
234
|
+
});
|
|
235
|
+
const label = el("label", { className: "fillo-option" });
|
|
236
|
+
const input = el("input", {
|
|
237
|
+
className: "fillo-option-input",
|
|
238
|
+
attrs: { id: `${FIELD_INPUT_PREFIX}${context.field.id}`, type: "checkbox" }
|
|
239
|
+
});
|
|
240
|
+
input.checked = context.value === true;
|
|
241
|
+
input.addEventListener("change", () => context.setValue(input.checked));
|
|
242
|
+
applyFieldAria(input, context.field, context.error);
|
|
243
|
+
const labelText = el("span", { className: "fillo-option-label", text: context.field.label });
|
|
244
|
+
if (context.field.required)
|
|
245
|
+
labelText.appendChild(el("span", { className: "fillo-required", text: " *", attrs: { "aria-hidden": "true" } }));
|
|
246
|
+
appendChildren(label, [input, labelText]);
|
|
247
|
+
wrap.appendChild(label);
|
|
248
|
+
if (context.field.description)
|
|
249
|
+
wrap.appendChild(
|
|
250
|
+
el("p", {
|
|
251
|
+
className: "fillo-description",
|
|
252
|
+
text: context.field.description,
|
|
253
|
+
attrs: { id: `${FIELD_INPUT_PREFIX}${context.field.id}-desc` }
|
|
254
|
+
})
|
|
255
|
+
);
|
|
256
|
+
if (context.error)
|
|
257
|
+
wrap.appendChild(
|
|
258
|
+
el("p", {
|
|
259
|
+
className: "fillo-error",
|
|
260
|
+
text: context.error,
|
|
261
|
+
attrs: { role: "alert", id: `${FIELD_INPUT_PREFIX}${context.field.id}-error` }
|
|
262
|
+
})
|
|
263
|
+
);
|
|
264
|
+
return wrap;
|
|
265
|
+
}
|
|
266
|
+
function rating(context) {
|
|
267
|
+
if (context.field.kind !== "rating") return el("div");
|
|
268
|
+
const max = typeof context.field.max === "number" ? context.field.max : 5;
|
|
269
|
+
const current = typeof context.value === "number" ? context.value : 0;
|
|
270
|
+
const wrap = el("div", { className: "fillo-rating", attrs: { role: "radiogroup", "aria-label": context.field.label } });
|
|
271
|
+
applyFieldAria(wrap, context.field, context.error);
|
|
272
|
+
for (let n = 1; n <= max; n++) {
|
|
273
|
+
const button = el("button", {
|
|
274
|
+
className: `fillo-star${n <= current ? " fillo-star--active" : ""}`,
|
|
275
|
+
text: "*",
|
|
276
|
+
attrs: { type: "button", "aria-label": `${n} of ${max}`, "aria-pressed": String(n === current) }
|
|
277
|
+
});
|
|
278
|
+
button.addEventListener("click", () => context.setValue(n === current ? null : n));
|
|
279
|
+
wrap.appendChild(button);
|
|
280
|
+
}
|
|
281
|
+
return shell(context.field, context.error, wrap);
|
|
282
|
+
}
|
|
283
|
+
function linearScale(context) {
|
|
284
|
+
if (context.field.kind !== "linear_scale") return el("div");
|
|
285
|
+
const min = typeof context.field.min === "number" ? context.field.min : 1;
|
|
286
|
+
const max = typeof context.field.max === "number" ? context.field.max : 10;
|
|
287
|
+
const wrap = el("div");
|
|
288
|
+
const steps = el("div", { className: "fillo-scale", attrs: { role: "radiogroup", "aria-label": context.field.label } });
|
|
289
|
+
applyFieldAria(steps, context.field, context.error);
|
|
290
|
+
for (let n = min; n <= max; n++) {
|
|
291
|
+
const button = el("button", {
|
|
292
|
+
className: `fillo-scale-step${context.value === n ? " fillo-scale-step--active" : ""}`,
|
|
293
|
+
text: String(n),
|
|
294
|
+
attrs: { type: "button", "aria-pressed": String(context.value === n) }
|
|
295
|
+
});
|
|
296
|
+
button.addEventListener("click", () => context.setValue(context.value === n ? null : n));
|
|
297
|
+
steps.appendChild(button);
|
|
298
|
+
}
|
|
299
|
+
wrap.appendChild(steps);
|
|
300
|
+
if ("minLabel" in context.field || "maxLabel" in context.field) {
|
|
301
|
+
appendChildren(wrap, [
|
|
302
|
+
appendChildren(el("div", { className: "fillo-scale-labels" }), [
|
|
303
|
+
el("span", { text: String(context.field.minLabel ?? "") }),
|
|
304
|
+
el("span", { text: String(context.field.maxLabel ?? "") })
|
|
305
|
+
])
|
|
306
|
+
]);
|
|
307
|
+
}
|
|
308
|
+
return shell(context.field, context.error, wrap);
|
|
309
|
+
}
|
|
310
|
+
function ranking(context) {
|
|
311
|
+
const field = context.field;
|
|
312
|
+
if (field.kind !== "ranking") return null;
|
|
313
|
+
const answered = Array.isArray(context.value) ? context.value : [];
|
|
314
|
+
const order = [
|
|
315
|
+
...answered.filter((id) => field.options.some((option) => option.id === id)),
|
|
316
|
+
...field.options.map((option) => option.id).filter((id) => !answered.includes(id))
|
|
317
|
+
];
|
|
318
|
+
const list = el("ol", { className: "fillo-ranking" });
|
|
319
|
+
for (const [index, id] of order.entries()) {
|
|
320
|
+
const option = field.options.find((item2) => item2.id === id);
|
|
321
|
+
if (!option) continue;
|
|
322
|
+
const item = el("li", { className: "fillo-ranking-item" });
|
|
323
|
+
const controls = el("span", { className: "fillo-ranking-controls" });
|
|
324
|
+
const move = (delta) => {
|
|
325
|
+
const target = index + delta;
|
|
326
|
+
if (target < 0 || target >= order.length) return;
|
|
327
|
+
const next = [...order];
|
|
328
|
+
[next[index], next[target]] = [next[target], next[index]];
|
|
329
|
+
context.setValue(next);
|
|
330
|
+
};
|
|
331
|
+
const up = el("button", { className: "fillo-ranking-move", text: "Up", attrs: { type: "button" } });
|
|
332
|
+
up.toggleAttribute("disabled", index === 0);
|
|
333
|
+
up.addEventListener("click", () => move(-1));
|
|
334
|
+
const down = el("button", { className: "fillo-ranking-move", text: "Down", attrs: { type: "button" } });
|
|
335
|
+
down.toggleAttribute("disabled", index === order.length - 1);
|
|
336
|
+
down.addEventListener("click", () => move(1));
|
|
337
|
+
appendChildren(controls, [up, down]);
|
|
338
|
+
appendChildren(item, [
|
|
339
|
+
el("span", { className: "fillo-ranking-index", text: String(index + 1) }),
|
|
340
|
+
el("span", { className: "fillo-ranking-label", text: option.label }),
|
|
341
|
+
controls
|
|
342
|
+
]);
|
|
343
|
+
list.appendChild(item);
|
|
344
|
+
}
|
|
345
|
+
return shell(context.field, context.error, list);
|
|
346
|
+
}
|
|
347
|
+
function matrix(context) {
|
|
348
|
+
const field = context.field;
|
|
349
|
+
if (field.kind !== "matrix") return null;
|
|
350
|
+
const answers = context.value && typeof context.value === "object" && !Array.isArray(context.value) ? context.value : {};
|
|
351
|
+
const table = el("table", { className: "fillo-matrix" });
|
|
352
|
+
const thead = document.createElement("thead");
|
|
353
|
+
const headRow = document.createElement("tr");
|
|
354
|
+
headRow.appendChild(document.createElement("th"));
|
|
355
|
+
for (const col of field.columns) headRow.appendChild(el("th", { text: col.label, attrs: { scope: "col" } }));
|
|
356
|
+
thead.appendChild(headRow);
|
|
357
|
+
table.appendChild(thead);
|
|
358
|
+
const tbody = document.createElement("tbody");
|
|
359
|
+
for (const row of field.rows) {
|
|
360
|
+
const tr = document.createElement("tr");
|
|
361
|
+
tr.appendChild(el("th", { text: row.label, attrs: { scope: "row" } }));
|
|
362
|
+
for (const col of field.columns) {
|
|
363
|
+
const td = document.createElement("td");
|
|
364
|
+
const input = el("input", {
|
|
365
|
+
className: "fillo-option-input",
|
|
366
|
+
attrs: { type: "radio", name: `fillo-${field.id}-${row.id}`, "aria-label": `${row.label}: ${col.label}` }
|
|
367
|
+
});
|
|
368
|
+
input.checked = answers[row.id] === col.id;
|
|
369
|
+
input.addEventListener("change", () => context.setValue({ ...answers, [row.id]: col.id }));
|
|
370
|
+
td.appendChild(input);
|
|
371
|
+
tr.appendChild(td);
|
|
372
|
+
}
|
|
373
|
+
tbody.appendChild(tr);
|
|
374
|
+
}
|
|
375
|
+
table.appendChild(tbody);
|
|
376
|
+
return shell(context.field, context.error, appendChildren(el("div", { className: "fillo-matrix-wrap" }), [table]));
|
|
377
|
+
}
|
|
378
|
+
function signature(context) {
|
|
379
|
+
const wrap = el("div", { className: "fillo-signature" });
|
|
380
|
+
const canvas = el("canvas", {
|
|
381
|
+
className: "fillo-signature-canvas",
|
|
382
|
+
attrs: { id: `${FIELD_INPUT_PREFIX}${context.field.id}` }
|
|
383
|
+
});
|
|
384
|
+
applyFieldAria(canvas, context.field, context.error);
|
|
385
|
+
const clear = el("button", {
|
|
386
|
+
className: "fillo-signature-clear",
|
|
387
|
+
text: "Clear",
|
|
388
|
+
attrs: { type: "button" }
|
|
389
|
+
});
|
|
390
|
+
const hint = el("div", { className: "fillo-signature-hint", text: "Sign here" });
|
|
391
|
+
canvas.width = 720;
|
|
392
|
+
canvas.height = 220;
|
|
393
|
+
const ctx = canvas.getContext("2d");
|
|
394
|
+
let drawing = false;
|
|
395
|
+
let hasInk = typeof context.value === "string" && context.value.startsWith("data:image/");
|
|
396
|
+
if (hasInk && ctx) {
|
|
397
|
+
const image = new Image();
|
|
398
|
+
image.onload = () => ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
|
|
399
|
+
image.src = String(context.value);
|
|
400
|
+
hint.style.display = "none";
|
|
401
|
+
}
|
|
402
|
+
const point = (event) => {
|
|
403
|
+
const rect = canvas.getBoundingClientRect();
|
|
404
|
+
return {
|
|
405
|
+
x: (event.clientX - rect.left) / rect.width * canvas.width,
|
|
406
|
+
y: (event.clientY - rect.top) / rect.height * canvas.height
|
|
407
|
+
};
|
|
408
|
+
};
|
|
409
|
+
canvas.addEventListener("pointerdown", (event) => {
|
|
410
|
+
if (!ctx) return;
|
|
411
|
+
drawing = true;
|
|
412
|
+
hasInk = true;
|
|
413
|
+
hint.style.display = "none";
|
|
414
|
+
canvas.setPointerCapture(event.pointerId);
|
|
415
|
+
const p = point(event);
|
|
416
|
+
ctx.beginPath();
|
|
417
|
+
ctx.moveTo(p.x, p.y);
|
|
418
|
+
});
|
|
419
|
+
canvas.addEventListener("pointermove", (event) => {
|
|
420
|
+
if (!drawing || !ctx) return;
|
|
421
|
+
const p = point(event);
|
|
422
|
+
ctx.lineWidth = 2.4;
|
|
423
|
+
ctx.lineCap = "round";
|
|
424
|
+
ctx.strokeStyle = getComputedStyle(canvas).color || "#18181b";
|
|
425
|
+
ctx.lineTo(p.x, p.y);
|
|
426
|
+
ctx.stroke();
|
|
427
|
+
});
|
|
428
|
+
const finish = () => {
|
|
429
|
+
if (!drawing) return;
|
|
430
|
+
drawing = false;
|
|
431
|
+
if (hasInk) context.setValue(canvas.toDataURL("image/png"));
|
|
432
|
+
};
|
|
433
|
+
canvas.addEventListener("pointerup", finish);
|
|
434
|
+
canvas.addEventListener("pointercancel", finish);
|
|
435
|
+
clear.addEventListener("click", () => {
|
|
436
|
+
ctx?.clearRect(0, 0, canvas.width, canvas.height);
|
|
437
|
+
hasInk = false;
|
|
438
|
+
hint.style.display = "";
|
|
439
|
+
context.setValue(null);
|
|
440
|
+
});
|
|
441
|
+
appendChildren(wrap, [canvas, hint, clear]);
|
|
442
|
+
return shell(context.field, context.error, wrap);
|
|
443
|
+
}
|
|
444
|
+
function fileUpload(context) {
|
|
445
|
+
const field = context.field;
|
|
446
|
+
if (field.kind !== "file_upload") return null;
|
|
447
|
+
const input = el("input", {
|
|
448
|
+
className: "fillo-input",
|
|
449
|
+
attrs: { id: `${FIELD_INPUT_PREFIX}${field.id}`, type: "file" }
|
|
450
|
+
});
|
|
451
|
+
if ((field.maxFiles ?? 1) > 1) input.multiple = true;
|
|
452
|
+
if (field.accept?.length) input.accept = field.accept.join(",");
|
|
453
|
+
applyFieldAria(input, field, context.error);
|
|
454
|
+
input.addEventListener("change", () => {
|
|
455
|
+
const files = Array.from(input.files ?? []);
|
|
456
|
+
void context.uploadFiles?.(field, files);
|
|
457
|
+
});
|
|
458
|
+
const wrap = el("div");
|
|
459
|
+
wrap.appendChild(input);
|
|
460
|
+
const progress = context.uploadProgress?.get(field.id);
|
|
461
|
+
if (progress !== void 0) {
|
|
462
|
+
const meter = el("div", { className: "fillo-upload-progress" });
|
|
463
|
+
meter.appendChild(el("div", { className: "fillo-upload-progress-fill" }));
|
|
464
|
+
meter.firstElementChild?.style.setProperty("width", `${Math.round(progress * 100)}%`);
|
|
465
|
+
wrap.appendChild(meter);
|
|
466
|
+
}
|
|
467
|
+
const value = Array.isArray(context.value) ? context.value : [];
|
|
468
|
+
for (const file of value) {
|
|
469
|
+
wrap.appendChild(el("p", { className: "fillo-description", text: `${file.name} (${Math.round(file.size / 1024)} KB)` }));
|
|
470
|
+
}
|
|
471
|
+
return shell(field, context.error, wrap);
|
|
472
|
+
}
|
|
473
|
+
function displayOptions(field) {
|
|
474
|
+
if (!field.shuffleOptions) return field.options;
|
|
475
|
+
const options = [...field.options];
|
|
476
|
+
for (let i = options.length - 1; i > 0; i--) {
|
|
477
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
478
|
+
[options[i], options[j]] = [options[j], options[i]];
|
|
479
|
+
}
|
|
480
|
+
return options;
|
|
481
|
+
}
|
|
482
|
+
var DEFAULT_COMPONENTS = {
|
|
483
|
+
short_text: (ctx) => textInput("text", ctx),
|
|
484
|
+
email: (ctx) => textInput("email", ctx),
|
|
485
|
+
url: (ctx) => textInput("url", ctx),
|
|
486
|
+
phone: (ctx) => textInput("tel", ctx),
|
|
487
|
+
number: (ctx) => textInput("number", ctx),
|
|
488
|
+
date: (ctx) => textInput("date", ctx),
|
|
489
|
+
long_text: longText,
|
|
490
|
+
select: singleChoice,
|
|
491
|
+
multi_select: multiChoice,
|
|
492
|
+
dropdown,
|
|
493
|
+
checkbox,
|
|
494
|
+
rating,
|
|
495
|
+
linear_scale: linearScale,
|
|
496
|
+
ranking,
|
|
497
|
+
matrix,
|
|
498
|
+
signature,
|
|
499
|
+
file_upload: fileUpload
|
|
500
|
+
};
|
|
501
|
+
function defaultFieldRenderer(context) {
|
|
502
|
+
if (context.field.kind === "hidden") return null;
|
|
503
|
+
if (context.field.kind === "custom") return null;
|
|
504
|
+
const renderer = DEFAULT_COMPONENTS[context.field.kind];
|
|
505
|
+
return renderer ? renderer(context) : null;
|
|
506
|
+
}
|
|
507
|
+
var DomFormController = class {
|
|
508
|
+
constructor(target, options) {
|
|
509
|
+
this.target = target;
|
|
510
|
+
this.options = options;
|
|
511
|
+
this.element.className = "fillo-dom-root";
|
|
512
|
+
target.replaceChildren(this.element);
|
|
513
|
+
void this.resolve();
|
|
514
|
+
}
|
|
515
|
+
target;
|
|
516
|
+
options;
|
|
517
|
+
element = el("div");
|
|
518
|
+
schema = null;
|
|
519
|
+
theme;
|
|
520
|
+
formId;
|
|
521
|
+
closed = false;
|
|
522
|
+
// The renderer owns its lifecycle phase; all field state, validation,
|
|
523
|
+
// conditional logic, paging, spam signals, funnel tracking, and submission
|
|
524
|
+
// live in the shared core engine.
|
|
525
|
+
phase = "loading";
|
|
526
|
+
engine = null;
|
|
527
|
+
destroyed = false;
|
|
528
|
+
renderQueued = false;
|
|
529
|
+
hpValue = "";
|
|
530
|
+
uploadProgress = /* @__PURE__ */ new Map();
|
|
531
|
+
get status() {
|
|
532
|
+
if (this.phase === "ready") return this.engine?.getState().status ?? "idle";
|
|
533
|
+
return this.phase;
|
|
534
|
+
}
|
|
535
|
+
get data() {
|
|
536
|
+
return this.engine?.getState().data ?? this.options.initialData ?? {};
|
|
537
|
+
}
|
|
538
|
+
get form() {
|
|
539
|
+
return this.schema;
|
|
540
|
+
}
|
|
541
|
+
get mutableData() {
|
|
542
|
+
return this.engine?.getState().data ?? this.options.initialData ?? {};
|
|
543
|
+
}
|
|
544
|
+
api() {
|
|
545
|
+
const form = this.schema ?? { version: 1, title: "", pages: [], settings: {} };
|
|
546
|
+
const state = this.engine?.getState();
|
|
547
|
+
const pageCount = state?.pageCount ?? form.pages.length;
|
|
548
|
+
return {
|
|
549
|
+
form,
|
|
550
|
+
formId: this.formId,
|
|
551
|
+
client: this.options.client,
|
|
552
|
+
data: state?.data ?? this.mutableData,
|
|
553
|
+
errors: state?.errors ?? {},
|
|
554
|
+
status: this.status,
|
|
555
|
+
pageIndex: state?.pageIndex ?? 0,
|
|
556
|
+
pageCount,
|
|
557
|
+
page: state?.page ?? form.pages[0] ?? { id: "empty", blocks: [] },
|
|
558
|
+
blocks: state?.blocks ?? [],
|
|
559
|
+
isFirstPage: state?.isFirstPage ?? true,
|
|
560
|
+
isLastPage: state?.isLastPage ?? pageCount <= 1,
|
|
561
|
+
setValue: (fieldId, value, opts) => this.setValueInternal(fieldId, value, opts),
|
|
562
|
+
next: () => this.next(),
|
|
563
|
+
back: () => this.back(),
|
|
564
|
+
submit: () => this.submit()
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
async resolve() {
|
|
568
|
+
try {
|
|
569
|
+
const codeForm = isCodeForm(this.options.form) ? this.options.form : null;
|
|
570
|
+
if (codeForm) {
|
|
571
|
+
this.setSchema(codeForm.schema, this.options.theme ?? codeForm.theme, this.options.formId);
|
|
572
|
+
if (this.options.client?.key) {
|
|
573
|
+
syncCodeForm(this.options.client, codeForm).then(({ formId }) => {
|
|
574
|
+
this.formId = formId;
|
|
575
|
+
this.engine?.setContext({ formId });
|
|
576
|
+
}).catch((error) => this.handleError(toFilloError(error)));
|
|
577
|
+
}
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
const schema = this.options.form && !isCodeForm(this.options.form) ? this.options.form : null;
|
|
581
|
+
if (schema) {
|
|
582
|
+
this.setSchema(schema, this.options.theme, this.options.formId);
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
if (!this.options.client || !this.options.formId) {
|
|
586
|
+
throw new FilloError("Provide either `form`, or `client` with `formId`.", 0);
|
|
587
|
+
}
|
|
588
|
+
this.render();
|
|
589
|
+
const published = await this.options.client.getForm(this.options.formId);
|
|
590
|
+
this.closed = Boolean(published.closed);
|
|
591
|
+
this.setSchema(published.schema, this.options.theme ?? published.theme ?? void 0, published.id);
|
|
592
|
+
} catch (error) {
|
|
593
|
+
this.handleError(toFilloError(error));
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
/** Bind the resolved schema, create the engine, and render. */
|
|
597
|
+
setSchema(schema, theme, formId) {
|
|
598
|
+
this.schema = schema;
|
|
599
|
+
this.theme = theme;
|
|
600
|
+
this.formId = formId;
|
|
601
|
+
this.engine = createFormController({
|
|
602
|
+
form: schema,
|
|
603
|
+
formId,
|
|
604
|
+
client: this.options.client,
|
|
605
|
+
initialData: this.options.initialData,
|
|
606
|
+
onChange: this.options.onChange,
|
|
607
|
+
onSubmitted: this.options.onSubmitted,
|
|
608
|
+
onSubmit: this.options.onSubmit,
|
|
609
|
+
getHoneypot: () => this.hpValue
|
|
610
|
+
});
|
|
611
|
+
this.phase = this.closed ? "closed" : "ready";
|
|
612
|
+
this.render();
|
|
613
|
+
}
|
|
614
|
+
setValue(fieldId, value) {
|
|
615
|
+
this.setValueInternal(fieldId, value);
|
|
616
|
+
}
|
|
617
|
+
setValueInternal(fieldId, value, opts = {}) {
|
|
618
|
+
this.engine?.setValue(fieldId, value);
|
|
619
|
+
if (opts.render !== false) this.queueRender();
|
|
620
|
+
}
|
|
621
|
+
next() {
|
|
622
|
+
if (!this.engine) return;
|
|
623
|
+
this.engine.next();
|
|
624
|
+
this.render();
|
|
625
|
+
}
|
|
626
|
+
back() {
|
|
627
|
+
if (!this.engine) return;
|
|
628
|
+
this.engine.back();
|
|
629
|
+
this.render();
|
|
630
|
+
}
|
|
631
|
+
async submit() {
|
|
632
|
+
if (!this.engine || this.phase !== "ready") return;
|
|
633
|
+
const pending = this.engine.submit();
|
|
634
|
+
this.render();
|
|
635
|
+
try {
|
|
636
|
+
await pending;
|
|
637
|
+
} catch (error) {
|
|
638
|
+
this.handleError(toFilloError(error));
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
this.render();
|
|
642
|
+
}
|
|
643
|
+
async uploadFiles(field, files) {
|
|
644
|
+
if (!this.schema) return;
|
|
645
|
+
const maxFiles = field.maxFiles ?? 1;
|
|
646
|
+
const selected = files.slice(0, maxFiles);
|
|
647
|
+
this.uploadProgress.set(field.id, 0);
|
|
648
|
+
this.queueRender();
|
|
649
|
+
try {
|
|
650
|
+
const values = [];
|
|
651
|
+
for (const [index, file] of selected.entries()) {
|
|
652
|
+
if (this.options.client && this.formId) {
|
|
653
|
+
values.push(
|
|
654
|
+
await this.options.client.uploadFile(this.formId, file, {
|
|
655
|
+
fieldId: field.id,
|
|
656
|
+
onProgress: (progress) => {
|
|
657
|
+
this.uploadProgress.set(field.id, (index + progress.fraction) / selected.length);
|
|
658
|
+
this.queueRender();
|
|
659
|
+
}
|
|
660
|
+
})
|
|
661
|
+
);
|
|
662
|
+
} else {
|
|
663
|
+
values.push({
|
|
664
|
+
fileId: `local:${file.name}:${file.size}`,
|
|
665
|
+
name: file.name,
|
|
666
|
+
size: file.size,
|
|
667
|
+
mime: file.type || "application/octet-stream"
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
this.uploadProgress.delete(field.id);
|
|
672
|
+
this.setValueInternal(field.id, values);
|
|
673
|
+
} catch (error) {
|
|
674
|
+
this.uploadProgress.delete(field.id);
|
|
675
|
+
this.handleError(toFilloError(error));
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
destroy() {
|
|
679
|
+
this.destroyed = true;
|
|
680
|
+
this.target.replaceChildren();
|
|
681
|
+
}
|
|
682
|
+
handleError(error) {
|
|
683
|
+
this.phase = "error";
|
|
684
|
+
this.options.onError?.(error);
|
|
685
|
+
this.render(error);
|
|
686
|
+
}
|
|
687
|
+
queueRender() {
|
|
688
|
+
if (this.renderQueued || this.destroyed) return;
|
|
689
|
+
this.renderQueued = true;
|
|
690
|
+
queueMicrotask(() => {
|
|
691
|
+
this.renderQueued = false;
|
|
692
|
+
this.render();
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
render(error) {
|
|
696
|
+
if (this.destroyed) return;
|
|
697
|
+
this.element.replaceChildren();
|
|
698
|
+
if (this.phase === "loading") {
|
|
699
|
+
const loading = el("div", { className: `fillo-form fillo-form--loading ${this.options.className ?? ""}`, attrs: { "aria-busy": "true" } });
|
|
700
|
+
themeStyle(loading, this.theme);
|
|
701
|
+
appendChildren(loading, [
|
|
702
|
+
el("div", { className: "fillo-skeleton" }),
|
|
703
|
+
el("div", { className: "fillo-skeleton" }),
|
|
704
|
+
el("div", { className: "fillo-skeleton fillo-skeleton--short" })
|
|
705
|
+
]);
|
|
706
|
+
this.element.appendChild(loading);
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
if (this.phase === "error" && error) {
|
|
710
|
+
this.element.appendChild(this.options.renderError?.(error) ?? el("div", { className: "fillo-form fillo-form--error", text: error.message }));
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
if (this.phase === "closed") {
|
|
714
|
+
const closed = el("div", { className: `fillo-form fillo-form--closed ${this.options.className ?? ""}` });
|
|
715
|
+
themeStyle(closed, this.theme);
|
|
716
|
+
closed.appendChild(el("p", { className: "fillo-closed", text: "This form is no longer accepting responses." }));
|
|
717
|
+
this.element.appendChild(closed);
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
if (!this.schema) return;
|
|
721
|
+
if (this.engine?.getState().status === "submitted") {
|
|
722
|
+
const api = this.api();
|
|
723
|
+
const success = this.options.renderSuccess?.(api) ?? defaultSuccess(this.schema);
|
|
724
|
+
themeStyle(success, this.theme);
|
|
725
|
+
this.element.appendChild(success);
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
this.element.appendChild(this.renderForm());
|
|
729
|
+
}
|
|
730
|
+
renderForm() {
|
|
731
|
+
const api = this.api();
|
|
732
|
+
const form = el("form", { className: `fillo-form ${this.options.className ?? ""}` });
|
|
733
|
+
themeStyle(form, this.theme);
|
|
734
|
+
form.noValidate = true;
|
|
735
|
+
const advance = () => {
|
|
736
|
+
if (api.isLastPage) void this.submit();
|
|
737
|
+
else this.next();
|
|
738
|
+
};
|
|
739
|
+
form.addEventListener("submit", (event) => {
|
|
740
|
+
event.preventDefault();
|
|
741
|
+
advance();
|
|
742
|
+
});
|
|
743
|
+
if (api.pageCount > 1 && this.schema?.settings.showProgress !== false) {
|
|
744
|
+
const progress = el("div", {
|
|
745
|
+
className: "fillo-progress-track",
|
|
746
|
+
attrs: {
|
|
747
|
+
role: "progressbar",
|
|
748
|
+
"aria-valuemin": "0",
|
|
749
|
+
"aria-valuemax": String(api.pageCount),
|
|
750
|
+
"aria-valuenow": String(api.pageIndex + 1)
|
|
751
|
+
}
|
|
752
|
+
});
|
|
753
|
+
progress.appendChild(el("div", { className: "fillo-progress-fill" }));
|
|
754
|
+
progress.firstElementChild?.style.setProperty(
|
|
755
|
+
"width",
|
|
756
|
+
`${(api.pageIndex + 1) / api.pageCount * 100}%`
|
|
757
|
+
);
|
|
758
|
+
form.appendChild(progress);
|
|
759
|
+
}
|
|
760
|
+
if (api.pageIndex === 0) {
|
|
761
|
+
const header = el("header", { className: "fillo-header" });
|
|
762
|
+
header.appendChild(el("h1", { className: "fillo-title", text: api.form.title }));
|
|
763
|
+
if (api.form.description) header.appendChild(el("p", { className: "fillo-form-description", text: api.form.description }));
|
|
764
|
+
form.appendChild(header);
|
|
765
|
+
} else if (api.page.title) {
|
|
766
|
+
form.appendChild(el("h2", { className: "fillo-page-title", text: api.page.title }));
|
|
767
|
+
}
|
|
768
|
+
const blocks = el("div", { className: "fillo-blocks" });
|
|
769
|
+
for (const block of api.blocks) {
|
|
770
|
+
const rendered = this.renderBlock(pipeBlock(block, this.mutableData, api.form), api);
|
|
771
|
+
if (rendered) blocks.appendChild(rendered);
|
|
772
|
+
}
|
|
773
|
+
form.appendChild(blocks);
|
|
774
|
+
const hp = el("input", { className: "fillo-hp", attrs: { type: "text", name: "fw_hp_field", tabindex: "-1", autocomplete: "off", "aria-hidden": "true" } });
|
|
775
|
+
hp.addEventListener("input", () => {
|
|
776
|
+
this.hpValue = hp.value;
|
|
777
|
+
});
|
|
778
|
+
form.appendChild(hp);
|
|
779
|
+
const footer = el("footer", { className: "fillo-footer" });
|
|
780
|
+
if (api.pageCount > 1 && !api.isFirstPage) {
|
|
781
|
+
const back = el("button", { className: "fillo-button fillo-button--ghost", text: "Back", attrs: { type: "button" } });
|
|
782
|
+
back.addEventListener("click", () => this.back());
|
|
783
|
+
footer.appendChild(back);
|
|
784
|
+
}
|
|
785
|
+
const submit = el("button", {
|
|
786
|
+
className: "fillo-button fillo-button--primary",
|
|
787
|
+
text: api.status === "submitting" ? "Submitting..." : api.isLastPage ? api.form.settings.submitLabel ?? "Submit" : "Next",
|
|
788
|
+
attrs: { type: "submit" }
|
|
789
|
+
});
|
|
790
|
+
submit.toggleAttribute("disabled", api.status === "submitting" || this.uploadProgress.size > 0);
|
|
791
|
+
submit.addEventListener("click", (event) => {
|
|
792
|
+
event.preventDefault();
|
|
793
|
+
advance();
|
|
794
|
+
});
|
|
795
|
+
footer.appendChild(submit);
|
|
796
|
+
form.appendChild(footer);
|
|
797
|
+
return form;
|
|
798
|
+
}
|
|
799
|
+
renderBlock(block, api) {
|
|
800
|
+
if (!isField(block)) return renderContent(block);
|
|
801
|
+
const setValue = (value, options) => this.setValueInternal(block.id, value, options);
|
|
802
|
+
const context = {
|
|
803
|
+
field: block,
|
|
804
|
+
value: api.data[block.id],
|
|
805
|
+
error: api.errors[block.id],
|
|
806
|
+
api,
|
|
807
|
+
setValue,
|
|
808
|
+
uploadFiles: (field, files) => this.uploadFiles(field, files),
|
|
809
|
+
uploadProgress: this.uploadProgress
|
|
810
|
+
};
|
|
811
|
+
const renderer = block.kind === "custom" ? this.options.customComponents?.[block.component] ?? this.options.components?.custom : this.options.components?.[block.kind] ?? defaultFieldRenderer;
|
|
812
|
+
return renderer?.(context) ?? null;
|
|
813
|
+
}
|
|
814
|
+
};
|
|
815
|
+
function renderContent(block) {
|
|
816
|
+
switch (block.kind) {
|
|
817
|
+
case "heading":
|
|
818
|
+
return el("h3", { className: "fillo-heading", text: block.text });
|
|
819
|
+
case "paragraph":
|
|
820
|
+
return el("p", { className: "fillo-paragraph", text: block.text });
|
|
821
|
+
case "divider":
|
|
822
|
+
return el("hr", { className: "fillo-divider" });
|
|
823
|
+
}
|
|
824
|
+
return el("div");
|
|
825
|
+
}
|
|
826
|
+
function defaultSuccess(schema) {
|
|
827
|
+
const success = el("div", { className: "fillo-form fillo-form--success" });
|
|
828
|
+
const inner = el("div", { className: "fillo-success" });
|
|
829
|
+
appendChildren(inner, [
|
|
830
|
+
el("div", { className: "fillo-success-mark", text: "\u2713", attrs: { "aria-hidden": "true" } }),
|
|
831
|
+
el("h2", { className: "fillo-success-title", text: schema.settings.successTitle ?? "Thanks!" }),
|
|
832
|
+
el("p", { className: "fillo-success-message", text: schema.settings.successMessage ?? "Your response has been recorded." })
|
|
833
|
+
]);
|
|
834
|
+
success.appendChild(inner);
|
|
835
|
+
return success;
|
|
836
|
+
}
|
|
837
|
+
function toFilloError(error) {
|
|
838
|
+
if (error instanceof FilloError) return error;
|
|
839
|
+
return new FilloError(error instanceof Error ? error.message : String(error), 0);
|
|
840
|
+
}
|
|
841
|
+
function renderForm(target, options) {
|
|
842
|
+
const element = typeof target === "string" ? document.querySelector(target) : target;
|
|
843
|
+
if (!element) throw new FilloError(`Fillo mount target not found: ${String(target)}`, 0);
|
|
844
|
+
return new DomFormController(element, { ...options, initialData: options.initialData ?? {} });
|
|
845
|
+
}
|
|
846
|
+
function createFormElement(options) {
|
|
847
|
+
const host = el("div");
|
|
848
|
+
renderForm(host, options);
|
|
849
|
+
return host;
|
|
850
|
+
}
|
|
851
|
+
var FilloFormElement = class extends HTMLElement {
|
|
852
|
+
instance = null;
|
|
853
|
+
assignedForm;
|
|
854
|
+
assignedClient;
|
|
855
|
+
assignedTheme;
|
|
856
|
+
assignedInitialData;
|
|
857
|
+
static get observedAttributes() {
|
|
858
|
+
return ["base-url", "publishable-key", "form-id"];
|
|
859
|
+
}
|
|
860
|
+
connectedCallback() {
|
|
861
|
+
this.mount();
|
|
862
|
+
}
|
|
863
|
+
disconnectedCallback() {
|
|
864
|
+
this.instance?.destroy();
|
|
865
|
+
this.instance = null;
|
|
866
|
+
}
|
|
867
|
+
attributeChangedCallback() {
|
|
868
|
+
if (this.isConnected) this.mount();
|
|
869
|
+
}
|
|
870
|
+
set form(value) {
|
|
871
|
+
this.assignedForm = value;
|
|
872
|
+
if (this.isConnected) this.mount();
|
|
873
|
+
}
|
|
874
|
+
get form() {
|
|
875
|
+
return this.assignedForm;
|
|
876
|
+
}
|
|
877
|
+
set client(value) {
|
|
878
|
+
this.assignedClient = value;
|
|
879
|
+
if (this.isConnected) this.mount();
|
|
880
|
+
}
|
|
881
|
+
set theme(value) {
|
|
882
|
+
this.assignedTheme = value;
|
|
883
|
+
if (this.isConnected) this.mount();
|
|
884
|
+
}
|
|
885
|
+
set initialData(value) {
|
|
886
|
+
this.assignedInitialData = value;
|
|
887
|
+
if (this.isConnected) this.mount();
|
|
888
|
+
}
|
|
889
|
+
mount() {
|
|
890
|
+
this.instance?.destroy();
|
|
891
|
+
const baseUrl = this.getAttribute("base-url");
|
|
892
|
+
const key = this.getAttribute("publishable-key") ?? void 0;
|
|
893
|
+
const formId = this.getAttribute("form-id") ?? void 0;
|
|
894
|
+
const client = this.assignedClient ?? (baseUrl ? createClient({ baseUrl, key }) : void 0);
|
|
895
|
+
this.instance = renderForm(this, {
|
|
896
|
+
form: this.assignedForm,
|
|
897
|
+
client,
|
|
898
|
+
formId,
|
|
899
|
+
theme: this.assignedTheme,
|
|
900
|
+
initialData: this.assignedInitialData,
|
|
901
|
+
onChange: (data) => this.dispatchEvent(new CustomEvent("fillo-change", { detail: { data } })),
|
|
902
|
+
onSubmitted: (responseId, data) => this.dispatchEvent(new CustomEvent("fillo-submit", { detail: { responseId, data } })),
|
|
903
|
+
onError: (error) => this.dispatchEvent(new CustomEvent("fillo-error", { detail: { error } }))
|
|
904
|
+
});
|
|
905
|
+
}
|
|
906
|
+
};
|
|
907
|
+
function registerFilloElement(name = "fillo-form") {
|
|
908
|
+
if (!customElements.get(name)) customElements.define(name, FilloFormElement);
|
|
909
|
+
}
|
|
910
|
+
export {
|
|
911
|
+
FilloClient,
|
|
912
|
+
FilloError,
|
|
913
|
+
FilloFormElement,
|
|
914
|
+
createClient,
|
|
915
|
+
createFormController,
|
|
916
|
+
createFormElement,
|
|
917
|
+
defineForm,
|
|
918
|
+
registerFilloElement,
|
|
919
|
+
renderForm
|
|
920
|
+
};
|