@hortonstudio/main 1.8.2 → 1.9.1
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 +144 -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,51 @@
|
|
|
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
|
+
};
|
|
14
|
+
|
|
15
|
+
function setupBlogListCleanup() {
|
|
16
|
+
const wrappers = document.querySelectorAll('[data-site-blog="wrapper"]');
|
|
17
|
+
|
|
18
|
+
wrappers.forEach(wrapper => {
|
|
19
|
+
// Check if wrapper has the delete-if-no-list config
|
|
20
|
+
const shouldDelete = wrapper.getAttribute('data-site-blog-config') === 'delete-if-no-list';
|
|
21
|
+
|
|
22
|
+
if (!shouldDelete) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Check if there's a descendant with data-site-blog="list"
|
|
27
|
+
const hasList = wrapper.querySelector('[data-site-blog="list"]') !== null;
|
|
28
|
+
|
|
29
|
+
// Delete wrapper if it doesn't have a list
|
|
30
|
+
if (!hasList) {
|
|
31
|
+
wrapper.remove();
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
}
|
|
3
35
|
|
|
4
36
|
function setupGeneralAccessibility() {
|
|
5
37
|
setupListAccessibility();
|
|
6
38
|
setupRemoveListAccessibility();
|
|
7
|
-
setupFAQAccessibility();
|
|
39
|
+
setupFAQAccessibility(addHandler);
|
|
8
40
|
setupConvertToSpan();
|
|
9
41
|
setupYearReplacement();
|
|
10
|
-
setupPreventDefault();
|
|
11
|
-
setupRichTextAccessibility();
|
|
12
|
-
setupSummaryAccessibility();
|
|
42
|
+
setupPreventDefault(addHandler);
|
|
43
|
+
setupRichTextAccessibility(addObserver, addHandler, cleanup);
|
|
44
|
+
setupSummaryAccessibility(addHandler);
|
|
13
45
|
setupCustomValuesReplacement();
|
|
14
|
-
setupClickForwarding();
|
|
15
|
-
setupTextSynchronization();
|
|
46
|
+
setupClickForwarding(addHandler);
|
|
47
|
+
setupTextSynchronization(addObserver);
|
|
48
|
+
setupBlogListCleanup();
|
|
16
49
|
}
|
|
17
50
|
|
|
18
51
|
function setupListAccessibility() {
|
|
@@ -88,36 +121,35 @@ export function init() {
|
|
|
88
121
|
});
|
|
89
122
|
}
|
|
90
123
|
|
|
91
|
-
function setupFAQAccessibility() {
|
|
124
|
+
function setupFAQAccessibility(addHandler) {
|
|
92
125
|
const faqContainers = document.querySelectorAll('[data-hs-a11y="faq-wrap"]');
|
|
93
126
|
|
|
94
127
|
faqContainers.forEach((container, index) => {
|
|
95
128
|
const button = container.querySelector('[data-hs-a11y="faq-btn"]');
|
|
96
129
|
const contentWrapper = container.querySelector('[data-hs-a11y="faq-content"]');
|
|
97
|
-
|
|
130
|
+
|
|
98
131
|
if (!button || !contentWrapper) return;
|
|
99
|
-
|
|
132
|
+
|
|
100
133
|
const buttonId = `faq-button-${index}`;
|
|
101
134
|
const contentId = `faq-content-${index}`;
|
|
102
|
-
|
|
135
|
+
|
|
103
136
|
button.setAttribute('id', buttonId);
|
|
104
137
|
button.setAttribute('aria-expanded', 'false');
|
|
105
138
|
button.setAttribute('aria-controls', contentId);
|
|
106
|
-
|
|
139
|
+
|
|
107
140
|
contentWrapper.setAttribute('id', contentId);
|
|
108
141
|
contentWrapper.setAttribute('aria-hidden', 'true');
|
|
109
142
|
contentWrapper.setAttribute('role', 'region');
|
|
110
143
|
contentWrapper.setAttribute('aria-labelledby', buttonId);
|
|
111
|
-
|
|
144
|
+
|
|
112
145
|
function toggleFAQ() {
|
|
113
146
|
const isOpen = button.getAttribute('aria-expanded') === 'true';
|
|
114
|
-
|
|
147
|
+
|
|
115
148
|
button.setAttribute('aria-expanded', !isOpen);
|
|
116
149
|
contentWrapper.setAttribute('aria-hidden', isOpen);
|
|
117
150
|
}
|
|
118
|
-
|
|
119
|
-
button
|
|
120
|
-
|
|
151
|
+
|
|
152
|
+
addHandler(button, 'click', toggleFAQ);
|
|
121
153
|
});
|
|
122
154
|
}
|
|
123
155
|
|
|
@@ -216,26 +248,28 @@ export function init() {
|
|
|
216
248
|
});
|
|
217
249
|
}
|
|
218
250
|
|
|
219
|
-
function setupPreventDefault() {
|
|
251
|
+
function setupPreventDefault(addHandler) {
|
|
220
252
|
const elements = document.querySelectorAll('[data-hs-a11y="prevent-default"]');
|
|
221
|
-
|
|
253
|
+
|
|
222
254
|
elements.forEach(element => {
|
|
223
255
|
// Prevent click
|
|
224
|
-
|
|
256
|
+
const clickHandler = (e) => {
|
|
225
257
|
e.preventDefault();
|
|
226
258
|
e.stopPropagation();
|
|
227
259
|
return false;
|
|
228
|
-
}
|
|
229
|
-
|
|
260
|
+
};
|
|
261
|
+
addHandler(element, 'click', clickHandler);
|
|
262
|
+
|
|
230
263
|
// Prevent keyboard activation
|
|
231
|
-
|
|
264
|
+
const keydownHandler = (e) => {
|
|
232
265
|
if (e.key === 'Enter' || e.key === ' ') {
|
|
233
266
|
e.preventDefault();
|
|
234
267
|
e.stopPropagation();
|
|
235
268
|
return false;
|
|
236
269
|
}
|
|
237
|
-
}
|
|
238
|
-
|
|
270
|
+
};
|
|
271
|
+
addHandler(element, 'keydown', keydownHandler);
|
|
272
|
+
|
|
239
273
|
// Additional prevention for anchor links
|
|
240
274
|
if (element.tagName.toLowerCase() === 'a') {
|
|
241
275
|
// Remove or modify href to prevent scroll
|
|
@@ -247,27 +281,23 @@ export function init() {
|
|
|
247
281
|
element.setAttribute('tabindex', '0');
|
|
248
282
|
}
|
|
249
283
|
}
|
|
250
|
-
|
|
251
284
|
});
|
|
252
285
|
}
|
|
253
286
|
|
|
254
|
-
function setupSummaryAccessibility() {
|
|
287
|
+
function setupSummaryAccessibility(addHandler) {
|
|
255
288
|
const summaryContainers = document.querySelectorAll('[data-hs-a11y="summary-wrap"]');
|
|
256
289
|
|
|
257
|
-
|
|
258
290
|
summaryContainers.forEach((container, index) => {
|
|
259
|
-
|
|
260
291
|
const button = container.querySelector('[data-hs-a11y="summary-btn"]');
|
|
261
292
|
const contentWrapper = container.querySelector('[data-hs-a11y="summary-content"]');
|
|
262
|
-
|
|
293
|
+
|
|
263
294
|
if (!button || !contentWrapper) {
|
|
264
295
|
return;
|
|
265
296
|
}
|
|
266
|
-
|
|
267
|
-
|
|
297
|
+
|
|
268
298
|
const buttonId = `summary-button-${index}`;
|
|
269
299
|
const contentId = `summary-content-${index}`;
|
|
270
|
-
|
|
300
|
+
|
|
271
301
|
// Get original button text from first text node only
|
|
272
302
|
const walker = document.createTreeWalker(
|
|
273
303
|
button,
|
|
@@ -275,14 +305,14 @@ export function init() {
|
|
|
275
305
|
null,
|
|
276
306
|
false
|
|
277
307
|
);
|
|
278
|
-
|
|
308
|
+
|
|
279
309
|
let firstTextNode = walker.nextNode();
|
|
280
310
|
while (firstTextNode && !firstTextNode.textContent.trim()) {
|
|
281
311
|
firstTextNode = walker.nextNode();
|
|
282
312
|
}
|
|
283
|
-
|
|
313
|
+
|
|
284
314
|
const originalButtonText = firstTextNode ? firstTextNode.textContent.trim() : button.textContent.trim();
|
|
285
|
-
|
|
315
|
+
|
|
286
316
|
// Function to update all text nodes in button
|
|
287
317
|
function updateButtonText(newText) {
|
|
288
318
|
// Find all text nodes and update them
|
|
@@ -292,7 +322,7 @@ export function init() {
|
|
|
292
322
|
null,
|
|
293
323
|
false
|
|
294
324
|
);
|
|
295
|
-
|
|
325
|
+
|
|
296
326
|
const textNodes = [];
|
|
297
327
|
let node;
|
|
298
328
|
while (node = walker.nextNode()) {
|
|
@@ -300,27 +330,27 @@ export function init() {
|
|
|
300
330
|
textNodes.push(node);
|
|
301
331
|
}
|
|
302
332
|
}
|
|
303
|
-
|
|
333
|
+
|
|
304
334
|
textNodes.forEach(textNode => {
|
|
305
335
|
textNode.textContent = newText;
|
|
306
336
|
});
|
|
307
337
|
}
|
|
308
|
-
|
|
338
|
+
|
|
309
339
|
button.setAttribute('id', buttonId);
|
|
310
340
|
button.setAttribute('aria-expanded', 'false');
|
|
311
341
|
button.setAttribute('aria-controls', contentId);
|
|
312
342
|
button.setAttribute('aria-label', 'View Summary');
|
|
313
|
-
|
|
343
|
+
|
|
314
344
|
contentWrapper.setAttribute('id', contentId);
|
|
315
345
|
contentWrapper.setAttribute('aria-hidden', 'true');
|
|
316
346
|
contentWrapper.setAttribute('role', 'region');
|
|
317
347
|
contentWrapper.setAttribute('aria-labelledby', buttonId);
|
|
318
|
-
|
|
348
|
+
|
|
319
349
|
// Summary is closed by default - no need to check initial state
|
|
320
|
-
|
|
350
|
+
|
|
321
351
|
function toggleSummary() {
|
|
322
352
|
const isOpen = button.getAttribute('aria-expanded') === 'true';
|
|
323
|
-
|
|
353
|
+
|
|
324
354
|
if (isOpen) {
|
|
325
355
|
// Closing
|
|
326
356
|
button.setAttribute('aria-expanded', 'false');
|
|
@@ -334,11 +364,9 @@ export function init() {
|
|
|
334
364
|
updateButtonText('Close');
|
|
335
365
|
contentWrapper.setAttribute('aria-hidden', 'false');
|
|
336
366
|
}
|
|
337
|
-
|
|
338
367
|
}
|
|
339
|
-
|
|
340
|
-
button
|
|
341
|
-
|
|
368
|
+
|
|
369
|
+
addHandler(button, 'click', toggleSummary);
|
|
342
370
|
});
|
|
343
371
|
}
|
|
344
372
|
|
|
@@ -434,54 +462,56 @@ export function init() {
|
|
|
434
462
|
});
|
|
435
463
|
}
|
|
436
464
|
|
|
437
|
-
function setupClickForwarding() {
|
|
465
|
+
function setupClickForwarding(addHandler) {
|
|
438
466
|
// Find all clickable elements (custom styled elements users click)
|
|
439
467
|
const clickableElements = document.querySelectorAll('[data-hs-a11y*="clickable"]');
|
|
440
|
-
|
|
468
|
+
|
|
441
469
|
clickableElements.forEach(clickableElement => {
|
|
442
470
|
const attribute = clickableElement.getAttribute('data-hs-a11y');
|
|
443
|
-
|
|
471
|
+
|
|
444
472
|
// Parse the attribute: "click-trigger-[identifier], clickable"
|
|
445
473
|
const parts = attribute.split(',').map(part => part.trim());
|
|
446
|
-
|
|
474
|
+
|
|
447
475
|
// Find the part with click-trigger and the part with clickable
|
|
448
476
|
const triggerPart = parts.find(part => part.startsWith('click-trigger-'));
|
|
449
477
|
const rolePart = parts.find(part => part === 'clickable');
|
|
450
|
-
|
|
478
|
+
|
|
451
479
|
if (!triggerPart || !rolePart) {
|
|
452
480
|
return;
|
|
453
481
|
}
|
|
454
|
-
|
|
482
|
+
|
|
455
483
|
// Extract identifier from "click-trigger-[identifier]"
|
|
456
484
|
const identifier = triggerPart.replace('click-trigger-', '').trim();
|
|
457
|
-
|
|
485
|
+
|
|
458
486
|
// Find the corresponding trigger element
|
|
459
487
|
const triggerSelector = `[data-hs-a11y*="click-trigger-${identifier}"][data-hs-a11y*="trigger"]`;
|
|
460
488
|
const triggerElement = document.querySelector(triggerSelector);
|
|
461
|
-
|
|
489
|
+
|
|
462
490
|
if (!triggerElement) {
|
|
463
491
|
return;
|
|
464
492
|
}
|
|
465
|
-
|
|
493
|
+
|
|
466
494
|
// Add click event listener to forward clicks
|
|
467
|
-
|
|
495
|
+
const clickHandler = (event) => {
|
|
468
496
|
// Prevent default behavior on the clickable element
|
|
469
497
|
event.preventDefault();
|
|
470
498
|
event.stopPropagation();
|
|
471
|
-
|
|
499
|
+
|
|
472
500
|
// Trigger click on the target element
|
|
473
501
|
triggerElement.click();
|
|
474
|
-
}
|
|
475
|
-
|
|
502
|
+
};
|
|
503
|
+
addHandler(clickableElement, 'click', clickHandler);
|
|
504
|
+
|
|
476
505
|
// Also handle keyboard events for accessibility
|
|
477
|
-
|
|
506
|
+
const keydownHandler = (event) => {
|
|
478
507
|
if (event.key === 'Enter' || event.key === ' ') {
|
|
479
508
|
event.preventDefault();
|
|
480
509
|
event.stopPropagation();
|
|
481
510
|
triggerElement.click();
|
|
482
511
|
}
|
|
483
|
-
}
|
|
484
|
-
|
|
512
|
+
};
|
|
513
|
+
addHandler(clickableElement, 'keydown', keydownHandler);
|
|
514
|
+
|
|
485
515
|
// Ensure clickable element is keyboard accessible
|
|
486
516
|
if (!clickableElement.hasAttribute('tabindex')) {
|
|
487
517
|
clickableElement.setAttribute('tabindex', '0');
|
|
@@ -492,44 +522,44 @@ export function init() {
|
|
|
492
522
|
});
|
|
493
523
|
}
|
|
494
524
|
|
|
495
|
-
function setupTextSynchronization() {
|
|
525
|
+
function setupTextSynchronization(addObserver) {
|
|
496
526
|
// Find all original elements (source of truth)
|
|
497
527
|
const originalElements = document.querySelectorAll('[data-hs-a11y*="original"]');
|
|
498
|
-
|
|
528
|
+
|
|
499
529
|
originalElements.forEach(originalElement => {
|
|
500
530
|
const attribute = originalElement.getAttribute('data-hs-a11y');
|
|
501
|
-
|
|
531
|
+
|
|
502
532
|
// Parse the attribute: "match-text-[identifier], original"
|
|
503
533
|
const parts = attribute.split(',').map(part => part.trim());
|
|
504
|
-
|
|
534
|
+
|
|
505
535
|
// Find the part with match-text and the part with original
|
|
506
536
|
const textPart = parts.find(part => part.startsWith('match-text-'));
|
|
507
537
|
const rolePart = parts.find(part => part === 'original');
|
|
508
|
-
|
|
538
|
+
|
|
509
539
|
if (!textPart || !rolePart) {
|
|
510
540
|
return;
|
|
511
541
|
}
|
|
512
|
-
|
|
542
|
+
|
|
513
543
|
// Extract identifier from "match-text-[identifier]"
|
|
514
544
|
const identifier = textPart.replace('match-text-', '').trim();
|
|
515
|
-
|
|
545
|
+
|
|
516
546
|
// Find all corresponding match elements
|
|
517
547
|
const matchSelector = `[data-hs-a11y*="match-text-${identifier}"][data-hs-a11y*="match"]`;
|
|
518
548
|
const matchElements = document.querySelectorAll(matchSelector);
|
|
519
|
-
|
|
549
|
+
|
|
520
550
|
if (matchElements.length === 0) {
|
|
521
551
|
return;
|
|
522
552
|
}
|
|
523
|
-
|
|
553
|
+
|
|
524
554
|
// Function to synchronize text and aria-label
|
|
525
555
|
function synchronizeContent() {
|
|
526
556
|
const originalText = originalElement.textContent;
|
|
527
557
|
const originalAriaLabel = originalElement.getAttribute('aria-label');
|
|
528
|
-
|
|
558
|
+
|
|
529
559
|
matchElements.forEach(matchElement => {
|
|
530
560
|
// Copy text content
|
|
531
561
|
matchElement.textContent = originalText;
|
|
532
|
-
|
|
562
|
+
|
|
533
563
|
// Synchronize aria-label
|
|
534
564
|
if (originalAriaLabel) {
|
|
535
565
|
// If original has aria-label, copy it to match
|
|
@@ -542,27 +572,27 @@ export function init() {
|
|
|
542
572
|
}
|
|
543
573
|
});
|
|
544
574
|
}
|
|
545
|
-
|
|
575
|
+
|
|
546
576
|
// Initial synchronization
|
|
547
577
|
synchronizeContent();
|
|
548
|
-
|
|
578
|
+
|
|
549
579
|
// Set up MutationObserver to watch for changes
|
|
550
580
|
const observer = new MutationObserver((mutations) => {
|
|
551
581
|
let shouldSync = false;
|
|
552
|
-
|
|
582
|
+
|
|
553
583
|
mutations.forEach((mutation) => {
|
|
554
|
-
if (mutation.type === 'childList' ||
|
|
555
|
-
mutation.type === 'characterData' ||
|
|
584
|
+
if (mutation.type === 'childList' ||
|
|
585
|
+
mutation.type === 'characterData' ||
|
|
556
586
|
(mutation.type === 'attributes' && mutation.attributeName === 'aria-label')) {
|
|
557
587
|
shouldSync = true;
|
|
558
588
|
}
|
|
559
589
|
});
|
|
560
|
-
|
|
590
|
+
|
|
561
591
|
if (shouldSync) {
|
|
562
592
|
synchronizeContent();
|
|
563
593
|
}
|
|
564
594
|
});
|
|
565
|
-
|
|
595
|
+
|
|
566
596
|
// Observe text changes and attribute changes
|
|
567
597
|
observer.observe(originalElement, {
|
|
568
598
|
childList: true,
|
|
@@ -571,16 +601,16 @@ export function init() {
|
|
|
571
601
|
attributes: true,
|
|
572
602
|
attributeFilter: ['aria-label']
|
|
573
603
|
});
|
|
604
|
+
|
|
605
|
+
addObserver(observer);
|
|
574
606
|
});
|
|
575
607
|
}
|
|
576
608
|
|
|
577
|
-
function setupRichTextAccessibility() {
|
|
609
|
+
function setupRichTextAccessibility(addObserver, addHandler, cleanup) {
|
|
578
610
|
const contentAreas = document.querySelectorAll('[data-hs-a11y="rich-content"]');
|
|
579
611
|
const tocLists = document.querySelectorAll('[data-hs-a11y="rich-toc"]');
|
|
580
612
|
|
|
581
|
-
|
|
582
613
|
contentAreas.forEach((contentArea) => {
|
|
583
|
-
|
|
584
614
|
// Since there's only 1 content area and 1 TOC list per page, use the first TOC list
|
|
585
615
|
const tocList = tocLists[0];
|
|
586
616
|
|
|
@@ -592,7 +622,6 @@ export function init() {
|
|
|
592
622
|
return;
|
|
593
623
|
}
|
|
594
624
|
|
|
595
|
-
|
|
596
625
|
const template = tocList.children[0].cloneNode(true);
|
|
597
626
|
// Remove is-active class from template if it exists
|
|
598
627
|
const templateLink = template.querySelector("a");
|
|
@@ -643,7 +672,7 @@ export function init() {
|
|
|
643
672
|
link.appendChild(document.createTextNode(heading.textContent));
|
|
644
673
|
|
|
645
674
|
// Add click handler for smooth scrolling
|
|
646
|
-
|
|
675
|
+
const clickHandler = (e) => {
|
|
647
676
|
e.preventDefault();
|
|
648
677
|
|
|
649
678
|
const targetSection = document.getElementById(sectionId);
|
|
@@ -654,7 +683,8 @@ export function init() {
|
|
|
654
683
|
targetSection.focus();
|
|
655
684
|
}, 100);
|
|
656
685
|
}
|
|
657
|
-
}
|
|
686
|
+
};
|
|
687
|
+
addHandler(link, "click", clickHandler);
|
|
658
688
|
|
|
659
689
|
// Ensure sections are focusable for keyboard users but use CSS to control focus visibility
|
|
660
690
|
const targetSection = document.getElementById(sectionId);
|
|
@@ -719,17 +749,38 @@ export function init() {
|
|
|
719
749
|
|
|
720
750
|
// Observe all sections
|
|
721
751
|
sections.forEach(section => observer.observe(section));
|
|
752
|
+
addObserver(observer);
|
|
722
753
|
|
|
723
754
|
// Also update on scroll for smoother tracking
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
755
|
+
const scrollHandler = () => {
|
|
756
|
+
if (cleanup.scrollTimeout) clearTimeout(cleanup.scrollTimeout);
|
|
757
|
+
cleanup.scrollTimeout = setTimeout(updateActiveLink, 50);
|
|
758
|
+
};
|
|
759
|
+
addHandler(window, 'scroll', scrollHandler);
|
|
729
760
|
|
|
730
761
|
});
|
|
731
762
|
}
|
|
732
763
|
|
|
733
764
|
setupGeneralAccessibility();
|
|
734
|
-
|
|
765
|
+
|
|
766
|
+
return {
|
|
767
|
+
result: "accessibility initialized",
|
|
768
|
+
destroy: () => {
|
|
769
|
+
// Disconnect all observers
|
|
770
|
+
cleanup.observers.forEach(obs => obs.disconnect());
|
|
771
|
+
cleanup.observers.length = 0;
|
|
772
|
+
|
|
773
|
+
// Remove all event listeners
|
|
774
|
+
cleanup.handlers.forEach(({ element, event, handler, options }) => {
|
|
775
|
+
element.removeEventListener(event, handler, options);
|
|
776
|
+
});
|
|
777
|
+
cleanup.handlers.length = 0;
|
|
778
|
+
|
|
779
|
+
// Clear scroll timeout
|
|
780
|
+
if (cleanup.scrollTimeout) {
|
|
781
|
+
clearTimeout(cleanup.scrollTimeout);
|
|
782
|
+
cleanup.scrollTimeout = null;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
};
|
|
735
786
|
}
|
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
|
}
|