@hortonstudio/main 1.7.13 → 1.7.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/autoInit/form.js +46 -603
  2. package/index.js +0 -1
  3. package/package.json +1 -1
  4. package/utils/css-animations/buttons/main/bgbasic/btn-main-bgbasic.html +10 -0
  5. package/utils/css-animations/buttons/main/bgfill/btn-main-bgfill.html +29 -0
  6. package/utils/css-animations/buttons/navbar/bgbasic/navbar-main-bgbasic.html +17 -0
  7. package/utils/css-animations/buttons/navbar/bgbasic/navbar-menu-bgbasic.html +16 -0
  8. package/utils/css-animations/buttons/navbar/bgfill/navbar-main-bgfill.html +46 -0
  9. package/utils/css-animations/buttons/navbar/bgfill/navbar-menu-bgfill.html +39 -0
  10. package/utils/css-animations/buttons/navbar/color/navbar-announce-color.html +5 -0
  11. package/utils/css-animations/buttons/navbar/color/navbar-main-color.html +7 -0
  12. package/utils/css-animations/buttons/navbar/color/navbar-menu-color.html +7 -0
  13. package/utils/css-animations/buttons/navbar/double-slide/navbar-announce-double-slide.html +40 -0
  14. package/utils/css-animations/buttons/navbar/double-slide/navbar-main-double-slide.html +77 -0
  15. package/utils/css-animations/buttons/navbar/scale/navbar-announce-scale.html +6 -0
  16. package/utils/css-animations/buttons/navbar/scale/navbar-main-scale.html +9 -0
  17. package/utils/css-animations/buttons/navbar/scale/navbar-menu-scale.html +8 -0
  18. package/utils/css-animations/buttons/navbar/underline/navbar-announce-underline.html +32 -0
  19. package/utils/css-animations/buttons/navbar/underline/navbar-main-underline.html +56 -0
  20. package/utils/css-animations/buttons/text/color/text-footer-color.html +5 -0
  21. package/utils/css-animations/buttons/text/color/text-main-color.html +5 -0
  22. package/utils/css-animations/buttons/text/double-slide/text-main-double-slide.html +56 -0
  23. package/utils/css-animations/buttons/text/scale/text-footer-scale.html +6 -0
  24. package/utils/css-animations/buttons/text/scale/text-main-scale.html +6 -0
  25. package/utils/css-animations/buttons/text/underline/text-footer-underline.html +45 -0
  26. package/utils/css-animations/buttons/text/underline/text-main-underline.html +58 -0
  27. package/utils/css-animations/cards/card-clickable.html +11 -0
  28. package/utils/css-animations/defaults.html +69 -0
package/autoInit/form.js CHANGED
@@ -1,32 +1,4 @@
1
1
  export function init() {
2
- const config = {
3
- selectors: {
4
- form: 'form',
5
- errorTemplate: '[data-hs-form="form-error"]',
6
- requiredStep: '[data-hs-form="required-step"]',
7
- nextButton: '[data-hs-form="next-button"]',
8
- nextAnim: '[data-hs-form="next-anim"]',
9
- finalAnim: '[data-hs-form="final-anim"]'
10
- },
11
- errorMessages: {
12
- email: 'Please enter a valid email address',
13
- tel: 'Please enter a valid phone number',
14
- number: 'Please enter a valid number',
15
- url: 'Please enter a valid URL',
16
- text: 'This field is required',
17
- textarea: 'This field is required',
18
- select: 'Please select an option',
19
- checkbox: 'Please check this box',
20
- radio: 'Please select an option',
21
- default: 'This field is required'
22
- }
23
- };
24
-
25
- const activeErrors = new Map();
26
- let errorTemplate = null;
27
- let originalErrorTemplate = null;
28
- let isValidating = false;
29
-
30
2
  // Simple Custom Select Component for Webflow
31
3
  (function() {
32
4
  'use strict';
@@ -43,7 +15,7 @@ export function init() {
43
15
  });
44
16
 
45
17
  const selectWrappers = document.querySelectorAll('[data-hs-form="select"]');
46
-
18
+
47
19
  selectWrappers.forEach(wrapper => {
48
20
  initSingleSelect(wrapper);
49
21
  });
@@ -54,36 +26,36 @@ export function init() {
54
26
  // Find all required elements
55
27
  const realSelect = wrapper.querySelector('select');
56
28
  if (!realSelect) return;
57
-
29
+
58
30
  const selectName = realSelect.getAttribute('name') || 'custom-select';
59
31
  const customList = wrapper.querySelector('[data-hs-form="select-list"]');
60
32
  const button = wrapper.querySelector('button') || wrapper.querySelector('[role="button"]');
61
-
33
+
62
34
  if (!customList || !button) return;
63
-
35
+
64
36
  // Get and clone the option template
65
37
  const optionTemplate = customList.firstElementChild;
66
38
  if (!optionTemplate) return;
67
-
39
+
68
40
  const templateClone = optionTemplate.cloneNode(true);
69
41
  optionTemplate.remove();
70
-
42
+
71
43
  // Build options from real select
72
44
  const realOptions = realSelect.querySelectorAll('option');
73
45
  realOptions.forEach((option, index) => {
74
46
  const optionElement = templateClone.cloneNode(true);
75
47
  const textSpan = optionElement.querySelector('span');
76
-
48
+
77
49
  if (textSpan) {
78
50
  textSpan.textContent = option.textContent;
79
51
  }
80
-
52
+
81
53
  // Add attributes
82
54
  optionElement.setAttribute('data-value', option.value);
83
55
  optionElement.setAttribute('role', 'option');
84
56
  optionElement.setAttribute('id', `${selectName}-option-${index}`);
85
57
  optionElement.setAttribute('tabindex', '-1');
86
-
58
+
87
59
  // Set selected state if this option is selected
88
60
  if (option.selected) {
89
61
  optionElement.setAttribute('aria-selected', 'true');
@@ -95,22 +67,22 @@ export function init() {
95
67
  } else {
96
68
  optionElement.setAttribute('aria-selected', 'false');
97
69
  }
98
-
70
+
99
71
  customList.appendChild(optionElement);
100
72
  });
101
-
73
+
102
74
  // Add ARIA attributes
103
75
  customList.setAttribute('role', 'listbox');
104
76
  customList.setAttribute('id', `${selectName}-listbox`);
105
-
77
+
106
78
  button.setAttribute('role', 'combobox');
107
79
  button.setAttribute('aria-haspopup', 'listbox');
108
80
  button.setAttribute('aria-controls', `${selectName}-listbox`);
109
81
  button.setAttribute('aria-expanded', 'false');
110
82
  button.setAttribute('id', `${selectName}-button`);
111
-
83
+
112
84
  // Find and connect label if exists
113
- const label = wrapper.querySelector('label') ||
85
+ const label = wrapper.querySelector('label') ||
114
86
  document.querySelector(`label[for="${realSelect.id}"]`);
115
87
  if (label) {
116
88
  const labelId = label.id || `${selectName}-label`;
@@ -122,28 +94,28 @@ export function init() {
122
94
  label.setAttribute('for', realSelect.id);
123
95
  button.setAttribute('aria-labelledby', labelId);
124
96
  }
125
-
97
+
126
98
  // Track state
127
99
  let currentIndex = -1;
128
100
  let isOpen = false;
129
-
101
+
130
102
  // Update expanded state
131
103
  function updateExpandedState(expanded) {
132
104
  isOpen = expanded;
133
105
  button.setAttribute('aria-expanded', expanded.toString());
134
106
  }
135
-
107
+
136
108
  // Focus option by index
137
109
  function focusOption(index) {
138
110
  const options = customList.querySelectorAll('[role="option"]');
139
111
  if (index < 0 || index >= options.length) return;
140
-
112
+
141
113
  // Remove previous focus
142
114
  options.forEach(opt => {
143
115
  opt.classList.remove('focused');
144
116
  opt.setAttribute('tabindex', '-1');
145
117
  });
146
-
118
+
147
119
  // Add new focus
148
120
  currentIndex = index;
149
121
  options[index].classList.add('focused');
@@ -151,32 +123,32 @@ export function init() {
151
123
  options[index].focus();
152
124
  button.setAttribute('aria-activedescendant', options[index].id);
153
125
  }
154
-
126
+
155
127
  // Select option
156
128
  function selectOption(optionElement) {
157
129
  const value = optionElement.getAttribute('data-value');
158
130
  const text = optionElement.querySelector('span')?.textContent || optionElement.textContent;
159
-
131
+
160
132
  // Update real select
161
133
  realSelect.value = value;
162
134
  realSelect.dispatchEvent(new Event('change', { bubbles: true }));
163
-
135
+
164
136
  // Update button text
165
137
  const buttonText = button.querySelector('span') || button;
166
138
  if (buttonText.tagName === 'SPAN') {
167
139
  buttonText.textContent = text;
168
140
  }
169
-
141
+
170
142
  // Update aria-selected
171
143
  customList.querySelectorAll('[role="option"]').forEach(opt => {
172
144
  opt.setAttribute('aria-selected', 'false');
173
145
  });
174
146
  optionElement.setAttribute('aria-selected', 'true');
175
-
147
+
176
148
  // Click the button to close
177
149
  button.click();
178
150
  }
179
-
151
+
180
152
  // Button keyboard events
181
153
  button.addEventListener('keydown', (e) => {
182
154
  switch(e.key) {
@@ -185,7 +157,7 @@ export function init() {
185
157
  e.preventDefault();
186
158
  button.click();
187
159
  break;
188
-
160
+
189
161
  case 'ArrowDown':
190
162
  e.preventDefault();
191
163
  if (!isOpen) {
@@ -194,7 +166,7 @@ export function init() {
194
166
  focusOption(0);
195
167
  }
196
168
  break;
197
-
169
+
198
170
  case 'ArrowUp':
199
171
  e.preventDefault();
200
172
  if (isOpen) {
@@ -202,7 +174,7 @@ export function init() {
202
174
  focusOption(options.length - 1);
203
175
  }
204
176
  break;
205
-
177
+
206
178
  case 'Escape':
207
179
  if (isOpen) {
208
180
  e.preventDefault();
@@ -211,15 +183,15 @@ export function init() {
211
183
  break;
212
184
  }
213
185
  });
214
-
186
+
215
187
  // Option keyboard events (delegated)
216
188
  customList.addEventListener('keydown', (e) => {
217
189
  const option = e.target.closest('[role="option"]');
218
190
  if (!option) return;
219
-
191
+
220
192
  const options = Array.from(customList.querySelectorAll('[role="option"]'));
221
193
  const currentIdx = options.indexOf(option);
222
-
194
+
223
195
  switch(e.key) {
224
196
  case 'ArrowDown':
225
197
  e.preventDefault();
@@ -227,7 +199,7 @@ export function init() {
227
199
  focusOption(currentIdx + 1);
228
200
  }
229
201
  break;
230
-
202
+
231
203
  case 'ArrowUp':
232
204
  e.preventDefault();
233
205
  if (currentIdx === 0) {
@@ -237,13 +209,13 @@ export function init() {
237
209
  focusOption(currentIdx - 1);
238
210
  }
239
211
  break;
240
-
212
+
241
213
  case 'Enter':
242
214
  case ' ':
243
215
  e.preventDefault();
244
216
  selectOption(option);
245
217
  break;
246
-
218
+
247
219
  case 'Escape':
248
220
  e.preventDefault();
249
221
  button.click();
@@ -251,7 +223,7 @@ export function init() {
251
223
  break;
252
224
  }
253
225
  });
254
-
226
+
255
227
  // Option click events
256
228
  customList.addEventListener('click', (e) => {
257
229
  const option = e.target.closest('[role="option"]');
@@ -259,18 +231,18 @@ export function init() {
259
231
  selectOption(option);
260
232
  }
261
233
  });
262
-
234
+
263
235
  // Track open/close state
264
236
  const observer = new MutationObserver((mutations) => {
265
237
  mutations.forEach((mutation) => {
266
238
  if (mutation.type === 'attributes') {
267
239
  // Check if dropdown is visible
268
- const isVisible = !customList.hidden &&
240
+ const isVisible = !customList.hidden &&
269
241
  customList.style.display !== 'none' &&
270
242
  !customList.classList.contains('hidden');
271
-
243
+
272
244
  updateExpandedState(isVisible);
273
-
245
+
274
246
  if (!isVisible) {
275
247
  currentIndex = -1;
276
248
  button.removeAttribute('aria-activedescendant');
@@ -282,13 +254,13 @@ export function init() {
282
254
  }
283
255
  });
284
256
  });
285
-
257
+
286
258
  // Observe the custom list for visibility changes
287
259
  observer.observe(customList, {
288
260
  attributes: true,
289
261
  attributeFilter: ['hidden', 'style', 'class']
290
262
  });
291
-
263
+
292
264
  // Sync with real select changes
293
265
  realSelect.addEventListener('change', () => {
294
266
  const selectedOption = realSelect.options[realSelect.selectedIndex];
@@ -301,7 +273,7 @@ export function init() {
301
273
  if (buttonText.tagName === 'SPAN') {
302
274
  buttonText.textContent = text;
303
275
  }
304
-
276
+
305
277
  // Update aria-selected
306
278
  customList.querySelectorAll('[role="option"]').forEach(opt => {
307
279
  opt.setAttribute('aria-selected', 'false');
@@ -311,546 +283,17 @@ export function init() {
311
283
  }
312
284
  });
313
285
  }
314
-
286
+
315
287
  // Initialize on DOM ready
316
288
  if (document.readyState === 'loading') {
317
289
  document.addEventListener('DOMContentLoaded', initCustomSelects);
318
290
  } else {
319
291
  initCustomSelects();
320
292
  }
321
-
293
+
322
294
  // Reinitialize for dynamic content
323
295
  window.initCustomSelects = initCustomSelects;
324
296
  })();
325
297
 
326
- const initializeForms = () => {
327
- const forms = document.querySelectorAll(config.selectors.form);
328
- forms.forEach(form => {
329
- form.setAttribute('novalidate', '');
330
-
331
- // Completely disable all browser validation methods
332
- form.checkValidity = () => true;
333
- form.reportValidity = () => true;
334
-
335
- // Override all input validation as well
336
- const inputs = form.querySelectorAll('input, textarea, select');
337
- inputs.forEach(input => {
338
- // Remove required attribute and store it
339
- if (input.hasAttribute('required')) {
340
- input.removeAttribute('required');
341
- input.setAttribute('data-was-required', 'true');
342
- }
343
-
344
- // Override validation methods
345
- input.checkValidity = () => true;
346
- input.reportValidity = () => true;
347
- input.setCustomValidity = () => {};
348
- });
349
- });
350
-
351
- errorTemplate = document.querySelector(config.selectors.errorTemplate);
352
- if (!errorTemplate) {
353
- console.warn('Form validation: Error template not found');
354
- } else {
355
- // Clone the template, clean it up, and remove original from DOM
356
- originalErrorTemplate = errorTemplate.cloneNode(true);
357
-
358
- // Clean up the cloned template
359
- originalErrorTemplate.removeAttribute('data-hs-form');
360
- originalErrorTemplate.removeAttribute('style');
361
- originalErrorTemplate.textContent = ''; // Clear any placeholder text
362
-
363
- // Remove the original template from the DOM
364
- errorTemplate.remove();
365
- }
366
- };
367
-
368
- const getErrorMessage = (input) => {
369
- const inputType = input.type || input.tagName.toLowerCase();
370
- const value = input.value.trim();
371
-
372
- // If field is empty and was required, return required message
373
- if (input.hasAttribute('data-was-required') && !value) {
374
- return config.errorMessages[inputType] || config.errorMessages.default;
375
- }
376
-
377
- // For non-empty invalid fields, return type-specific messages
378
- if (value) {
379
- switch (inputType) {
380
- case 'email':
381
- return 'Please enter a valid email address';
382
- case 'url':
383
- return 'Please enter a valid URL';
384
- case 'number':
385
- if (input.hasAttribute('min') && parseFloat(value) < parseFloat(input.min)) {
386
- return `Number must be at least ${input.min}`;
387
- }
388
- if (input.hasAttribute('max') && parseFloat(value) > parseFloat(input.max)) {
389
- return `Number must be no more than ${input.max}`;
390
- }
391
- return 'Please enter a valid number';
392
- case 'tel':
393
- return 'Please enter a valid phone number';
394
- default:
395
- if (input.hasAttribute('minlength') && value.length < parseInt(input.minlength)) {
396
- return `Must be at least ${input.minlength} characters`;
397
- }
398
- if (input.hasAttribute('maxlength') && value.length > parseInt(input.maxlength)) {
399
- return `Must be no more than ${input.maxlength} characters`;
400
- }
401
- if (input.hasAttribute('pattern')) {
402
- return 'Please match the required format';
403
- }
404
- return config.errorMessages[inputType] || config.errorMessages.default;
405
- }
406
- }
407
-
408
- return config.errorMessages[inputType] || config.errorMessages.default;
409
- };
410
-
411
- const createErrorElement = (message) => {
412
- if (!originalErrorTemplate) return null;
413
-
414
- const errorElement = originalErrorTemplate.cloneNode(true);
415
- errorElement.textContent = message;
416
-
417
- return errorElement;
418
- };
419
-
420
- const positionError = (errorElement, input) => {
421
- const inputRect = input.getBoundingClientRect();
422
- const errorRect = errorElement.getBoundingClientRect();
423
-
424
- let top = inputRect.bottom + window.scrollY + 8;
425
- let left = inputRect.left + window.scrollX + (inputRect.width / 2) - (errorRect.width / 2);
426
-
427
- // Check viewport boundaries
428
- const viewportWidth = window.innerWidth;
429
- const viewportHeight = window.innerHeight;
430
-
431
- // Adjust horizontal position
432
- if (left < 10) {
433
- left = 10;
434
- } else if (left + errorRect.width > viewportWidth - 10) {
435
- left = viewportWidth - errorRect.width - 10;
436
- }
437
-
438
- // Adjust vertical position if error would be below viewport
439
- if (inputRect.bottom + errorRect.height + 16 > viewportHeight) {
440
- top = inputRect.top + window.scrollY - errorRect.height - 8;
441
- }
442
-
443
- const finalTop = top - window.scrollY;
444
- const finalLeft = left - window.scrollX;
445
-
446
- // Only set the minimal positioning styles needed
447
- errorElement.style.position = 'fixed';
448
- errorElement.style.top = `${finalTop}px`;
449
- errorElement.style.left = `${finalLeft}px`;
450
- errorElement.style.zIndex = '9999';
451
- };
452
-
453
- const showError = (input, message) => {
454
- removeError(input);
455
-
456
- const errorElement = createErrorElement(message);
457
- if (!errorElement) return;
458
- document.body.appendChild(errorElement);
459
- positionError(errorElement, input);
460
-
461
- activeErrors.set(input, errorElement);
462
-
463
- input.setAttribute('aria-invalid', 'true');
464
- input.setAttribute('aria-describedby', `error-${Date.now()}`);
465
- errorElement.id = input.getAttribute('aria-describedby');
466
- };
467
-
468
- const removeError = (input) => {
469
- const errorElement = activeErrors.get(input);
470
- if (errorElement) {
471
- errorElement.remove();
472
- activeErrors.delete(input);
473
- input.removeAttribute('aria-invalid');
474
- input.removeAttribute('aria-describedby');
475
- }
476
- };
477
-
478
- const customValidateField = (field) => {
479
- const value = field.value.trim();
480
- const type = field.type || field.tagName.toLowerCase();
481
-
482
- // Check if field was required (now stored in data-was-required)
483
- if (field.hasAttribute('data-was-required')) {
484
- // Special handling for checkboxes
485
- if (type === 'checkbox') {
486
- return field.checked;
487
- }
488
-
489
- // Special handling for radio buttons - check if ANY radio in the group is checked
490
- if (type === 'radio') {
491
- const radioName = field.getAttribute('name');
492
- if (radioName) {
493
- // Find all radios with the same name in the same form
494
- const form = field.closest('form');
495
- const radioGroup = form ?
496
- form.querySelectorAll(`input[type="radio"][name="${radioName}"]`) :
497
- document.querySelectorAll(`input[type="radio"][name="${radioName}"]`);
498
-
499
- // Check if any radio in the group is checked
500
- return Array.from(radioGroup).some(radio => radio.checked);
501
- }
502
- // Fallback to individual check if no name attribute
503
- return field.checked;
504
- }
505
-
506
- // For other field types, check if empty
507
- if (!value) {
508
- return false;
509
- }
510
-
511
- // Type-specific validation for non-empty values
512
- switch (type) {
513
- case 'email':
514
- // Basic email validation
515
- const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
516
- return emailRegex.test(value);
517
-
518
- case 'url':
519
- // Basic URL validation
520
- try {
521
- new URL(value);
522
- return true;
523
- } catch {
524
- return false;
525
- }
526
-
527
- case 'number':
528
- // Number validation
529
- const num = parseFloat(value);
530
- if (isNaN(num)) return false;
531
-
532
- // Check min/max if present
533
- if (field.hasAttribute('min') && num < parseFloat(field.min)) return false;
534
- if (field.hasAttribute('max') && num > parseFloat(field.max)) return false;
535
- return true;
536
-
537
- case 'tel':
538
- // Basic phone validation (at least 10 digits)
539
- const phoneRegex = /\d{10,}/;
540
- return phoneRegex.test(value.replace(/\D/g, ''));
541
-
542
- default:
543
- // For text, textarea, etc. - check minlength/maxlength
544
- if (field.hasAttribute('minlength') && value.length < parseInt(field.minlength)) return false;
545
- if (field.hasAttribute('maxlength') && value.length > parseInt(field.maxlength)) return false;
546
-
547
- // Check pattern if present
548
- if (field.hasAttribute('pattern')) {
549
- const pattern = new RegExp(field.pattern);
550
- return pattern.test(value);
551
- }
552
-
553
- return true;
554
- }
555
- }
556
-
557
- return true; // Field is valid if not required or passes validation
558
- };
559
-
560
- const validateContainer = (container) => {
561
- const requiredFields = container.querySelectorAll('input[data-was-required], textarea[data-was-required], select[data-was-required]');
562
- let isValid = true;
563
- let firstInvalidField = null;
564
- const validatedRadioGroups = new Set(); // Track validated radio groups
565
-
566
- requiredFields.forEach((field) => {
567
- const type = field.type || field.tagName.toLowerCase();
568
-
569
- // For radio buttons, only validate once per group
570
- if (type === 'radio') {
571
- const radioName = field.getAttribute('name');
572
- if (radioName && validatedRadioGroups.has(radioName)) {
573
- return; // Skip - already validated this radio group
574
- }
575
- if (radioName) {
576
- validatedRadioGroups.add(radioName);
577
- }
578
- }
579
-
580
- const fieldValid = customValidateField(field);
581
-
582
- if (!fieldValid) {
583
- isValid = false;
584
-
585
- if (!firstInvalidField) {
586
- firstInvalidField = field;
587
- }
588
- }
589
- });
590
-
591
- if (!isValid && firstInvalidField) {
592
- activeErrors.forEach((_, input) => {
593
- removeError(input);
594
- });
595
-
596
- firstInvalidField.focus();
597
-
598
- const message = getErrorMessage(firstInvalidField);
599
- showError(firstInvalidField, message);
600
- }
601
-
602
- return isValid;
603
- };
604
-
605
- // Helper function to parse comma-separated config values
606
- const parseFormConfig = (configString) => {
607
- if (!configString) return [];
608
- return configString.split(',').map(config => config.trim());
609
- };
610
-
611
- const handleFormSubmit = (event) => {
612
- const form = event.target;
613
-
614
- // Set validation flag to prevent click outside interference
615
- isValidating = true;
616
-
617
- const isValid = validateContainer(form);
618
-
619
- // Reset validation flag after a brief delay
620
- setTimeout(() => {
621
- isValidating = false;
622
- }, 200);
623
-
624
- if (!isValid) {
625
- // Only prevent submission if form is invalid
626
- event.preventDefault();
627
- event.stopPropagation();
628
- event.stopImmediatePropagation();
629
- return false;
630
- } else {
631
- // Clear all errors before submission
632
- activeErrors.forEach((_, input) => {
633
- removeError(input);
634
- });
635
-
636
- // Handle text replacement if this form has replace fields
637
- const replaceFieldElements = form.querySelectorAll('input[data-hs-form^="replace-field-"], textarea[data-hs-form^="replace-field-"], select[data-hs-form^="replace-field-"]');
638
- if (replaceFieldElements.length > 0) {
639
- replaceFieldElements.forEach(field => {
640
- const dataHsForm = field.getAttribute('data-hs-form');
641
- const suffix = dataHsForm.replace('replace-field-', '');
642
- const value = field.value;
643
-
644
- // Find all matching text elements
645
- const textElements = document.querySelectorAll(`[data-hs-form="replace-text-${suffix}"]`);
646
-
647
- textElements.forEach(element => {
648
- element.textContent = value;
649
- });
650
- });
651
- }
652
-
653
- // Handle form configuration
654
- const formWrapper = form.closest('[data-hs-form="wrapper"]');
655
- let shouldPreventSubmit = false;
656
-
657
- if (formWrapper && formWrapper.hasAttribute('data-hs-config')) {
658
- const configString = formWrapper.getAttribute('data-hs-config');
659
- const configs = parseFormConfig(configString);
660
-
661
- // Check for prevent-submit config
662
- if (configs.includes('prevent-submit')) {
663
- shouldPreventSubmit = true;
664
- }
665
-
666
- // Check for click-trigger configs
667
- configs.forEach(config => {
668
- if (config.startsWith('click-trigger-')) {
669
- const trigger = document.querySelector(`[data-hs-form="trigger"][data-hs-config*="${config}"]`);
670
- if (trigger) {
671
- setTimeout(() => {
672
- trigger.click();
673
- }, 100);
674
- }
675
- }
676
- });
677
- }
678
-
679
- // Trigger final animation if it exists
680
- const finalAnimElement = form.querySelector(config.selectors.finalAnim);
681
- if (finalAnimElement) {
682
- finalAnimElement.click();
683
- }
684
-
685
- // Prevent submission if configured to do so
686
- if (shouldPreventSubmit) {
687
- event.preventDefault();
688
- event.stopPropagation();
689
- event.stopImmediatePropagation();
690
- return false;
691
- }
692
-
693
- // Don't prevent default - let the form submit naturally with its action/method
694
- }
695
- };
696
-
697
- const handleNextButtonClick = (event) => {
698
- event.preventDefault();
699
-
700
- const button = event.target;
701
- const requiredStepContainer = button.closest(config.selectors.requiredStep);
702
-
703
- if (!requiredStepContainer) {
704
- return;
705
- }
706
-
707
- // Set validation flag to prevent click outside interference
708
- isValidating = true;
709
-
710
- const isValid = validateContainer(requiredStepContainer);
711
-
712
- // Reset validation flag after a brief delay
713
- setTimeout(() => {
714
- isValidating = false;
715
- }, 200);
716
-
717
- if (isValid) {
718
- const nextAnimElement = requiredStepContainer.querySelector(config.selectors.nextAnim);
719
- if (nextAnimElement) {
720
- nextAnimElement.click();
721
- }
722
- }
723
- };
724
-
725
- const handleInputChange = (event) => {
726
- const input = event.target;
727
- if (activeErrors.has(input)) {
728
- removeError(input);
729
- }
730
- };
731
-
732
- const handleClickOutside = (event) => {
733
- const clickedElement = event.target;
734
-
735
- // Don't process click outside during validation
736
- if (isValidating) {
737
- return;
738
- }
739
-
740
- // Don't remove errors if clicking on next button
741
- if (clickedElement.closest(config.selectors.nextButton)) {
742
- return;
743
- }
744
-
745
- // Don't remove errors immediately after they're created
746
- setTimeout(() => {
747
- // Double check validation flag hasn't been set during timeout
748
- if (isValidating) {
749
- return;
750
- }
751
-
752
- activeErrors.forEach((errorElement, input) => {
753
- if (input !== clickedElement && !input.contains(clickedElement) &&
754
- errorElement !== clickedElement && !errorElement.contains(clickedElement)) {
755
- removeError(input);
756
- }
757
- });
758
- }, 100); // Small delay to prevent immediate removal
759
- };
760
-
761
- const handleScroll = () => {
762
- activeErrors.forEach((errorElement, input) => {
763
- const inputRect = input.getBoundingClientRect();
764
- const isVisible = inputRect.top >= 0 && inputRect.bottom <= window.innerHeight;
765
-
766
- if (isVisible) {
767
- positionError(errorElement, input);
768
- } else {
769
- errorElement.style.display = 'none';
770
- }
771
- });
772
- };
773
-
774
- const handleResize = () => {
775
- activeErrors.forEach((errorElement, input) => {
776
- positionError(errorElement, input);
777
- });
778
- };
779
-
780
- const setupEventListeners = () => {
781
- // Global invalid event prevention - this catches ALL invalid events
782
- document.addEventListener('invalid', (e) => {
783
- e.preventDefault();
784
- e.stopPropagation();
785
- e.stopImmediatePropagation();
786
- return false;
787
- }, true);
788
-
789
- // Form submit listeners - use capture phase to ensure we catch it first
790
- document.addEventListener('submit', handleFormSubmit, true);
791
-
792
- // Next button listeners
793
- document.addEventListener('click', (event) => {
794
- if (event.target.closest(config.selectors.nextButton)) {
795
- handleNextButtonClick(event);
796
- }
797
- });
798
-
799
- // Input change listeners
800
- document.addEventListener('input', handleInputChange);
801
-
802
- // Click outside listeners
803
- document.addEventListener('click', handleClickOutside);
804
-
805
- // Scroll and resize listeners
806
- window.addEventListener('scroll', handleScroll);
807
- window.addEventListener('resize', handleResize);
808
- };
809
-
810
- const destroy = () => {
811
- // Clean up all active errors
812
- activeErrors.forEach((_, input) => {
813
- removeError(input);
814
- });
815
-
816
- // Remove event listeners
817
- document.removeEventListener('invalid', (e) => {
818
- e.preventDefault();
819
- e.stopPropagation();
820
- e.stopImmediatePropagation();
821
- return false;
822
- }, true);
823
- document.removeEventListener('submit', handleFormSubmit, true);
824
- document.removeEventListener('input', handleInputChange);
825
- document.removeEventListener('click', handleClickOutside);
826
- window.removeEventListener('scroll', handleScroll);
827
- window.removeEventListener('resize', handleResize);
828
- };
829
-
830
-
831
- // Initialize the form validation system
832
- const initializeFormValidation = () => {
833
- try {
834
- initializeForms();
835
- setupEventListeners();
836
-
837
- return {
838
- result: "form initialized",
839
- destroy
840
- };
841
- } catch (error) {
842
- console.error('Form validation initialization failed:', error);
843
- return {
844
- result: "form initialization failed",
845
- destroy
846
- };
847
- }
848
- };
849
-
850
- // Handle DOM ready state
851
- if (document.readyState === "loading") {
852
- document.addEventListener("DOMContentLoaded", initializeFormValidation);
853
- } else {
854
- return initializeFormValidation();
855
- }
856
- }
298
+ return { result: "form initialized" };
299
+ }