@formsnip/embed 1.1.0 → 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/README.md +1 -9
- package/dist/FormSnip.d.ts.map +1 -1
- package/dist/index.d.ts +19 -18
- package/dist/index.esm.js +214 -53
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +214 -53
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +13 -11
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/dataLayer.d.ts +2 -3
- package/dist/utils/dataLayer.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;
|
|
@@ -314,17 +328,18 @@ const CLASSES = {
|
|
|
314
328
|
};
|
|
315
329
|
|
|
316
330
|
/**
|
|
317
|
-
* Push a GTM-friendly event to
|
|
318
|
-
* Safe no-op
|
|
331
|
+
* Push a GTM-friendly event to the global dataLayer after a successful submission.
|
|
332
|
+
* Uses `globalThis` so it works in typical browser bundles. Safe no-op if push throws.
|
|
319
333
|
*/
|
|
320
334
|
function pushFormSnipSubmissionDataLayer(options) {
|
|
321
|
-
if (typeof window === 'undefined')
|
|
322
|
-
return;
|
|
323
335
|
try {
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
336
|
+
if (typeof globalThis === 'undefined')
|
|
337
|
+
return;
|
|
338
|
+
const g = globalThis;
|
|
339
|
+
const layer = Array.isArray(g.dataLayer) ? g.dataLayer : [];
|
|
340
|
+
g.dataLayer = layer;
|
|
341
|
+
layer.push({
|
|
342
|
+
event: 'formsnip_submission_success',
|
|
328
343
|
formsnipFormId: options.formId,
|
|
329
344
|
...(options.formName ? { formsnipFormName: options.formName } : {}),
|
|
330
345
|
});
|
|
@@ -334,6 +349,86 @@ function pushFormSnipSubmissionDataLayer(options) {
|
|
|
334
349
|
}
|
|
335
350
|
}
|
|
336
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
|
+
|
|
337
432
|
/** Build rows from flat fields (full = own row, half = paired in row) */
|
|
338
433
|
function buildRowsFromFields(fields) {
|
|
339
434
|
const rows = [];
|
|
@@ -358,7 +453,7 @@ function buildRowsFromFields(fields) {
|
|
|
358
453
|
return rows;
|
|
359
454
|
}
|
|
360
455
|
const DEFAULT_BASE_URL = 'https://api.formsnip.com';
|
|
361
|
-
const FormSnip = ({ formId, baseUrl = DEFAULT_BASE_URL, onSuccess, onError, className = '', prefill,
|
|
456
|
+
const FormSnip = ({ formId, baseUrl = DEFAULT_BASE_URL, onSuccess, onError, className = '', prefill, }) => {
|
|
362
457
|
const [form, setForm] = React.useState(null);
|
|
363
458
|
const [formData, setFormData] = React.useState({});
|
|
364
459
|
const [selectedFiles, setSelectedFiles] = React.useState({});
|
|
@@ -369,6 +464,8 @@ const FormSnip = ({ formId, baseUrl = DEFAULT_BASE_URL, onSuccess, onError, clas
|
|
|
369
464
|
const [submitError, setSubmitError] = React.useState(null);
|
|
370
465
|
const [submitSuccess, setSubmitSuccess] = React.useState(false);
|
|
371
466
|
const [isSubmitting, setIsSubmitting] = React.useState(false);
|
|
467
|
+
const [submitToken, setSubmitToken] = React.useState('');
|
|
468
|
+
const [honeypot, setHoneypot] = React.useState('');
|
|
372
469
|
React.useEffect(() => {
|
|
373
470
|
// Inject base styles on mount (same as embed script)
|
|
374
471
|
injectBaseStyles();
|
|
@@ -383,6 +480,7 @@ const FormSnip = ({ formId, baseUrl = DEFAULT_BASE_URL, onSuccess, onError, clas
|
|
|
383
480
|
// Form data includes embedStyles from the database
|
|
384
481
|
// These styles are set in the EmbedForm page and stored in the Form model
|
|
385
482
|
setForm(json.data);
|
|
483
|
+
setSubmitToken(json.data.submitToken || '');
|
|
386
484
|
// Parse URL query once for prefill support
|
|
387
485
|
let urlParams = null;
|
|
388
486
|
if (typeof window !== 'undefined') {
|
|
@@ -395,22 +493,27 @@ const FormSnip = ({ formId, baseUrl = DEFAULT_BASE_URL, onSuccess, onError, clas
|
|
|
395
493
|
return acc;
|
|
396
494
|
}
|
|
397
495
|
let initialValue = '';
|
|
398
|
-
const prefillKey = field.
|
|
496
|
+
const prefillKey = field.id;
|
|
399
497
|
const canPrefillType = field.type === 'text' ||
|
|
400
498
|
field.type === 'email' ||
|
|
401
499
|
field.type === 'textarea' ||
|
|
402
500
|
field.type === 'number' ||
|
|
403
501
|
field.type === 'date' ||
|
|
404
502
|
field.type === 'datetime' ||
|
|
503
|
+
field.type === 'time' ||
|
|
405
504
|
field.type === 'phone' ||
|
|
505
|
+
field.type === 'website' ||
|
|
506
|
+
field.type === 'choice' ||
|
|
406
507
|
field.type === 'hidden' ||
|
|
407
508
|
field.type === 'select';
|
|
408
509
|
// 1) Prefill prop (takes precedence over URL)
|
|
409
510
|
if (prefill && prefillKey && prefillKey.trim() !== '' && prefillKey in prefill) {
|
|
410
511
|
const v = prefill[prefillKey];
|
|
411
512
|
if (typeof v === 'string' && canPrefillType) {
|
|
412
|
-
|
|
413
|
-
|
|
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
|
|
414
517
|
}
|
|
415
518
|
else {
|
|
416
519
|
initialValue = v;
|
|
@@ -422,8 +525,10 @@ const FormSnip = ({ formId, baseUrl = DEFAULT_BASE_URL, onSuccess, onError, clas
|
|
|
422
525
|
const v = urlParams.get(prefillKey);
|
|
423
526
|
if (v !== null && v !== '') {
|
|
424
527
|
if (canPrefillType) {
|
|
425
|
-
|
|
426
|
-
|
|
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
|
|
427
532
|
}
|
|
428
533
|
else {
|
|
429
534
|
initialValue = v;
|
|
@@ -431,16 +536,6 @@ const FormSnip = ({ formId, baseUrl = DEFAULT_BASE_URL, onSuccess, onError, clas
|
|
|
431
536
|
}
|
|
432
537
|
}
|
|
433
538
|
}
|
|
434
|
-
// Debugging: log resolved prefill for hidden fields
|
|
435
|
-
if (typeof window !== 'undefined' && field.type === 'hidden') {
|
|
436
|
-
// eslint-disable-next-line no-console
|
|
437
|
-
console.debug('FormSnip hidden prefill', {
|
|
438
|
-
fieldId: field.id,
|
|
439
|
-
prefillKey,
|
|
440
|
-
resolvedValue: initialValue,
|
|
441
|
-
hasDefault: !!field.value,
|
|
442
|
-
});
|
|
443
|
-
}
|
|
444
539
|
// Hidden fields can also have a static default value from the schema
|
|
445
540
|
if (initialValue === '' && field.type === 'hidden' && field.value) {
|
|
446
541
|
initialValue = field.value;
|
|
@@ -486,7 +581,7 @@ const FormSnip = ({ formId, baseUrl = DEFAULT_BASE_URL, onSuccess, onError, clas
|
|
|
486
581
|
delete newErrors[fieldId];
|
|
487
582
|
return newErrors;
|
|
488
583
|
});
|
|
489
|
-
const result = await uploadFiles(files, formId, baseUrl);
|
|
584
|
+
const result = await uploadFiles(files, formId, baseUrl, submitToken);
|
|
490
585
|
setUploadingFiles((prev) => ({ ...prev, [fieldId]: false }));
|
|
491
586
|
if (result.success && result.fileKeys && result.fileKeys.length > 0) {
|
|
492
587
|
const existingFileKeys = Array.isArray(formData[fieldId])
|
|
@@ -510,6 +605,16 @@ const FormSnip = ({ formId, baseUrl = DEFAULT_BASE_URL, onSuccess, onError, clas
|
|
|
510
605
|
setSubmitSuccess(false);
|
|
511
606
|
setIsSubmitting(true);
|
|
512
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
|
+
};
|
|
513
618
|
const response = await fetch(`${baseUrl}/submissions/create-submission/${formId}`, {
|
|
514
619
|
method: 'POST',
|
|
515
620
|
headers: {
|
|
@@ -517,21 +622,23 @@ const FormSnip = ({ formId, baseUrl = DEFAULT_BASE_URL, onSuccess, onError, clas
|
|
|
517
622
|
'X-Requested-With': 'XMLHttpRequest',
|
|
518
623
|
},
|
|
519
624
|
credentials: 'include',
|
|
520
|
-
body: JSON.stringify(
|
|
625
|
+
body: JSON.stringify(payload),
|
|
521
626
|
});
|
|
522
627
|
const json = await response.json();
|
|
523
628
|
if (response.ok && (json.success === undefined || json.success === true)) {
|
|
524
629
|
setSubmitSuccess(true);
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
formName: form?.name,
|
|
530
|
-
});
|
|
531
|
-
}
|
|
630
|
+
pushFormSnipSubmissionDataLayer({
|
|
631
|
+
formId,
|
|
632
|
+
formName: form?.name,
|
|
633
|
+
});
|
|
532
634
|
if (onSuccess) {
|
|
533
635
|
onSuccess(json.data);
|
|
534
636
|
}
|
|
637
|
+
const redirectUrl = form?.validation?.redirectUrl?.trim();
|
|
638
|
+
if (redirectUrl && /^https?:\/\//i.test(redirectUrl)) {
|
|
639
|
+
window.location.assign(redirectUrl);
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
535
642
|
// Reset form
|
|
536
643
|
const emptyData = Object.keys(formData).reduce((acc, key) => {
|
|
537
644
|
const field = form?.fields.find((f) => f.id === key);
|
|
@@ -541,6 +648,15 @@ const FormSnip = ({ formId, baseUrl = DEFAULT_BASE_URL, onSuccess, onError, clas
|
|
|
541
648
|
setFormData(emptyData);
|
|
542
649
|
setSelectedFiles({});
|
|
543
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 (_) { }
|
|
544
660
|
}
|
|
545
661
|
else {
|
|
546
662
|
const errorMsg = json.message || 'Submission failed';
|
|
@@ -565,6 +681,7 @@ const FormSnip = ({ formId, baseUrl = DEFAULT_BASE_URL, onSuccess, onError, clas
|
|
|
565
681
|
// Use embedStyles from database (set in EmbedForm page)
|
|
566
682
|
const fieldStyles = form?.embedStyles?.field || {};
|
|
567
683
|
const isRequired = field.required === true;
|
|
684
|
+
const inputStyle = applyStyles({}, fieldStyles);
|
|
568
685
|
const commonProps = {
|
|
569
686
|
id: `field-${field.id}`,
|
|
570
687
|
name: field.id,
|
|
@@ -572,8 +689,8 @@ const FormSnip = ({ formId, baseUrl = DEFAULT_BASE_URL, onSuccess, onError, clas
|
|
|
572
689
|
onChange: (e) => handleInputChange(field.id, e.target.value),
|
|
573
690
|
placeholder: field.placeholder || '',
|
|
574
691
|
required: isRequired,
|
|
575
|
-
style:
|
|
576
|
-
className: field.type === 'textarea' ? CLASSES.textarea : CLASSES.input
|
|
692
|
+
style: inputStyle,
|
|
693
|
+
className: `${field.type === 'textarea' ? CLASSES.textarea : CLASSES.input} formsnip-embed-ph`,
|
|
577
694
|
};
|
|
578
695
|
const labelStyle = {};
|
|
579
696
|
if (fieldStyles.labelFontSize) {
|
|
@@ -582,18 +699,54 @@ const FormSnip = ({ formId, baseUrl = DEFAULT_BASE_URL, onSuccess, onError, clas
|
|
|
582
699
|
if (field.type === 'hidden') {
|
|
583
700
|
return (React.createElement("input", { type: "hidden", id: `field-${field.id}`, name: field.id, value: formData[field.id] || field.value || '' }));
|
|
584
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
|
+
}
|
|
585
738
|
// For checkbox and radio, return without extra wrapper
|
|
586
739
|
if (field.type === 'checkbox') {
|
|
587
|
-
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" },
|
|
588
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 }),
|
|
589
|
-
React.createElement("span", { style: { marginLeft: '0.5rem' } }, field.label || field.placeholder || 'Check this box'),
|
|
590
|
-
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 } }, "*")));
|
|
591
744
|
}
|
|
592
745
|
if (field.type === 'radio') {
|
|
593
746
|
return (React.createElement("div", { className: "fs-relative" },
|
|
594
|
-
React.createElement("label", { className: "fs-text-sm fs-text-gray-700
|
|
595
|
-
field.label,
|
|
596
|
-
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 } }, "*"))),
|
|
597
750
|
React.createElement("label", { className: "fs-flex fs-items-center" },
|
|
598
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 }),
|
|
599
752
|
React.createElement("span", { style: { marginLeft: '0.5rem' } }, " Yes")),
|
|
@@ -608,19 +761,19 @@ const FormSnip = ({ formId, baseUrl = DEFAULT_BASE_URL, onSuccess, onError, clas
|
|
|
608
761
|
? [formData[field.id]]
|
|
609
762
|
: [];
|
|
610
763
|
return (React.createElement(React.Fragment, null,
|
|
611
|
-
React.createElement("label", { htmlFor: `field-${field.id}`, className: "fs-text-sm fs-text-gray-700
|
|
612
|
-
field.label,
|
|
613
|
-
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 } }, "*"))),
|
|
614
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] })));
|
|
615
768
|
}
|
|
616
769
|
// For other fields, use minimal wrapper structure
|
|
617
770
|
return (React.createElement(React.Fragment, null,
|
|
618
|
-
React.createElement("label", { htmlFor: `field-${field.id}`, className: "fs-text-sm fs-text-gray-700
|
|
619
|
-
field.label,
|
|
620
|
-
isRequired && React.createElement("span", { className: "fs-text-red-500", style: {
|
|
621
|
-
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` },
|
|
622
775
|
React.createElement("option", { value: "" }, field.placeholder || 'Select an option'),
|
|
623
|
-
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 })))));
|
|
624
777
|
};
|
|
625
778
|
if (loading) {
|
|
626
779
|
const content = (React.createElement("div", { className: "fs-text-center", style: { padding: '2rem 0' } },
|
|
@@ -649,6 +802,14 @@ const FormSnip = ({ formId, baseUrl = DEFAULT_BASE_URL, onSuccess, onError, clas
|
|
|
649
802
|
marginBottom: '1rem',
|
|
650
803
|
} }, form.validation?.confirmationText || 'Thank you! Your submission has been received.')),
|
|
651
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
|
+
} }),
|
|
652
813
|
form.fields.filter((f) => f.isActive !== false && f.type === 'hidden').map((field) => (React.createElement(React.Fragment, { key: field.id }, renderField(field)))),
|
|
653
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))))))),
|
|
654
815
|
React.createElement("button", { type: "submit", disabled: isSubmitting, className: CLASSES.button, style: {
|