@hortonstudio/main 1.8.2 → 1.9.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/autoInit/accessibility.js +122 -93
- package/autoInit/counter.js +17 -1
- package/autoInit/form.js +58 -13
- package/autoInit/navbar.js +136 -77
- package/autoInit/smooth-scroll.js +33 -10
- package/autoInit/transition.js +88 -62
- package/index.js +18 -9
- package/package.json +1 -1
- package/utils/slider.js +58 -19
|
@@ -1,18 +1,29 @@
|
|
|
1
1
|
export function init() {
|
|
2
|
-
|
|
2
|
+
// Centralized cleanup tracking
|
|
3
|
+
const cleanup = {
|
|
4
|
+
observers: [],
|
|
5
|
+
handlers: [],
|
|
6
|
+
scrollTimeout: null
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const addObserver = (observer) => cleanup.observers.push(observer);
|
|
10
|
+
const addHandler = (element, event, handler, options) => {
|
|
11
|
+
element.addEventListener(event, handler, options);
|
|
12
|
+
cleanup.handlers.push({ element, event, handler, options });
|
|
13
|
+
};
|
|
3
14
|
|
|
4
15
|
function setupGeneralAccessibility() {
|
|
5
16
|
setupListAccessibility();
|
|
6
17
|
setupRemoveListAccessibility();
|
|
7
|
-
setupFAQAccessibility();
|
|
18
|
+
setupFAQAccessibility(addHandler);
|
|
8
19
|
setupConvertToSpan();
|
|
9
20
|
setupYearReplacement();
|
|
10
|
-
setupPreventDefault();
|
|
11
|
-
setupRichTextAccessibility();
|
|
12
|
-
setupSummaryAccessibility();
|
|
21
|
+
setupPreventDefault(addHandler);
|
|
22
|
+
setupRichTextAccessibility(addObserver, addHandler, cleanup);
|
|
23
|
+
setupSummaryAccessibility(addHandler);
|
|
13
24
|
setupCustomValuesReplacement();
|
|
14
|
-
setupClickForwarding();
|
|
15
|
-
setupTextSynchronization();
|
|
25
|
+
setupClickForwarding(addHandler);
|
|
26
|
+
setupTextSynchronization(addObserver);
|
|
16
27
|
}
|
|
17
28
|
|
|
18
29
|
function setupListAccessibility() {
|
|
@@ -88,36 +99,35 @@ export function init() {
|
|
|
88
99
|
});
|
|
89
100
|
}
|
|
90
101
|
|
|
91
|
-
function setupFAQAccessibility() {
|
|
102
|
+
function setupFAQAccessibility(addHandler) {
|
|
92
103
|
const faqContainers = document.querySelectorAll('[data-hs-a11y="faq-wrap"]');
|
|
93
104
|
|
|
94
105
|
faqContainers.forEach((container, index) => {
|
|
95
106
|
const button = container.querySelector('[data-hs-a11y="faq-btn"]');
|
|
96
107
|
const contentWrapper = container.querySelector('[data-hs-a11y="faq-content"]');
|
|
97
|
-
|
|
108
|
+
|
|
98
109
|
if (!button || !contentWrapper) return;
|
|
99
|
-
|
|
110
|
+
|
|
100
111
|
const buttonId = `faq-button-${index}`;
|
|
101
112
|
const contentId = `faq-content-${index}`;
|
|
102
|
-
|
|
113
|
+
|
|
103
114
|
button.setAttribute('id', buttonId);
|
|
104
115
|
button.setAttribute('aria-expanded', 'false');
|
|
105
116
|
button.setAttribute('aria-controls', contentId);
|
|
106
|
-
|
|
117
|
+
|
|
107
118
|
contentWrapper.setAttribute('id', contentId);
|
|
108
119
|
contentWrapper.setAttribute('aria-hidden', 'true');
|
|
109
120
|
contentWrapper.setAttribute('role', 'region');
|
|
110
121
|
contentWrapper.setAttribute('aria-labelledby', buttonId);
|
|
111
|
-
|
|
122
|
+
|
|
112
123
|
function toggleFAQ() {
|
|
113
124
|
const isOpen = button.getAttribute('aria-expanded') === 'true';
|
|
114
|
-
|
|
125
|
+
|
|
115
126
|
button.setAttribute('aria-expanded', !isOpen);
|
|
116
127
|
contentWrapper.setAttribute('aria-hidden', isOpen);
|
|
117
128
|
}
|
|
118
|
-
|
|
119
|
-
button
|
|
120
|
-
|
|
129
|
+
|
|
130
|
+
addHandler(button, 'click', toggleFAQ);
|
|
121
131
|
});
|
|
122
132
|
}
|
|
123
133
|
|
|
@@ -216,26 +226,28 @@ export function init() {
|
|
|
216
226
|
});
|
|
217
227
|
}
|
|
218
228
|
|
|
219
|
-
function setupPreventDefault() {
|
|
229
|
+
function setupPreventDefault(addHandler) {
|
|
220
230
|
const elements = document.querySelectorAll('[data-hs-a11y="prevent-default"]');
|
|
221
|
-
|
|
231
|
+
|
|
222
232
|
elements.forEach(element => {
|
|
223
233
|
// Prevent click
|
|
224
|
-
|
|
234
|
+
const clickHandler = (e) => {
|
|
225
235
|
e.preventDefault();
|
|
226
236
|
e.stopPropagation();
|
|
227
237
|
return false;
|
|
228
|
-
}
|
|
229
|
-
|
|
238
|
+
};
|
|
239
|
+
addHandler(element, 'click', clickHandler);
|
|
240
|
+
|
|
230
241
|
// Prevent keyboard activation
|
|
231
|
-
|
|
242
|
+
const keydownHandler = (e) => {
|
|
232
243
|
if (e.key === 'Enter' || e.key === ' ') {
|
|
233
244
|
e.preventDefault();
|
|
234
245
|
e.stopPropagation();
|
|
235
246
|
return false;
|
|
236
247
|
}
|
|
237
|
-
}
|
|
238
|
-
|
|
248
|
+
};
|
|
249
|
+
addHandler(element, 'keydown', keydownHandler);
|
|
250
|
+
|
|
239
251
|
// Additional prevention for anchor links
|
|
240
252
|
if (element.tagName.toLowerCase() === 'a') {
|
|
241
253
|
// Remove or modify href to prevent scroll
|
|
@@ -247,27 +259,23 @@ export function init() {
|
|
|
247
259
|
element.setAttribute('tabindex', '0');
|
|
248
260
|
}
|
|
249
261
|
}
|
|
250
|
-
|
|
251
262
|
});
|
|
252
263
|
}
|
|
253
264
|
|
|
254
|
-
function setupSummaryAccessibility() {
|
|
265
|
+
function setupSummaryAccessibility(addHandler) {
|
|
255
266
|
const summaryContainers = document.querySelectorAll('[data-hs-a11y="summary-wrap"]');
|
|
256
267
|
|
|
257
|
-
|
|
258
268
|
summaryContainers.forEach((container, index) => {
|
|
259
|
-
|
|
260
269
|
const button = container.querySelector('[data-hs-a11y="summary-btn"]');
|
|
261
270
|
const contentWrapper = container.querySelector('[data-hs-a11y="summary-content"]');
|
|
262
|
-
|
|
271
|
+
|
|
263
272
|
if (!button || !contentWrapper) {
|
|
264
273
|
return;
|
|
265
274
|
}
|
|
266
|
-
|
|
267
|
-
|
|
275
|
+
|
|
268
276
|
const buttonId = `summary-button-${index}`;
|
|
269
277
|
const contentId = `summary-content-${index}`;
|
|
270
|
-
|
|
278
|
+
|
|
271
279
|
// Get original button text from first text node only
|
|
272
280
|
const walker = document.createTreeWalker(
|
|
273
281
|
button,
|
|
@@ -275,14 +283,14 @@ export function init() {
|
|
|
275
283
|
null,
|
|
276
284
|
false
|
|
277
285
|
);
|
|
278
|
-
|
|
286
|
+
|
|
279
287
|
let firstTextNode = walker.nextNode();
|
|
280
288
|
while (firstTextNode && !firstTextNode.textContent.trim()) {
|
|
281
289
|
firstTextNode = walker.nextNode();
|
|
282
290
|
}
|
|
283
|
-
|
|
291
|
+
|
|
284
292
|
const originalButtonText = firstTextNode ? firstTextNode.textContent.trim() : button.textContent.trim();
|
|
285
|
-
|
|
293
|
+
|
|
286
294
|
// Function to update all text nodes in button
|
|
287
295
|
function updateButtonText(newText) {
|
|
288
296
|
// Find all text nodes and update them
|
|
@@ -292,7 +300,7 @@ export function init() {
|
|
|
292
300
|
null,
|
|
293
301
|
false
|
|
294
302
|
);
|
|
295
|
-
|
|
303
|
+
|
|
296
304
|
const textNodes = [];
|
|
297
305
|
let node;
|
|
298
306
|
while (node = walker.nextNode()) {
|
|
@@ -300,27 +308,27 @@ export function init() {
|
|
|
300
308
|
textNodes.push(node);
|
|
301
309
|
}
|
|
302
310
|
}
|
|
303
|
-
|
|
311
|
+
|
|
304
312
|
textNodes.forEach(textNode => {
|
|
305
313
|
textNode.textContent = newText;
|
|
306
314
|
});
|
|
307
315
|
}
|
|
308
|
-
|
|
316
|
+
|
|
309
317
|
button.setAttribute('id', buttonId);
|
|
310
318
|
button.setAttribute('aria-expanded', 'false');
|
|
311
319
|
button.setAttribute('aria-controls', contentId);
|
|
312
320
|
button.setAttribute('aria-label', 'View Summary');
|
|
313
|
-
|
|
321
|
+
|
|
314
322
|
contentWrapper.setAttribute('id', contentId);
|
|
315
323
|
contentWrapper.setAttribute('aria-hidden', 'true');
|
|
316
324
|
contentWrapper.setAttribute('role', 'region');
|
|
317
325
|
contentWrapper.setAttribute('aria-labelledby', buttonId);
|
|
318
|
-
|
|
326
|
+
|
|
319
327
|
// Summary is closed by default - no need to check initial state
|
|
320
|
-
|
|
328
|
+
|
|
321
329
|
function toggleSummary() {
|
|
322
330
|
const isOpen = button.getAttribute('aria-expanded') === 'true';
|
|
323
|
-
|
|
331
|
+
|
|
324
332
|
if (isOpen) {
|
|
325
333
|
// Closing
|
|
326
334
|
button.setAttribute('aria-expanded', 'false');
|
|
@@ -334,11 +342,9 @@ export function init() {
|
|
|
334
342
|
updateButtonText('Close');
|
|
335
343
|
contentWrapper.setAttribute('aria-hidden', 'false');
|
|
336
344
|
}
|
|
337
|
-
|
|
338
345
|
}
|
|
339
|
-
|
|
340
|
-
button
|
|
341
|
-
|
|
346
|
+
|
|
347
|
+
addHandler(button, 'click', toggleSummary);
|
|
342
348
|
});
|
|
343
349
|
}
|
|
344
350
|
|
|
@@ -434,54 +440,56 @@ export function init() {
|
|
|
434
440
|
});
|
|
435
441
|
}
|
|
436
442
|
|
|
437
|
-
function setupClickForwarding() {
|
|
443
|
+
function setupClickForwarding(addHandler) {
|
|
438
444
|
// Find all clickable elements (custom styled elements users click)
|
|
439
445
|
const clickableElements = document.querySelectorAll('[data-hs-a11y*="clickable"]');
|
|
440
|
-
|
|
446
|
+
|
|
441
447
|
clickableElements.forEach(clickableElement => {
|
|
442
448
|
const attribute = clickableElement.getAttribute('data-hs-a11y');
|
|
443
|
-
|
|
449
|
+
|
|
444
450
|
// Parse the attribute: "click-trigger-[identifier], clickable"
|
|
445
451
|
const parts = attribute.split(',').map(part => part.trim());
|
|
446
|
-
|
|
452
|
+
|
|
447
453
|
// Find the part with click-trigger and the part with clickable
|
|
448
454
|
const triggerPart = parts.find(part => part.startsWith('click-trigger-'));
|
|
449
455
|
const rolePart = parts.find(part => part === 'clickable');
|
|
450
|
-
|
|
456
|
+
|
|
451
457
|
if (!triggerPart || !rolePart) {
|
|
452
458
|
return;
|
|
453
459
|
}
|
|
454
|
-
|
|
460
|
+
|
|
455
461
|
// Extract identifier from "click-trigger-[identifier]"
|
|
456
462
|
const identifier = triggerPart.replace('click-trigger-', '').trim();
|
|
457
|
-
|
|
463
|
+
|
|
458
464
|
// Find the corresponding trigger element
|
|
459
465
|
const triggerSelector = `[data-hs-a11y*="click-trigger-${identifier}"][data-hs-a11y*="trigger"]`;
|
|
460
466
|
const triggerElement = document.querySelector(triggerSelector);
|
|
461
|
-
|
|
467
|
+
|
|
462
468
|
if (!triggerElement) {
|
|
463
469
|
return;
|
|
464
470
|
}
|
|
465
|
-
|
|
471
|
+
|
|
466
472
|
// Add click event listener to forward clicks
|
|
467
|
-
|
|
473
|
+
const clickHandler = (event) => {
|
|
468
474
|
// Prevent default behavior on the clickable element
|
|
469
475
|
event.preventDefault();
|
|
470
476
|
event.stopPropagation();
|
|
471
|
-
|
|
477
|
+
|
|
472
478
|
// Trigger click on the target element
|
|
473
479
|
triggerElement.click();
|
|
474
|
-
}
|
|
475
|
-
|
|
480
|
+
};
|
|
481
|
+
addHandler(clickableElement, 'click', clickHandler);
|
|
482
|
+
|
|
476
483
|
// Also handle keyboard events for accessibility
|
|
477
|
-
|
|
484
|
+
const keydownHandler = (event) => {
|
|
478
485
|
if (event.key === 'Enter' || event.key === ' ') {
|
|
479
486
|
event.preventDefault();
|
|
480
487
|
event.stopPropagation();
|
|
481
488
|
triggerElement.click();
|
|
482
489
|
}
|
|
483
|
-
}
|
|
484
|
-
|
|
490
|
+
};
|
|
491
|
+
addHandler(clickableElement, 'keydown', keydownHandler);
|
|
492
|
+
|
|
485
493
|
// Ensure clickable element is keyboard accessible
|
|
486
494
|
if (!clickableElement.hasAttribute('tabindex')) {
|
|
487
495
|
clickableElement.setAttribute('tabindex', '0');
|
|
@@ -492,44 +500,44 @@ export function init() {
|
|
|
492
500
|
});
|
|
493
501
|
}
|
|
494
502
|
|
|
495
|
-
function setupTextSynchronization() {
|
|
503
|
+
function setupTextSynchronization(addObserver) {
|
|
496
504
|
// Find all original elements (source of truth)
|
|
497
505
|
const originalElements = document.querySelectorAll('[data-hs-a11y*="original"]');
|
|
498
|
-
|
|
506
|
+
|
|
499
507
|
originalElements.forEach(originalElement => {
|
|
500
508
|
const attribute = originalElement.getAttribute('data-hs-a11y');
|
|
501
|
-
|
|
509
|
+
|
|
502
510
|
// Parse the attribute: "match-text-[identifier], original"
|
|
503
511
|
const parts = attribute.split(',').map(part => part.trim());
|
|
504
|
-
|
|
512
|
+
|
|
505
513
|
// Find the part with match-text and the part with original
|
|
506
514
|
const textPart = parts.find(part => part.startsWith('match-text-'));
|
|
507
515
|
const rolePart = parts.find(part => part === 'original');
|
|
508
|
-
|
|
516
|
+
|
|
509
517
|
if (!textPart || !rolePart) {
|
|
510
518
|
return;
|
|
511
519
|
}
|
|
512
|
-
|
|
520
|
+
|
|
513
521
|
// Extract identifier from "match-text-[identifier]"
|
|
514
522
|
const identifier = textPart.replace('match-text-', '').trim();
|
|
515
|
-
|
|
523
|
+
|
|
516
524
|
// Find all corresponding match elements
|
|
517
525
|
const matchSelector = `[data-hs-a11y*="match-text-${identifier}"][data-hs-a11y*="match"]`;
|
|
518
526
|
const matchElements = document.querySelectorAll(matchSelector);
|
|
519
|
-
|
|
527
|
+
|
|
520
528
|
if (matchElements.length === 0) {
|
|
521
529
|
return;
|
|
522
530
|
}
|
|
523
|
-
|
|
531
|
+
|
|
524
532
|
// Function to synchronize text and aria-label
|
|
525
533
|
function synchronizeContent() {
|
|
526
534
|
const originalText = originalElement.textContent;
|
|
527
535
|
const originalAriaLabel = originalElement.getAttribute('aria-label');
|
|
528
|
-
|
|
536
|
+
|
|
529
537
|
matchElements.forEach(matchElement => {
|
|
530
538
|
// Copy text content
|
|
531
539
|
matchElement.textContent = originalText;
|
|
532
|
-
|
|
540
|
+
|
|
533
541
|
// Synchronize aria-label
|
|
534
542
|
if (originalAriaLabel) {
|
|
535
543
|
// If original has aria-label, copy it to match
|
|
@@ -542,27 +550,27 @@ export function init() {
|
|
|
542
550
|
}
|
|
543
551
|
});
|
|
544
552
|
}
|
|
545
|
-
|
|
553
|
+
|
|
546
554
|
// Initial synchronization
|
|
547
555
|
synchronizeContent();
|
|
548
|
-
|
|
556
|
+
|
|
549
557
|
// Set up MutationObserver to watch for changes
|
|
550
558
|
const observer = new MutationObserver((mutations) => {
|
|
551
559
|
let shouldSync = false;
|
|
552
|
-
|
|
560
|
+
|
|
553
561
|
mutations.forEach((mutation) => {
|
|
554
|
-
if (mutation.type === 'childList' ||
|
|
555
|
-
mutation.type === 'characterData' ||
|
|
562
|
+
if (mutation.type === 'childList' ||
|
|
563
|
+
mutation.type === 'characterData' ||
|
|
556
564
|
(mutation.type === 'attributes' && mutation.attributeName === 'aria-label')) {
|
|
557
565
|
shouldSync = true;
|
|
558
566
|
}
|
|
559
567
|
});
|
|
560
|
-
|
|
568
|
+
|
|
561
569
|
if (shouldSync) {
|
|
562
570
|
synchronizeContent();
|
|
563
571
|
}
|
|
564
572
|
});
|
|
565
|
-
|
|
573
|
+
|
|
566
574
|
// Observe text changes and attribute changes
|
|
567
575
|
observer.observe(originalElement, {
|
|
568
576
|
childList: true,
|
|
@@ -571,16 +579,16 @@ export function init() {
|
|
|
571
579
|
attributes: true,
|
|
572
580
|
attributeFilter: ['aria-label']
|
|
573
581
|
});
|
|
582
|
+
|
|
583
|
+
addObserver(observer);
|
|
574
584
|
});
|
|
575
585
|
}
|
|
576
586
|
|
|
577
|
-
function setupRichTextAccessibility() {
|
|
587
|
+
function setupRichTextAccessibility(addObserver, addHandler, cleanup) {
|
|
578
588
|
const contentAreas = document.querySelectorAll('[data-hs-a11y="rich-content"]');
|
|
579
589
|
const tocLists = document.querySelectorAll('[data-hs-a11y="rich-toc"]');
|
|
580
590
|
|
|
581
|
-
|
|
582
591
|
contentAreas.forEach((contentArea) => {
|
|
583
|
-
|
|
584
592
|
// Since there's only 1 content area and 1 TOC list per page, use the first TOC list
|
|
585
593
|
const tocList = tocLists[0];
|
|
586
594
|
|
|
@@ -592,7 +600,6 @@ export function init() {
|
|
|
592
600
|
return;
|
|
593
601
|
}
|
|
594
602
|
|
|
595
|
-
|
|
596
603
|
const template = tocList.children[0].cloneNode(true);
|
|
597
604
|
// Remove is-active class from template if it exists
|
|
598
605
|
const templateLink = template.querySelector("a");
|
|
@@ -643,7 +650,7 @@ export function init() {
|
|
|
643
650
|
link.appendChild(document.createTextNode(heading.textContent));
|
|
644
651
|
|
|
645
652
|
// Add click handler for smooth scrolling
|
|
646
|
-
|
|
653
|
+
const clickHandler = (e) => {
|
|
647
654
|
e.preventDefault();
|
|
648
655
|
|
|
649
656
|
const targetSection = document.getElementById(sectionId);
|
|
@@ -654,7 +661,8 @@ export function init() {
|
|
|
654
661
|
targetSection.focus();
|
|
655
662
|
}, 100);
|
|
656
663
|
}
|
|
657
|
-
}
|
|
664
|
+
};
|
|
665
|
+
addHandler(link, "click", clickHandler);
|
|
658
666
|
|
|
659
667
|
// Ensure sections are focusable for keyboard users but use CSS to control focus visibility
|
|
660
668
|
const targetSection = document.getElementById(sectionId);
|
|
@@ -719,17 +727,38 @@ export function init() {
|
|
|
719
727
|
|
|
720
728
|
// Observe all sections
|
|
721
729
|
sections.forEach(section => observer.observe(section));
|
|
730
|
+
addObserver(observer);
|
|
722
731
|
|
|
723
732
|
// Also update on scroll for smoother tracking
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
733
|
+
const scrollHandler = () => {
|
|
734
|
+
if (cleanup.scrollTimeout) clearTimeout(cleanup.scrollTimeout);
|
|
735
|
+
cleanup.scrollTimeout = setTimeout(updateActiveLink, 50);
|
|
736
|
+
};
|
|
737
|
+
addHandler(window, 'scroll', scrollHandler);
|
|
729
738
|
|
|
730
739
|
});
|
|
731
740
|
}
|
|
732
741
|
|
|
733
742
|
setupGeneralAccessibility();
|
|
734
|
-
|
|
743
|
+
|
|
744
|
+
return {
|
|
745
|
+
result: "accessibility initialized",
|
|
746
|
+
destroy: () => {
|
|
747
|
+
// Disconnect all observers
|
|
748
|
+
cleanup.observers.forEach(obs => obs.disconnect());
|
|
749
|
+
cleanup.observers.length = 0;
|
|
750
|
+
|
|
751
|
+
// Remove all event listeners
|
|
752
|
+
cleanup.handlers.forEach(({ element, event, handler, options }) => {
|
|
753
|
+
element.removeEventListener(event, handler, options);
|
|
754
|
+
});
|
|
755
|
+
cleanup.handlers.length = 0;
|
|
756
|
+
|
|
757
|
+
// Clear scroll timeout
|
|
758
|
+
if (cleanup.scrollTimeout) {
|
|
759
|
+
clearTimeout(cleanup.scrollTimeout);
|
|
760
|
+
cleanup.scrollTimeout = null;
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
};
|
|
735
764
|
}
|
package/autoInit/counter.js
CHANGED
|
@@ -150,5 +150,21 @@ export function init() {
|
|
|
150
150
|
counters,
|
|
151
151
|
};
|
|
152
152
|
|
|
153
|
-
return {
|
|
153
|
+
return {
|
|
154
|
+
result: "counter initialized",
|
|
155
|
+
destroy: () => {
|
|
156
|
+
// Disconnect observer
|
|
157
|
+
if (observer) {
|
|
158
|
+
observer.disconnect();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Clear counters array
|
|
162
|
+
counters.length = 0;
|
|
163
|
+
|
|
164
|
+
// Remove window API
|
|
165
|
+
if (window.hsmain && window.hsmain.counter) {
|
|
166
|
+
delete window.hsmain.counter;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
};
|
|
154
170
|
}
|
package/autoInit/form.js
CHANGED
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
export function init() {
|
|
2
|
+
// Centralized cleanup tracking
|
|
3
|
+
const cleanup = {
|
|
4
|
+
observers: [],
|
|
5
|
+
handlers: [],
|
|
6
|
+
honeypotHandler: null
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const addObserver = (observer) => cleanup.observers.push(observer);
|
|
10
|
+
const addHandler = (element, event, handler, options) => {
|
|
11
|
+
element.addEventListener(event, handler, options);
|
|
12
|
+
cleanup.handlers.push({ element, event, handler, options });
|
|
13
|
+
};
|
|
14
|
+
|
|
2
15
|
// Honeypot spam prevention
|
|
3
|
-
|
|
16
|
+
const honeypotHandler = (e) => {
|
|
4
17
|
const form = e.target;
|
|
5
18
|
if (form.tagName !== 'FORM') return;
|
|
6
19
|
|
|
@@ -12,7 +25,10 @@ export function init() {
|
|
|
12
25
|
e.stopImmediatePropagation();
|
|
13
26
|
return false;
|
|
14
27
|
}
|
|
15
|
-
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
cleanup.honeypotHandler = honeypotHandler;
|
|
31
|
+
document.addEventListener('submit', honeypotHandler, true);
|
|
16
32
|
|
|
17
33
|
// Simple Custom Select Component for Webflow
|
|
18
34
|
(function() {
|
|
@@ -32,12 +48,12 @@ export function init() {
|
|
|
32
48
|
const selectWrappers = document.querySelectorAll('[data-hs-form="select"]');
|
|
33
49
|
|
|
34
50
|
selectWrappers.forEach(wrapper => {
|
|
35
|
-
initSingleSelect(wrapper);
|
|
51
|
+
initSingleSelect(wrapper, addHandler, addObserver);
|
|
36
52
|
});
|
|
37
53
|
}
|
|
38
54
|
|
|
39
55
|
// Initialize a single custom select
|
|
40
|
-
function initSingleSelect(wrapper) {
|
|
56
|
+
function initSingleSelect(wrapper, addHandler, addObserver) {
|
|
41
57
|
// Find all required elements
|
|
42
58
|
const realSelect = wrapper.querySelector('select');
|
|
43
59
|
if (!realSelect) return;
|
|
@@ -165,7 +181,7 @@ export function init() {
|
|
|
165
181
|
}
|
|
166
182
|
|
|
167
183
|
// Button keyboard events
|
|
168
|
-
|
|
184
|
+
const buttonKeydownHandler = (e) => {
|
|
169
185
|
switch(e.key) {
|
|
170
186
|
case ' ':
|
|
171
187
|
case 'Enter':
|
|
@@ -197,10 +213,11 @@ export function init() {
|
|
|
197
213
|
}
|
|
198
214
|
break;
|
|
199
215
|
}
|
|
200
|
-
}
|
|
216
|
+
};
|
|
217
|
+
addHandler(button, 'keydown', buttonKeydownHandler);
|
|
201
218
|
|
|
202
219
|
// Option keyboard events (delegated)
|
|
203
|
-
|
|
220
|
+
const listKeydownHandler = (e) => {
|
|
204
221
|
const option = e.target.closest('[role="option"]');
|
|
205
222
|
if (!option) return;
|
|
206
223
|
|
|
@@ -237,15 +254,17 @@ export function init() {
|
|
|
237
254
|
button.focus();
|
|
238
255
|
break;
|
|
239
256
|
}
|
|
240
|
-
}
|
|
257
|
+
};
|
|
258
|
+
addHandler(customList, 'keydown', listKeydownHandler);
|
|
241
259
|
|
|
242
260
|
// Option click events
|
|
243
|
-
|
|
261
|
+
const listClickHandler = (e) => {
|
|
244
262
|
const option = e.target.closest('[role="option"]');
|
|
245
263
|
if (option) {
|
|
246
264
|
selectOption(option);
|
|
247
265
|
}
|
|
248
|
-
}
|
|
266
|
+
};
|
|
267
|
+
addHandler(customList, 'click', listClickHandler);
|
|
249
268
|
|
|
250
269
|
// Track open/close state
|
|
251
270
|
const observer = new MutationObserver((mutations) => {
|
|
@@ -275,9 +294,10 @@ export function init() {
|
|
|
275
294
|
attributes: true,
|
|
276
295
|
attributeFilter: ['hidden', 'style', 'class']
|
|
277
296
|
});
|
|
297
|
+
addObserver(observer);
|
|
278
298
|
|
|
279
299
|
// Sync with real select changes
|
|
280
|
-
|
|
300
|
+
const selectChangeHandler = () => {
|
|
281
301
|
const selectedOption = realSelect.options[realSelect.selectedIndex];
|
|
282
302
|
if (selectedOption) {
|
|
283
303
|
const customOption = customList.querySelector(`[data-value="${selectedOption.value}"]`);
|
|
@@ -296,7 +316,8 @@ export function init() {
|
|
|
296
316
|
customOption.setAttribute('aria-selected', 'true');
|
|
297
317
|
}
|
|
298
318
|
}
|
|
299
|
-
}
|
|
319
|
+
};
|
|
320
|
+
addHandler(realSelect, 'change', selectChangeHandler);
|
|
300
321
|
}
|
|
301
322
|
|
|
302
323
|
// Initialize on DOM ready
|
|
@@ -310,5 +331,29 @@ export function init() {
|
|
|
310
331
|
window.initCustomSelects = initCustomSelects;
|
|
311
332
|
})();
|
|
312
333
|
|
|
313
|
-
return {
|
|
334
|
+
return {
|
|
335
|
+
result: "form initialized",
|
|
336
|
+
destroy: () => {
|
|
337
|
+
// Remove honeypot handler
|
|
338
|
+
if (cleanup.honeypotHandler) {
|
|
339
|
+
document.removeEventListener('submit', cleanup.honeypotHandler, true);
|
|
340
|
+
cleanup.honeypotHandler = null;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Disconnect all observers
|
|
344
|
+
cleanup.observers.forEach(obs => obs.disconnect());
|
|
345
|
+
cleanup.observers.length = 0;
|
|
346
|
+
|
|
347
|
+
// Remove all event listeners
|
|
348
|
+
cleanup.handlers.forEach(({ element, event, handler, options }) => {
|
|
349
|
+
element.removeEventListener(event, handler, options);
|
|
350
|
+
});
|
|
351
|
+
cleanup.handlers.length = 0;
|
|
352
|
+
|
|
353
|
+
// Remove window API
|
|
354
|
+
if (window.initCustomSelects) {
|
|
355
|
+
delete window.initCustomSelects;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
};
|
|
314
359
|
}
|