@formsnip/embed 1.1.1 → 1.1.2
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/dist/FormSnip.d.ts.map +1 -1
- package/dist/index.d.ts +18 -8
- package/dist/index.esm.js +200 -27
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +200 -27
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +14 -4
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/phoneCountries.d.ts +14 -0
- package/dist/utils/phoneCountries.d.ts.map +1 -0
- package/dist/utils/spamProtection.d.ts +3 -0
- package/dist/utils/spamProtection.d.ts.map +1 -0
- package/dist/utils/styles.d.ts +1 -0
- package/dist/utils/styles.d.ts.map +1 -1
- package/dist/utils/upload.d.ts +3 -3
- package/dist/utils/upload.d.ts.map +1 -1
- package/package.json +5 -1
package/dist/index.js
CHANGED
|
@@ -2,13 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
var React = require('react');
|
|
4
4
|
|
|
5
|
-
const
|
|
5
|
+
const HONEYPOT_FIELD = '_fs_hp';
|
|
6
|
+
const SUBMIT_TOKEN_FIELD = '_fs_token';
|
|
7
|
+
|
|
8
|
+
const ALLOWED_FILE_EXTENSIONS = '.jpg,.jpeg,.png,.gif,.webp,.pdf,.mp4,.mpeg,.mov,.avi,.webm,.mkv,.mp3,.wav,.ogg,.aac,.flac';
|
|
6
9
|
const MAX_TOTAL_FILE_SIZE = 15 * 1024 * 1024; // 15MB
|
|
7
10
|
/**
|
|
8
11
|
* Upload a file to R2 via presigned URL
|
|
9
12
|
*/
|
|
10
|
-
async function uploadFile(file, formId, baseUrl) {
|
|
13
|
+
async function uploadFile(file, formId, baseUrl, submitToken) {
|
|
11
14
|
try {
|
|
15
|
+
if (!submitToken) {
|
|
16
|
+
return {
|
|
17
|
+
success: false,
|
|
18
|
+
error: 'Form session expired. Please reload and try again.',
|
|
19
|
+
};
|
|
20
|
+
}
|
|
12
21
|
// Step 1: Get presigned upload URL
|
|
13
22
|
const signResponse = await fetch(`${baseUrl}/uploads/sign`, {
|
|
14
23
|
method: 'POST',
|
|
@@ -21,6 +30,7 @@ async function uploadFile(file, formId, baseUrl) {
|
|
|
21
30
|
contentType: file.type,
|
|
22
31
|
size: file.size,
|
|
23
32
|
formId,
|
|
33
|
+
[SUBMIT_TOKEN_FIELD]: submitToken,
|
|
24
34
|
}),
|
|
25
35
|
});
|
|
26
36
|
if (!signResponse.ok) {
|
|
@@ -66,7 +76,7 @@ async function uploadFile(file, formId, baseUrl) {
|
|
|
66
76
|
/**
|
|
67
77
|
* Upload multiple files to R2
|
|
68
78
|
*/
|
|
69
|
-
async function uploadFiles(files, formId, baseUrl) {
|
|
79
|
+
async function uploadFiles(files, formId, baseUrl, submitToken) {
|
|
70
80
|
// Check total size
|
|
71
81
|
const totalSize = files.reduce((sum, file) => sum + file.size, 0);
|
|
72
82
|
if (totalSize > MAX_TOTAL_FILE_SIZE) {
|
|
@@ -75,7 +85,7 @@ async function uploadFiles(files, formId, baseUrl) {
|
|
|
75
85
|
errors: [`Total file size exceeds 15MB limit per submission`],
|
|
76
86
|
};
|
|
77
87
|
}
|
|
78
|
-
const results = await Promise.allSettled(files.map((file) => uploadFile(file, formId, baseUrl)));
|
|
88
|
+
const results = await Promise.allSettled(files.map((file) => uploadFile(file, formId, baseUrl, submitToken)));
|
|
79
89
|
const fileKeys = [];
|
|
80
90
|
const errors = [];
|
|
81
91
|
results.forEach((result, index) => {
|
|
@@ -263,6 +273,7 @@ function injectBaseStyles() {
|
|
|
263
273
|
'.fs-space-x-2>*+*{margin-left:0.5rem}',
|
|
264
274
|
'.fs-outline-none{outline:none}',
|
|
265
275
|
'.fs-hover-opacity:hover{opacity:0.9}',
|
|
276
|
+
'.formsnip-embed-ph::placeholder{color:var(--fs-placeholder-color,#9ca3af);opacity:1}',
|
|
266
277
|
].join('');
|
|
267
278
|
const style = document.createElement('style');
|
|
268
279
|
style.id = 'formsnip-base-styles';
|
|
@@ -298,6 +309,9 @@ function applyStyles(styles, customStyles) {
|
|
|
298
309
|
if (customStyles.fontSize !== undefined) {
|
|
299
310
|
result.fontSize = `${customStyles.fontSize}px`;
|
|
300
311
|
}
|
|
312
|
+
if (customStyles.placeholderColor) {
|
|
313
|
+
result['--fs-placeholder-color'] = customStyles.placeholderColor;
|
|
314
|
+
}
|
|
301
315
|
// Note: fullWidth is handled separately in the component, not here
|
|
302
316
|
// to ensure proper display property handling
|
|
303
317
|
return result;
|
|
@@ -335,6 +349,86 @@ function pushFormSnipSubmissionDataLayer(options) {
|
|
|
335
349
|
}
|
|
336
350
|
}
|
|
337
351
|
|
|
352
|
+
const PHONE_COUNTRIES = [
|
|
353
|
+
{ iso: 'AU', name: 'Australia', dial: '+61' },
|
|
354
|
+
{ iso: 'AT', name: 'Austria', dial: '+43' },
|
|
355
|
+
{ iso: 'BE', name: 'Belgium', dial: '+32' },
|
|
356
|
+
{ iso: 'BR', name: 'Brazil', dial: '+55' },
|
|
357
|
+
{ iso: 'CA', name: 'Canada', dial: '+1' },
|
|
358
|
+
{ iso: 'CN', name: 'China', dial: '+86' },
|
|
359
|
+
{ iso: 'DK', name: 'Denmark', dial: '+45' },
|
|
360
|
+
{ iso: 'FI', name: 'Finland', dial: '+358' },
|
|
361
|
+
{ iso: 'FR', name: 'France', dial: '+33' },
|
|
362
|
+
{ iso: 'DE', name: 'Germany', dial: '+49' },
|
|
363
|
+
{ iso: 'GR', name: 'Greece', dial: '+30' },
|
|
364
|
+
{ iso: 'HK', name: 'Hong Kong', dial: '+852' },
|
|
365
|
+
{ iso: 'IS', name: 'Iceland', dial: '+354' },
|
|
366
|
+
{ iso: 'IN', name: 'India', dial: '+91' },
|
|
367
|
+
{ iso: 'IE', name: 'Ireland', dial: '+353' },
|
|
368
|
+
{ iso: 'IT', name: 'Italy', dial: '+39' },
|
|
369
|
+
{ iso: 'JP', name: 'Japan', dial: '+81' },
|
|
370
|
+
{ iso: 'LU', name: 'Luxembourg', dial: '+352' },
|
|
371
|
+
{ iso: 'MX', name: 'Mexico', dial: '+52' },
|
|
372
|
+
{ iso: 'NL', name: 'Netherlands', dial: '+31' },
|
|
373
|
+
{ iso: 'NZ', name: 'New Zealand', dial: '+64' },
|
|
374
|
+
{ iso: 'NO', name: 'Norway', dial: '+47' },
|
|
375
|
+
{ iso: 'PL', name: 'Poland', dial: '+48' },
|
|
376
|
+
{ iso: 'PT', name: 'Portugal', dial: '+351' },
|
|
377
|
+
{ iso: 'SG', name: 'Singapore', dial: '+65' },
|
|
378
|
+
{ iso: 'ZA', name: 'South Africa', dial: '+27' },
|
|
379
|
+
{ iso: 'KR', name: 'South Korea', dial: '+82' },
|
|
380
|
+
{ iso: 'ES', name: 'Spain', dial: '+34' },
|
|
381
|
+
{ iso: 'SE', name: 'Sweden', dial: '+46' },
|
|
382
|
+
{ iso: 'CH', name: 'Switzerland', dial: '+41' },
|
|
383
|
+
{ iso: 'AE', name: 'United Arab Emirates', dial: '+971' },
|
|
384
|
+
{ iso: 'GB', name: 'United Kingdom', dial: '+44' },
|
|
385
|
+
{ iso: 'US', name: 'United States', dial: '+1' },
|
|
386
|
+
];
|
|
387
|
+
const PREFERRED_ISO_FOR_DIAL = {
|
|
388
|
+
'+1': 'US',
|
|
389
|
+
'+44': 'GB',
|
|
390
|
+
};
|
|
391
|
+
function defaultPhoneIso() {
|
|
392
|
+
try {
|
|
393
|
+
const region = new Intl.Locale(navigator.language).region;
|
|
394
|
+
if (region && PHONE_COUNTRIES.some((country) => country.iso === region)) {
|
|
395
|
+
return region;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
catch {
|
|
399
|
+
/* ignore */
|
|
400
|
+
}
|
|
401
|
+
return 'DK';
|
|
402
|
+
}
|
|
403
|
+
function phoneCountryByIso(iso) {
|
|
404
|
+
return PHONE_COUNTRIES.find((country) => country.iso === iso) ?? PHONE_COUNTRIES.find((country) => country.iso === 'DK');
|
|
405
|
+
}
|
|
406
|
+
function formatPhoneValue(dial, national) {
|
|
407
|
+
const digits = national.replace(/[^\d\s()-]/g, '').trim();
|
|
408
|
+
if (!digits)
|
|
409
|
+
return '';
|
|
410
|
+
return `${dial} ${digits}`;
|
|
411
|
+
}
|
|
412
|
+
function parsePhoneValue(value, fallbackIso = 'DK') {
|
|
413
|
+
const trimmed = value.trim();
|
|
414
|
+
if (!trimmed.startsWith('+')) {
|
|
415
|
+
return { iso: fallbackIso, national: trimmed };
|
|
416
|
+
}
|
|
417
|
+
const match = trimmed.match(/^(\+\d{1,4})(?:\s+(.*))?$/);
|
|
418
|
+
if (!match) {
|
|
419
|
+
return { iso: fallbackIso, national: trimmed };
|
|
420
|
+
}
|
|
421
|
+
const dial = match[1];
|
|
422
|
+
const national = (match[2] ?? '').trim();
|
|
423
|
+
const matches = PHONE_COUNTRIES.filter((country) => country.dial === dial);
|
|
424
|
+
if (matches.length === 0) {
|
|
425
|
+
return { iso: fallbackIso, national: trimmed.replace(/^\+\d{1,4}\s*/, '') };
|
|
426
|
+
}
|
|
427
|
+
const preferred = PREFERRED_ISO_FOR_DIAL[dial];
|
|
428
|
+
const country = matches.find((item) => item.iso === preferred) ?? matches[0];
|
|
429
|
+
return { iso: country.iso, national };
|
|
430
|
+
}
|
|
431
|
+
|
|
338
432
|
/** Build rows from flat fields (full = own row, half = paired in row) */
|
|
339
433
|
function buildRowsFromFields(fields) {
|
|
340
434
|
const rows = [];
|
|
@@ -370,6 +464,8 @@ const FormSnip = ({ formId, baseUrl = DEFAULT_BASE_URL, onSuccess, onError, clas
|
|
|
370
464
|
const [submitError, setSubmitError] = React.useState(null);
|
|
371
465
|
const [submitSuccess, setSubmitSuccess] = React.useState(false);
|
|
372
466
|
const [isSubmitting, setIsSubmitting] = React.useState(false);
|
|
467
|
+
const [submitToken, setSubmitToken] = React.useState('');
|
|
468
|
+
const [honeypot, setHoneypot] = React.useState('');
|
|
373
469
|
React.useEffect(() => {
|
|
374
470
|
// Inject base styles on mount (same as embed script)
|
|
375
471
|
injectBaseStyles();
|
|
@@ -384,6 +480,7 @@ const FormSnip = ({ formId, baseUrl = DEFAULT_BASE_URL, onSuccess, onError, clas
|
|
|
384
480
|
// Form data includes embedStyles from the database
|
|
385
481
|
// These styles are set in the EmbedForm page and stored in the Form model
|
|
386
482
|
setForm(json.data);
|
|
483
|
+
setSubmitToken(json.data.submitToken || '');
|
|
387
484
|
// Parse URL query once for prefill support
|
|
388
485
|
let urlParams = null;
|
|
389
486
|
if (typeof window !== 'undefined') {
|
|
@@ -396,22 +493,27 @@ const FormSnip = ({ formId, baseUrl = DEFAULT_BASE_URL, onSuccess, onError, clas
|
|
|
396
493
|
return acc;
|
|
397
494
|
}
|
|
398
495
|
let initialValue = '';
|
|
399
|
-
const prefillKey = field.
|
|
496
|
+
const prefillKey = field.id;
|
|
400
497
|
const canPrefillType = field.type === 'text' ||
|
|
401
498
|
field.type === 'email' ||
|
|
402
499
|
field.type === 'textarea' ||
|
|
403
500
|
field.type === 'number' ||
|
|
404
501
|
field.type === 'date' ||
|
|
405
502
|
field.type === 'datetime' ||
|
|
503
|
+
field.type === 'time' ||
|
|
406
504
|
field.type === 'phone' ||
|
|
505
|
+
field.type === 'website' ||
|
|
506
|
+
field.type === 'choice' ||
|
|
407
507
|
field.type === 'hidden' ||
|
|
408
508
|
field.type === 'select';
|
|
409
509
|
// 1) Prefill prop (takes precedence over URL)
|
|
410
510
|
if (prefill && prefillKey && prefillKey.trim() !== '' && prefillKey in prefill) {
|
|
411
511
|
const v = prefill[prefillKey];
|
|
412
512
|
if (typeof v === 'string' && canPrefillType) {
|
|
413
|
-
|
|
414
|
-
|
|
513
|
+
const options = field.options || [];
|
|
514
|
+
const optionField = field.type === 'select' || field.type === 'choice';
|
|
515
|
+
if (optionField && options.length > 0 && !options.includes(v)) {
|
|
516
|
+
// Ignore invalid option values
|
|
415
517
|
}
|
|
416
518
|
else {
|
|
417
519
|
initialValue = v;
|
|
@@ -423,8 +525,10 @@ const FormSnip = ({ formId, baseUrl = DEFAULT_BASE_URL, onSuccess, onError, clas
|
|
|
423
525
|
const v = urlParams.get(prefillKey);
|
|
424
526
|
if (v !== null && v !== '') {
|
|
425
527
|
if (canPrefillType) {
|
|
426
|
-
|
|
427
|
-
|
|
528
|
+
const options = field.options || [];
|
|
529
|
+
const optionField = field.type === 'select' || field.type === 'choice';
|
|
530
|
+
if (optionField && options.length > 0 && !options.includes(v)) {
|
|
531
|
+
// Ignore invalid option values
|
|
428
532
|
}
|
|
429
533
|
else {
|
|
430
534
|
initialValue = v;
|
|
@@ -477,7 +581,7 @@ const FormSnip = ({ formId, baseUrl = DEFAULT_BASE_URL, onSuccess, onError, clas
|
|
|
477
581
|
delete newErrors[fieldId];
|
|
478
582
|
return newErrors;
|
|
479
583
|
});
|
|
480
|
-
const result = await uploadFiles(files, formId, baseUrl);
|
|
584
|
+
const result = await uploadFiles(files, formId, baseUrl, submitToken);
|
|
481
585
|
setUploadingFiles((prev) => ({ ...prev, [fieldId]: false }));
|
|
482
586
|
if (result.success && result.fileKeys && result.fileKeys.length > 0) {
|
|
483
587
|
const existingFileKeys = Array.isArray(formData[fieldId])
|
|
@@ -501,6 +605,16 @@ const FormSnip = ({ formId, baseUrl = DEFAULT_BASE_URL, onSuccess, onError, clas
|
|
|
501
605
|
setSubmitSuccess(false);
|
|
502
606
|
setIsSubmitting(true);
|
|
503
607
|
try {
|
|
608
|
+
const pageUrl = new URL(window.location.href);
|
|
609
|
+
const payload = {
|
|
610
|
+
...formData,
|
|
611
|
+
[SUBMIT_TOKEN_FIELD]: submitToken,
|
|
612
|
+
[HONEYPOT_FIELD]: honeypot,
|
|
613
|
+
_fs_page: pageUrl.href,
|
|
614
|
+
_fs_utm_source: pageUrl.searchParams.get('utm_source') || undefined,
|
|
615
|
+
_fs_utm_medium: pageUrl.searchParams.get('utm_medium') || undefined,
|
|
616
|
+
_fs_utm_campaign: pageUrl.searchParams.get('utm_campaign') || undefined,
|
|
617
|
+
};
|
|
504
618
|
const response = await fetch(`${baseUrl}/submissions/create-submission/${formId}`, {
|
|
505
619
|
method: 'POST',
|
|
506
620
|
headers: {
|
|
@@ -508,7 +622,7 @@ const FormSnip = ({ formId, baseUrl = DEFAULT_BASE_URL, onSuccess, onError, clas
|
|
|
508
622
|
'X-Requested-With': 'XMLHttpRequest',
|
|
509
623
|
},
|
|
510
624
|
credentials: 'include',
|
|
511
|
-
body: JSON.stringify(
|
|
625
|
+
body: JSON.stringify(payload),
|
|
512
626
|
});
|
|
513
627
|
const json = await response.json();
|
|
514
628
|
if (response.ok && (json.success === undefined || json.success === true)) {
|
|
@@ -520,6 +634,11 @@ const FormSnip = ({ formId, baseUrl = DEFAULT_BASE_URL, onSuccess, onError, clas
|
|
|
520
634
|
if (onSuccess) {
|
|
521
635
|
onSuccess(json.data);
|
|
522
636
|
}
|
|
637
|
+
const redirectUrl = form?.validation?.redirectUrl?.trim();
|
|
638
|
+
if (redirectUrl && /^https?:\/\//i.test(redirectUrl)) {
|
|
639
|
+
window.location.assign(redirectUrl);
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
523
642
|
// Reset form
|
|
524
643
|
const emptyData = Object.keys(formData).reduce((acc, key) => {
|
|
525
644
|
const field = form?.fields.find((f) => f.id === key);
|
|
@@ -529,6 +648,15 @@ const FormSnip = ({ formId, baseUrl = DEFAULT_BASE_URL, onSuccess, onError, clas
|
|
|
529
648
|
setFormData(emptyData);
|
|
530
649
|
setSelectedFiles({});
|
|
531
650
|
setUploadErrors({});
|
|
651
|
+
setHoneypot('');
|
|
652
|
+
try {
|
|
653
|
+
const refresh = await fetch(`${baseUrl}/forms/get-form/${formId}`);
|
|
654
|
+
const refreshJson = await refresh.json();
|
|
655
|
+
if (refreshJson?.data?.submitToken) {
|
|
656
|
+
setSubmitToken(refreshJson.data.submitToken);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
catch (_) { }
|
|
532
660
|
}
|
|
533
661
|
else {
|
|
534
662
|
const errorMsg = json.message || 'Submission failed';
|
|
@@ -553,6 +681,7 @@ const FormSnip = ({ formId, baseUrl = DEFAULT_BASE_URL, onSuccess, onError, clas
|
|
|
553
681
|
// Use embedStyles from database (set in EmbedForm page)
|
|
554
682
|
const fieldStyles = form?.embedStyles?.field || {};
|
|
555
683
|
const isRequired = field.required === true;
|
|
684
|
+
const inputStyle = applyStyles({}, fieldStyles);
|
|
556
685
|
const commonProps = {
|
|
557
686
|
id: `field-${field.id}`,
|
|
558
687
|
name: field.id,
|
|
@@ -560,8 +689,8 @@ const FormSnip = ({ formId, baseUrl = DEFAULT_BASE_URL, onSuccess, onError, clas
|
|
|
560
689
|
onChange: (e) => handleInputChange(field.id, e.target.value),
|
|
561
690
|
placeholder: field.placeholder || '',
|
|
562
691
|
required: isRequired,
|
|
563
|
-
style:
|
|
564
|
-
className: field.type === 'textarea' ? CLASSES.textarea : CLASSES.input
|
|
692
|
+
style: inputStyle,
|
|
693
|
+
className: `${field.type === 'textarea' ? CLASSES.textarea : CLASSES.input} formsnip-embed-ph`,
|
|
565
694
|
};
|
|
566
695
|
const labelStyle = {};
|
|
567
696
|
if (fieldStyles.labelFontSize) {
|
|
@@ -570,18 +699,54 @@ const FormSnip = ({ formId, baseUrl = DEFAULT_BASE_URL, onSuccess, onError, clas
|
|
|
570
699
|
if (field.type === 'hidden') {
|
|
571
700
|
return (React.createElement("input", { type: "hidden", id: `field-${field.id}`, name: field.id, value: formData[field.id] || field.value || '' }));
|
|
572
701
|
}
|
|
702
|
+
if (field.type === 'heading') {
|
|
703
|
+
return React.createElement("h2", { style: { margin: 0, fontSize: '1.5rem', fontWeight: 600 } }, field.label);
|
|
704
|
+
}
|
|
705
|
+
if (field.type === 'paragraph') {
|
|
706
|
+
return React.createElement("p", { style: { margin: 0, whiteSpace: 'pre-wrap', color: '#4b5563' } }, field.content || '');
|
|
707
|
+
}
|
|
708
|
+
if (field.type === 'divider') {
|
|
709
|
+
return React.createElement("hr", { style: { border: 0, borderTop: '1px solid #e5e7eb', margin: '0.25rem 0' } });
|
|
710
|
+
}
|
|
711
|
+
if (field.type === 'choice') {
|
|
712
|
+
const options = field.options && field.options.length > 0 ? field.options : ['Yes', 'No'];
|
|
713
|
+
return (React.createElement("div", { className: "fs-relative" },
|
|
714
|
+
!field.hideLabel && (React.createElement("label", { className: "fs-mb-2 fs-flex fs-w-full fs-items-center fs-justify-between fs-gap-2 fs-text-sm fs-text-gray-700", style: { fontWeight: 500, ...labelStyle } },
|
|
715
|
+
React.createElement("span", { className: "fs-min-w-0 fs-flex-1" }, field.label),
|
|
716
|
+
isRequired && React.createElement("span", { className: "fs-text-red-500 fs-flex-shrink-0", style: { lineHeight: 1 } }, "*"))),
|
|
717
|
+
options.map((option) => (React.createElement("label", { key: option, className: "fs-flex fs-items-center", style: { marginTop: '0.4rem' } },
|
|
718
|
+
React.createElement("input", { type: "radio", name: field.id, value: option, checked: formData[field.id] === option, onChange: (event) => handleInputChange(field.id, event.target.value), required: isRequired }),
|
|
719
|
+
React.createElement("span", { style: { marginLeft: '0.5rem' } }, option))))));
|
|
720
|
+
}
|
|
721
|
+
if (field.type === 'phone') {
|
|
722
|
+
const parsed = parsePhoneValue(formData[field.id] || '', defaultPhoneIso());
|
|
723
|
+
const country = phoneCountryByIso(parsed.iso);
|
|
724
|
+
return (React.createElement(React.Fragment, null,
|
|
725
|
+
!field.hideLabel && (React.createElement("label", { htmlFor: `field-${field.id}`, className: "fs-mb-2 fs-flex fs-w-full fs-items-center fs-justify-between fs-gap-2 fs-text-sm fs-text-gray-700", style: { fontWeight: 500, ...labelStyle } },
|
|
726
|
+
React.createElement("span", { className: "fs-min-w-0 fs-flex-1" }, field.label),
|
|
727
|
+
isRequired && React.createElement("span", { className: "fs-text-red-500 fs-flex-shrink-0", style: { lineHeight: 1 } }, "*"))),
|
|
728
|
+
React.createElement("div", { style: { display: 'flex', gap: '0.5rem' } },
|
|
729
|
+
React.createElement("select", { "aria-label": "Country code", value: country.iso, onChange: (event) => {
|
|
730
|
+
const next = phoneCountryByIso(event.target.value);
|
|
731
|
+
handleInputChange(field.id, formatPhoneValue(next.dial, parsed.national));
|
|
732
|
+
}, className: CLASSES.input, style: { ...inputStyle, width: '8.5rem', flex: '0 0 auto' } }, PHONE_COUNTRIES.map((item) => (React.createElement("option", { key: item.iso, value: item.iso },
|
|
733
|
+
item.dial,
|
|
734
|
+
" ",
|
|
735
|
+
item.name)))),
|
|
736
|
+
React.createElement("input", { id: `field-${field.id}`, name: field.id, type: "tel", value: parsed.national, onChange: (event) => handleInputChange(field.id, formatPhoneValue(country.dial, event.target.value)), placeholder: field.placeholder || '12 34 56 78', required: isRequired, style: { ...inputStyle, flex: 1 }, className: `${CLASSES.input} formsnip-embed-ph` }))));
|
|
737
|
+
}
|
|
573
738
|
// For checkbox and radio, return without extra wrapper
|
|
574
739
|
if (field.type === 'checkbox') {
|
|
575
|
-
return (React.createElement("label", { className: "fs-flex fs-items-center" },
|
|
740
|
+
return (React.createElement("label", { className: "fs-flex fs-w-full fs-items-center fs-gap-2" },
|
|
576
741
|
React.createElement("input", { type: "checkbox", id: `field-${field.id}`, name: field.id, checked: String(formData[field.id]) === 'true' || formData[field.id] === 'on', onChange: (e) => handleInputChange(field.id, e.target.checked ? 'true' : 'false'), required: isRequired }),
|
|
577
|
-
React.createElement("span", { style: { marginLeft: '0.5rem' } }, field.label || field.placeholder || 'Check this box'),
|
|
578
|
-
isRequired && React.createElement("span", { className: "fs-text-red-500", style: {
|
|
742
|
+
!field.hideLabel && (React.createElement("span", { className: "fs-min-w-0 fs-flex-1", style: { marginLeft: '0.5rem' } }, field.label || field.placeholder || 'Check this box')),
|
|
743
|
+
isRequired && React.createElement("span", { className: "fs-text-red-500 fs-flex-shrink-0 fs-ml-auto", style: { lineHeight: 1 } }, "*")));
|
|
579
744
|
}
|
|
580
745
|
if (field.type === 'radio') {
|
|
581
746
|
return (React.createElement("div", { className: "fs-relative" },
|
|
582
|
-
React.createElement("label", { className: "fs-text-sm fs-text-gray-700
|
|
583
|
-
field.label,
|
|
584
|
-
isRequired && React.createElement("span", { className: "fs-text-red-500", style: {
|
|
747
|
+
!field.hideLabel && (React.createElement("label", { className: "fs-mb-2 fs-flex fs-w-full fs-items-center fs-justify-between fs-gap-2 fs-text-sm fs-text-gray-700", style: { fontWeight: 500 } },
|
|
748
|
+
React.createElement("span", { className: "fs-min-w-0 fs-flex-1" }, field.label),
|
|
749
|
+
isRequired && React.createElement("span", { className: "fs-text-red-500 fs-flex-shrink-0", style: { lineHeight: 1 } }, "*"))),
|
|
585
750
|
React.createElement("label", { className: "fs-flex fs-items-center" },
|
|
586
751
|
React.createElement("input", { type: "radio", id: `field-${field.id}-yes`, name: field.id, value: "yes", checked: formData[field.id] === 'yes', onChange: (e) => handleInputChange(field.id, e.target.value), required: isRequired }),
|
|
587
752
|
React.createElement("span", { style: { marginLeft: '0.5rem' } }, " Yes")),
|
|
@@ -596,19 +761,19 @@ const FormSnip = ({ formId, baseUrl = DEFAULT_BASE_URL, onSuccess, onError, clas
|
|
|
596
761
|
? [formData[field.id]]
|
|
597
762
|
: [];
|
|
598
763
|
return (React.createElement(React.Fragment, null,
|
|
599
|
-
React.createElement("label", { htmlFor: `field-${field.id}`, className: "fs-text-sm fs-text-gray-700
|
|
600
|
-
field.label,
|
|
601
|
-
isRequired && React.createElement("span", { className: "fs-text-red-500", style: {
|
|
764
|
+
!field.hideLabel && (React.createElement("label", { htmlFor: `field-${field.id}`, className: "fs-mb-2 fs-flex fs-w-full fs-items-center fs-justify-between fs-gap-2 fs-text-sm fs-text-gray-700", style: { fontWeight: 500 } },
|
|
765
|
+
React.createElement("span", { className: "fs-min-w-0 fs-flex-1" }, field.label),
|
|
766
|
+
isRequired && React.createElement("span", { className: "fs-text-red-500 fs-flex-shrink-0", style: { lineHeight: 1 } }, "*"))),
|
|
602
767
|
React.createElement(FileUpload, { id: field.id, required: isRequired, disabled: uploadingFiles[field.id], multiple: true, onFilesChange: (files) => handleFilesChange(field.id, files), uploadedFiles: uploadedFileKeys, uploading: uploadingFiles[field.id], error: uploadErrors[field.id] })));
|
|
603
768
|
}
|
|
604
769
|
// For other fields, use minimal wrapper structure
|
|
605
770
|
return (React.createElement(React.Fragment, null,
|
|
606
|
-
React.createElement("label", { htmlFor: `field-${field.id}`, className: "fs-text-sm fs-text-gray-700
|
|
607
|
-
field.label,
|
|
608
|
-
isRequired && React.createElement("span", { className: "fs-text-red-500", style: {
|
|
609
|
-
React.createElement("div", { className: CLASSES.inner }, field.type === 'select' ? (React.createElement("select", { id: `field-${field.id}`, name: field.id, value: formData[field.id] || '', onChange: (e) => handleInputChange(field.id, e.target.value), required: isRequired, style:
|
|
771
|
+
!field.hideLabel && (React.createElement("label", { htmlFor: `field-${field.id}`, className: "fs-mb-2 fs-flex fs-w-full fs-items-center fs-justify-between fs-gap-2 fs-text-sm fs-text-gray-700", style: { fontWeight: 500, ...labelStyle } },
|
|
772
|
+
React.createElement("span", { className: "fs-min-w-0 fs-flex-1" }, field.label),
|
|
773
|
+
isRequired && React.createElement("span", { className: "fs-text-red-500 fs-flex-shrink-0", style: { lineHeight: 1 } }, "*"))),
|
|
774
|
+
React.createElement("div", { className: CLASSES.inner }, field.type === 'select' ? (React.createElement("select", { id: `field-${field.id}`, name: field.id, value: formData[field.id] || '', onChange: (e) => handleInputChange(field.id, e.target.value), required: isRequired, style: inputStyle, className: `${CLASSES.input} formsnip-embed-ph` },
|
|
610
775
|
React.createElement("option", { value: "" }, field.placeholder || 'Select an option'),
|
|
611
|
-
field.options?.map((option, index) => (React.createElement("option", { key: index, value: option }, option))))) : field.type === 'textarea' ? (React.createElement("textarea", { ...commonProps, rows: 4 })) : field.type === 'phone' ? (React.createElement("input", { type: "tel", ...commonProps })) : field.type === 'datetime' ? (React.createElement("input", { type: "datetime-local", ...commonProps })) : (React.createElement("input", { type: field.type || 'text', ...commonProps })))));
|
|
776
|
+
field.options?.map((option, index) => (React.createElement("option", { key: index, value: option }, option))))) : field.type === 'textarea' ? (React.createElement("textarea", { ...commonProps, rows: 4 })) : field.type === 'phone' ? (React.createElement("input", { type: "tel", ...commonProps })) : field.type === 'website' ? (React.createElement("input", { type: "url", ...commonProps })) : field.type === 'time' ? (React.createElement("input", { type: "time", ...commonProps })) : field.type === 'datetime' ? (React.createElement("input", { type: "datetime-local", ...commonProps })) : (React.createElement("input", { type: field.type || 'text', ...commonProps })))));
|
|
612
777
|
};
|
|
613
778
|
if (loading) {
|
|
614
779
|
const content = (React.createElement("div", { className: "fs-text-center", style: { padding: '2rem 0' } },
|
|
@@ -637,6 +802,14 @@ const FormSnip = ({ formId, baseUrl = DEFAULT_BASE_URL, onSuccess, onError, clas
|
|
|
637
802
|
marginBottom: '1rem',
|
|
638
803
|
} }, form.validation?.confirmationText || 'Thank you! Your submission has been received.')),
|
|
639
804
|
React.createElement("form", { id: "formsnip-form", onSubmit: handleSubmit, className: CLASSES.form },
|
|
805
|
+
React.createElement("input", { type: "text", name: HONEYPOT_FIELD, value: honeypot, onChange: (e) => setHoneypot(e.target.value), tabIndex: -1, autoComplete: "off", "aria-hidden": "true", style: {
|
|
806
|
+
position: 'absolute',
|
|
807
|
+
left: '-9999px',
|
|
808
|
+
height: 0,
|
|
809
|
+
width: 0,
|
|
810
|
+
opacity: 0,
|
|
811
|
+
overflow: 'hidden',
|
|
812
|
+
} }),
|
|
640
813
|
form.fields.filter((f) => f.isActive !== false && f.type === 'hidden').map((field) => (React.createElement(React.Fragment, { key: field.id }, renderField(field)))),
|
|
641
814
|
buildRowsFromFields(form.fields.filter((f) => f.isActive !== false && f.type !== 'hidden')).map((row, rowIdx) => row.length === 1 ? (React.createElement("div", { key: row[0].id, style: { display: 'flex', flexDirection: 'column' } }, renderField(row[0]))) : (React.createElement("div", { key: `row-${rowIdx}`, style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1.25rem' } }, row.map((field) => (React.createElement("div", { key: field.id, style: { display: 'flex', flexDirection: 'column' } }, renderField(field))))))),
|
|
642
815
|
React.createElement("button", { type: "submit", disabled: isSubmitting, className: CLASSES.button, style: {
|