@hortonstudio/main 1.4.0 → 1.4.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/animations/text.js +180 -46
- package/autoInit/accessibility.js +184 -1
- package/autoInit/custom-values.js +4 -46
- package/autoInit/form.js +296 -0
- package/autoInit/navbar.js +11 -1
- package/index.js +1 -3
- package/package.json +1 -1
package/animations/text.js
CHANGED
@@ -106,14 +106,34 @@ async function waitForFonts() {
|
|
106
106
|
}
|
107
107
|
}
|
108
108
|
|
109
|
+
function findTextElement(container) {
|
110
|
+
// Find the deepest element that contains actual text
|
111
|
+
const walker = document.createTreeWalker(
|
112
|
+
container,
|
113
|
+
NodeFilter.SHOW_ELEMENT,
|
114
|
+
{
|
115
|
+
acceptNode: (node) => {
|
116
|
+
// Check if this element has direct text content (not just whitespace)
|
117
|
+
const hasDirectText = Array.from(node.childNodes).some(child =>
|
118
|
+
child.nodeType === Node.TEXT_NODE && child.textContent.trim()
|
119
|
+
);
|
120
|
+
return hasDirectText ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP;
|
121
|
+
}
|
122
|
+
}
|
123
|
+
);
|
124
|
+
|
125
|
+
return walker.nextNode() || container;
|
126
|
+
}
|
127
|
+
|
109
128
|
function showElementsWithoutAnimation() {
|
110
129
|
// Simply show all text elements without any animation or split text
|
111
130
|
const allTextElements = [
|
112
|
-
...document.querySelectorAll(
|
113
|
-
...document.querySelectorAll(
|
114
|
-
...document.querySelectorAll(
|
115
|
-
...document.querySelectorAll("
|
116
|
-
...document.querySelectorAll("
|
131
|
+
...document.querySelectorAll('[data-hs-anim="char"]'),
|
132
|
+
...document.querySelectorAll('[data-hs-anim="word"]'),
|
133
|
+
...document.querySelectorAll('[data-hs-anim="line"]'),
|
134
|
+
...document.querySelectorAll('[data-hs-anim="appear"]'),
|
135
|
+
...document.querySelectorAll('[data-hs-anim="reveal"]'),
|
136
|
+
...document.querySelectorAll('[data-hs-anim="group"]'),
|
117
137
|
];
|
118
138
|
|
119
139
|
allTextElements.forEach((element) => {
|
@@ -138,16 +158,22 @@ const CharSplitAnimations = {
|
|
138
158
|
return;
|
139
159
|
}
|
140
160
|
|
141
|
-
const elements = document.querySelectorAll(
|
161
|
+
const elements = document.querySelectorAll('[data-hs-anim="char"]');
|
142
162
|
|
143
|
-
elements.forEach((
|
163
|
+
elements.forEach((container) => {
|
164
|
+
// Skip if already processed
|
165
|
+
if (container.hasAttribute('data-split-processed')) return;
|
166
|
+
|
167
|
+
const textElement = findTextElement(container);
|
168
|
+
if (!textElement) return;
|
144
169
|
try {
|
145
170
|
const split = SplitText.create(textElement, {
|
146
171
|
type: "words,chars",
|
147
172
|
mask: "chars",
|
148
173
|
charsClass: "char",
|
149
174
|
});
|
150
|
-
|
175
|
+
container.splitTextInstance = split;
|
176
|
+
container.setAttribute('data-split-processed', 'true');
|
151
177
|
|
152
178
|
gsap.set(split.chars, {
|
153
179
|
yPercent: config.charSplit.yPercent,
|
@@ -168,20 +194,25 @@ const CharSplitAnimations = {
|
|
168
194
|
}
|
169
195
|
|
170
196
|
document
|
171
|
-
.querySelectorAll(
|
172
|
-
.forEach((
|
197
|
+
.querySelectorAll('[data-hs-anim="char"]')
|
198
|
+
.forEach((container) => {
|
199
|
+
const textElement = findTextElement(container);
|
200
|
+
if (!textElement) return;
|
173
201
|
try {
|
174
|
-
const chars =
|
202
|
+
const chars = container.querySelectorAll(".char");
|
175
203
|
const tl = gsap.timeline({
|
176
204
|
scrollTrigger: {
|
177
|
-
trigger:
|
205
|
+
trigger: container,
|
178
206
|
start: config.charSplit.start,
|
179
|
-
invalidateOnRefresh:
|
207
|
+
invalidateOnRefresh: true,
|
208
|
+
toggleActions: "play none none none",
|
209
|
+
once: true,
|
180
210
|
},
|
181
211
|
onComplete: () => {
|
182
|
-
if (
|
183
|
-
|
184
|
-
delete
|
212
|
+
if (container.splitTextInstance) {
|
213
|
+
container.splitTextInstance.revert();
|
214
|
+
delete container.splitTextInstance;
|
215
|
+
container.removeAttribute('data-split-processed');
|
185
216
|
}
|
186
217
|
},
|
187
218
|
});
|
@@ -209,16 +240,22 @@ const WordSplitAnimations = {
|
|
209
240
|
return;
|
210
241
|
}
|
211
242
|
|
212
|
-
const elements = document.querySelectorAll(
|
243
|
+
const elements = document.querySelectorAll('[data-hs-anim="word"]');
|
213
244
|
|
214
|
-
elements.forEach((
|
245
|
+
elements.forEach((container) => {
|
246
|
+
// Skip if already processed
|
247
|
+
if (container.hasAttribute('data-split-processed')) return;
|
248
|
+
|
249
|
+
const textElement = findTextElement(container);
|
250
|
+
if (!textElement) return;
|
215
251
|
try {
|
216
252
|
const split = SplitText.create(textElement, {
|
217
253
|
type: "words",
|
218
254
|
mask: "words",
|
219
255
|
wordsClass: "word",
|
220
256
|
});
|
221
|
-
|
257
|
+
container.splitTextInstance = split;
|
258
|
+
container.setAttribute('data-split-processed', 'true');
|
222
259
|
|
223
260
|
gsap.set(split.words, {
|
224
261
|
yPercent: config.wordSplit.yPercent,
|
@@ -239,20 +276,25 @@ const WordSplitAnimations = {
|
|
239
276
|
}
|
240
277
|
|
241
278
|
document
|
242
|
-
.querySelectorAll(
|
243
|
-
.forEach((
|
279
|
+
.querySelectorAll('[data-hs-anim="word"]')
|
280
|
+
.forEach((container) => {
|
281
|
+
const textElement = findTextElement(container);
|
282
|
+
if (!textElement) return;
|
244
283
|
try {
|
245
|
-
const words =
|
284
|
+
const words = container.querySelectorAll(".word");
|
246
285
|
const tl = gsap.timeline({
|
247
286
|
scrollTrigger: {
|
248
|
-
trigger:
|
287
|
+
trigger: container,
|
249
288
|
start: config.wordSplit.start,
|
250
|
-
invalidateOnRefresh:
|
289
|
+
invalidateOnRefresh: true,
|
290
|
+
toggleActions: "play none none none",
|
291
|
+
once: true,
|
251
292
|
},
|
252
293
|
onComplete: () => {
|
253
|
-
if (
|
254
|
-
|
255
|
-
delete
|
294
|
+
if (container.splitTextInstance) {
|
295
|
+
container.splitTextInstance.revert();
|
296
|
+
delete container.splitTextInstance;
|
297
|
+
container.removeAttribute('data-split-processed');
|
256
298
|
}
|
257
299
|
},
|
258
300
|
});
|
@@ -280,16 +322,22 @@ const LineSplitAnimations = {
|
|
280
322
|
return;
|
281
323
|
}
|
282
324
|
|
283
|
-
const elements = document.querySelectorAll(
|
325
|
+
const elements = document.querySelectorAll('[data-hs-anim="line"]');
|
284
326
|
|
285
|
-
elements.forEach((
|
327
|
+
elements.forEach((container) => {
|
328
|
+
// Skip if already processed
|
329
|
+
if (container.hasAttribute('data-split-processed')) return;
|
330
|
+
|
331
|
+
const textElement = findTextElement(container);
|
332
|
+
if (!textElement) return;
|
286
333
|
try {
|
287
334
|
const split = SplitText.create(textElement, {
|
288
335
|
type: "lines",
|
289
336
|
mask: "lines",
|
290
337
|
linesClass: "line",
|
291
338
|
});
|
292
|
-
|
339
|
+
container.splitTextInstance = split;
|
340
|
+
container.setAttribute('data-split-processed', 'true');
|
293
341
|
|
294
342
|
gsap.set(split.lines, {
|
295
343
|
yPercent: config.lineSplit.yPercent,
|
@@ -310,20 +358,25 @@ const LineSplitAnimations = {
|
|
310
358
|
}
|
311
359
|
|
312
360
|
document
|
313
|
-
.querySelectorAll(
|
314
|
-
.forEach((
|
361
|
+
.querySelectorAll('[data-hs-anim="line"]')
|
362
|
+
.forEach((container) => {
|
363
|
+
const textElement = findTextElement(container);
|
364
|
+
if (!textElement) return;
|
315
365
|
try {
|
316
|
-
const lines =
|
366
|
+
const lines = container.querySelectorAll(".line");
|
317
367
|
const tl = gsap.timeline({
|
318
368
|
scrollTrigger: {
|
319
|
-
trigger:
|
369
|
+
trigger: container,
|
320
370
|
start: config.lineSplit.start,
|
321
|
-
invalidateOnRefresh:
|
371
|
+
invalidateOnRefresh: true,
|
372
|
+
toggleActions: "play none none none",
|
373
|
+
once: true,
|
322
374
|
},
|
323
375
|
onComplete: () => {
|
324
|
-
if (
|
325
|
-
|
326
|
-
delete
|
376
|
+
if (container.splitTextInstance) {
|
377
|
+
container.splitTextInstance.revert();
|
378
|
+
delete container.splitTextInstance;
|
379
|
+
container.removeAttribute('data-split-processed');
|
327
380
|
}
|
328
381
|
},
|
329
382
|
});
|
@@ -351,7 +404,7 @@ const AppearAnimations = {
|
|
351
404
|
return;
|
352
405
|
}
|
353
406
|
|
354
|
-
const elements = document.querySelectorAll("
|
407
|
+
const elements = document.querySelectorAll('[data-hs-anim="appear"]');
|
355
408
|
elements.forEach((element) => {
|
356
409
|
try {
|
357
410
|
gsap.set(element, {
|
@@ -371,13 +424,15 @@ const AppearAnimations = {
|
|
371
424
|
return;
|
372
425
|
}
|
373
426
|
|
374
|
-
document.querySelectorAll("
|
427
|
+
document.querySelectorAll('[data-hs-anim="appear"]').forEach((element) => {
|
375
428
|
try {
|
376
429
|
const tl = gsap.timeline({
|
377
430
|
scrollTrigger: {
|
378
431
|
trigger: element,
|
379
432
|
start: config.appear.start,
|
380
|
-
invalidateOnRefresh:
|
433
|
+
invalidateOnRefresh: true,
|
434
|
+
toggleActions: "play none none none",
|
435
|
+
once: true,
|
381
436
|
},
|
382
437
|
});
|
383
438
|
|
@@ -404,7 +459,7 @@ const RevealAnimations = {
|
|
404
459
|
return;
|
405
460
|
}
|
406
461
|
|
407
|
-
const elements = document.querySelectorAll("
|
462
|
+
const elements = document.querySelectorAll('[data-hs-anim="reveal"]');
|
408
463
|
elements.forEach((element) => {
|
409
464
|
try {
|
410
465
|
gsap.set(element, {
|
@@ -424,13 +479,15 @@ const RevealAnimations = {
|
|
424
479
|
return;
|
425
480
|
}
|
426
481
|
|
427
|
-
document.querySelectorAll("
|
482
|
+
document.querySelectorAll('[data-hs-anim="reveal"]').forEach((element) => {
|
428
483
|
try {
|
429
484
|
const tl = gsap.timeline({
|
430
485
|
scrollTrigger: {
|
431
486
|
trigger: element,
|
432
487
|
start: config.reveal.start,
|
433
|
-
invalidateOnRefresh:
|
488
|
+
invalidateOnRefresh: true,
|
489
|
+
toggleActions: "play none none none",
|
490
|
+
once: true,
|
434
491
|
},
|
435
492
|
});
|
436
493
|
|
@@ -449,6 +506,64 @@ const RevealAnimations = {
|
|
449
506
|
},
|
450
507
|
};
|
451
508
|
|
509
|
+
const GroupAnimations = {
|
510
|
+
async initial() {
|
511
|
+
await waitForFonts();
|
512
|
+
|
513
|
+
if (prefersReducedMotion()) {
|
514
|
+
return;
|
515
|
+
}
|
516
|
+
|
517
|
+
const elements = document.querySelectorAll('[data-hs-anim="group"]');
|
518
|
+
elements.forEach((element) => {
|
519
|
+
try {
|
520
|
+
const children = Array.from(element.children);
|
521
|
+
gsap.set(children, {
|
522
|
+
y: config.appear.y,
|
523
|
+
opacity: 0,
|
524
|
+
});
|
525
|
+
} catch (error) {
|
526
|
+
console.warn("Error setting group initial state:", error);
|
527
|
+
}
|
528
|
+
});
|
529
|
+
},
|
530
|
+
|
531
|
+
async animate() {
|
532
|
+
await waitForFonts();
|
533
|
+
|
534
|
+
if (prefersReducedMotion()) {
|
535
|
+
return;
|
536
|
+
}
|
537
|
+
|
538
|
+
document.querySelectorAll('[data-hs-anim="group"]').forEach((element) => {
|
539
|
+
try {
|
540
|
+
const children = Array.from(element.children);
|
541
|
+
const tl = gsap.timeline({
|
542
|
+
scrollTrigger: {
|
543
|
+
trigger: element,
|
544
|
+
start: config.appear.start,
|
545
|
+
invalidateOnRefresh: true,
|
546
|
+
toggleActions: "play none none none",
|
547
|
+
once: true,
|
548
|
+
},
|
549
|
+
});
|
550
|
+
|
551
|
+
tl.to(children, {
|
552
|
+
y: 0,
|
553
|
+
opacity: 1,
|
554
|
+
duration: config.appear.duration,
|
555
|
+
ease: config.appear.ease,
|
556
|
+
stagger: 0.1,
|
557
|
+
});
|
558
|
+
|
559
|
+
activeAnimations.push({ timeline: tl, element: element });
|
560
|
+
} catch (error) {
|
561
|
+
console.warn("Error animating group:", error);
|
562
|
+
}
|
563
|
+
});
|
564
|
+
},
|
565
|
+
};
|
566
|
+
|
452
567
|
async function setInitialStates() {
|
453
568
|
await Promise.all([
|
454
569
|
CharSplitAnimations.initial(),
|
@@ -456,6 +571,7 @@ async function setInitialStates() {
|
|
456
571
|
LineSplitAnimations.initial(),
|
457
572
|
AppearAnimations.initial(),
|
458
573
|
RevealAnimations.initial(),
|
574
|
+
GroupAnimations.initial(),
|
459
575
|
]);
|
460
576
|
}
|
461
577
|
|
@@ -472,6 +588,7 @@ async function initAnimations() {
|
|
472
588
|
LineSplitAnimations.animate(),
|
473
589
|
AppearAnimations.animate(),
|
474
590
|
RevealAnimations.animate(),
|
591
|
+
GroupAnimations.animate(),
|
475
592
|
]);
|
476
593
|
}
|
477
594
|
|
@@ -483,7 +600,7 @@ export async function init() {
|
|
483
600
|
if (
|
484
601
|
elements.length === 0 &&
|
485
602
|
document.querySelectorAll(
|
486
|
-
"
|
603
|
+
'[data-hs-anim="char"]:not([data-initialized]), [data-hs-anim="word"]:not([data-initialized]), [data-hs-anim="line"]:not([data-initialized]), [data-hs-anim="appear"]:not([data-initialized]), [data-hs-anim="reveal"]:not([data-initialized]), [data-hs-anim="group"]:not([data-initialized])',
|
487
604
|
).length === 0
|
488
605
|
) {
|
489
606
|
return { result: "anim-text already initialized", destroy: () => {} };
|
@@ -493,7 +610,7 @@ export async function init() {
|
|
493
610
|
elements.forEach((el) => el.setAttribute("data-initialized", "true"));
|
494
611
|
document
|
495
612
|
.querySelectorAll(
|
496
|
-
"
|
613
|
+
'[data-hs-anim="char"], [data-hs-anim="word"], [data-hs-anim="line"], [data-hs-anim="appear"], [data-hs-anim="reveal"], [data-hs-anim="group"]',
|
497
614
|
)
|
498
615
|
.forEach((el) => {
|
499
616
|
el.setAttribute("data-initialized", "true");
|
@@ -525,6 +642,23 @@ export async function init() {
|
|
525
642
|
};
|
526
643
|
window.addEventListener("resize", resizeHandler);
|
527
644
|
|
645
|
+
// Add page load handler for proper ScrollTrigger refresh timing
|
646
|
+
const handlePageLoad = () => {
|
647
|
+
setTimeout(() => {
|
648
|
+
try {
|
649
|
+
ScrollTrigger.refresh();
|
650
|
+
} catch (error) {
|
651
|
+
console.warn("Error refreshing ScrollTrigger on page load:", error);
|
652
|
+
}
|
653
|
+
}, 100);
|
654
|
+
};
|
655
|
+
|
656
|
+
if (document.readyState === 'complete') {
|
657
|
+
handlePageLoad();
|
658
|
+
} else {
|
659
|
+
window.addEventListener('load', handlePageLoad);
|
660
|
+
}
|
661
|
+
|
528
662
|
// Initialize API with proper checks
|
529
663
|
if (!window[API_NAME]) {
|
530
664
|
window[API_NAME] = {};
|
@@ -3,7 +3,190 @@ export function init() {
|
|
3
3
|
// Stats accessibility has been moved to counter.js
|
4
4
|
|
5
5
|
function setupGeneralAccessibility() {
|
6
|
-
|
6
|
+
setupListAccessibility();
|
7
|
+
setupFAQAccessibility();
|
8
|
+
setupConvertToSpan();
|
9
|
+
setupYearReplacement();
|
10
|
+
setupPreventDefault();
|
11
|
+
}
|
12
|
+
|
13
|
+
function setupListAccessibility() {
|
14
|
+
const listElements = document.querySelectorAll('[data-hs-a11y="list"]');
|
15
|
+
const listItemElements = document.querySelectorAll('[data-hs-a11y="list-item"]');
|
16
|
+
|
17
|
+
listElements.forEach(element => {
|
18
|
+
element.setAttribute('role', 'list');
|
19
|
+
element.removeAttribute('data-hs-a11y');
|
20
|
+
});
|
21
|
+
|
22
|
+
listItemElements.forEach(element => {
|
23
|
+
element.setAttribute('role', 'listitem');
|
24
|
+
element.removeAttribute('data-hs-a11y');
|
25
|
+
});
|
26
|
+
}
|
27
|
+
|
28
|
+
function setupFAQAccessibility() {
|
29
|
+
const faqContainers = document.querySelectorAll('[data-hs-a11y="faq"]');
|
30
|
+
|
31
|
+
faqContainers.forEach((container, index) => {
|
32
|
+
const button = container.querySelector('button');
|
33
|
+
const contentWrapper = button.parentElement.nextElementSibling;
|
34
|
+
|
35
|
+
const buttonId = `faq-button-${index}`;
|
36
|
+
const contentId = `faq-content-${index}`;
|
37
|
+
|
38
|
+
button.setAttribute('id', buttonId);
|
39
|
+
button.setAttribute('aria-expanded', 'false');
|
40
|
+
button.setAttribute('aria-controls', contentId);
|
41
|
+
|
42
|
+
contentWrapper.setAttribute('id', contentId);
|
43
|
+
contentWrapper.setAttribute('aria-hidden', 'true');
|
44
|
+
contentWrapper.setAttribute('role', 'region');
|
45
|
+
contentWrapper.setAttribute('aria-labelledby', buttonId);
|
46
|
+
|
47
|
+
if (contentWrapper.style.height !== '0px') {
|
48
|
+
button.setAttribute('aria-expanded', 'true');
|
49
|
+
contentWrapper.setAttribute('aria-hidden', 'false');
|
50
|
+
}
|
51
|
+
|
52
|
+
function toggleFAQ() {
|
53
|
+
const isOpen = button.getAttribute('aria-expanded') === 'true';
|
54
|
+
|
55
|
+
button.setAttribute('aria-expanded', !isOpen);
|
56
|
+
contentWrapper.setAttribute('aria-hidden', isOpen);
|
57
|
+
}
|
58
|
+
|
59
|
+
button.addEventListener('click', toggleFAQ);
|
60
|
+
|
61
|
+
container.removeAttribute('data-hs-a11y');
|
62
|
+
});
|
63
|
+
}
|
64
|
+
|
65
|
+
function setupConvertToSpan() {
|
66
|
+
const containers = document.querySelectorAll('[data-hs-a11y="convert-span"]');
|
67
|
+
|
68
|
+
containers.forEach(container => {
|
69
|
+
const skipTags = [
|
70
|
+
'span', 'a', 'button', 'input', 'textarea', 'select', 'img', 'video', 'audio',
|
71
|
+
'iframe', 'object', 'embed', 'canvas', 'svg', 'form', 'table', 'thead', 'tbody',
|
72
|
+
'tr', 'td', 'th', 'ul', 'ol', 'li', 'dl', 'dt', 'dd', 'h1', 'h2', 'h3', 'h4',
|
73
|
+
'h5', 'h6', 'script', 'style', 'link', 'meta', 'title', 'head', 'html', 'body'
|
74
|
+
];
|
75
|
+
|
76
|
+
// Convert all child elements first
|
77
|
+
const elementsToConvert = container.querySelectorAll('*');
|
78
|
+
|
79
|
+
elementsToConvert.forEach(element => {
|
80
|
+
const tagName = element.tagName.toLowerCase();
|
81
|
+
|
82
|
+
if (!skipTags.includes(tagName)) {
|
83
|
+
const newSpan = document.createElement('span');
|
84
|
+
|
85
|
+
// Copy all attributes except data-hs-a11y
|
86
|
+
Array.from(element.attributes).forEach(attr => {
|
87
|
+
if (attr.name !== 'data-hs-a11y') {
|
88
|
+
newSpan.setAttribute(attr.name, attr.value);
|
89
|
+
}
|
90
|
+
});
|
91
|
+
|
92
|
+
// Move all child nodes
|
93
|
+
while (element.firstChild) {
|
94
|
+
newSpan.appendChild(element.firstChild);
|
95
|
+
}
|
96
|
+
|
97
|
+
// Replace the element
|
98
|
+
element.parentNode.replaceChild(newSpan, element);
|
99
|
+
}
|
100
|
+
});
|
101
|
+
|
102
|
+
// Convert the container itself to span
|
103
|
+
const containerTagName = container.tagName.toLowerCase();
|
104
|
+
if (!skipTags.includes(containerTagName)) {
|
105
|
+
const newSpan = document.createElement('span');
|
106
|
+
|
107
|
+
// Copy all attributes except data-hs-a11y
|
108
|
+
Array.from(container.attributes).forEach(attr => {
|
109
|
+
if (attr.name !== 'data-hs-a11y') {
|
110
|
+
newSpan.setAttribute(attr.name, attr.value);
|
111
|
+
}
|
112
|
+
});
|
113
|
+
|
114
|
+
// Move all child nodes
|
115
|
+
while (container.firstChild) {
|
116
|
+
newSpan.appendChild(container.firstChild);
|
117
|
+
}
|
118
|
+
|
119
|
+
// Replace the container
|
120
|
+
container.parentNode.replaceChild(newSpan, container);
|
121
|
+
} else {
|
122
|
+
// Just remove the attribute if container shouldn't be converted
|
123
|
+
container.removeAttribute('data-hs-a11y');
|
124
|
+
}
|
125
|
+
});
|
126
|
+
}
|
127
|
+
|
128
|
+
function setupYearReplacement() {
|
129
|
+
const currentYear = new Date().getFullYear().toString();
|
130
|
+
const walker = document.createTreeWalker(
|
131
|
+
document.body,
|
132
|
+
NodeFilter.SHOW_TEXT,
|
133
|
+
{
|
134
|
+
acceptNode: (node) => {
|
135
|
+
return node.textContent.includes('{{year}}')
|
136
|
+
? NodeFilter.FILTER_ACCEPT
|
137
|
+
: NodeFilter.FILTER_SKIP;
|
138
|
+
}
|
139
|
+
}
|
140
|
+
);
|
141
|
+
|
142
|
+
const textNodes = [];
|
143
|
+
let node;
|
144
|
+
while (node = walker.nextNode()) {
|
145
|
+
textNodes.push(node);
|
146
|
+
}
|
147
|
+
|
148
|
+
textNodes.forEach(textNode => {
|
149
|
+
const newText = textNode.textContent.replace(/\{\{year\}\}/gi, currentYear);
|
150
|
+
if (newText !== textNode.textContent) {
|
151
|
+
textNode.textContent = newText;
|
152
|
+
}
|
153
|
+
});
|
154
|
+
}
|
155
|
+
|
156
|
+
function setupPreventDefault() {
|
157
|
+
const elements = document.querySelectorAll('[data-hs-a11y="prevent-default"]');
|
158
|
+
|
159
|
+
elements.forEach(element => {
|
160
|
+
// Prevent click
|
161
|
+
element.addEventListener('click', (e) => {
|
162
|
+
e.preventDefault();
|
163
|
+
e.stopPropagation();
|
164
|
+
return false;
|
165
|
+
});
|
166
|
+
|
167
|
+
// Prevent keyboard activation
|
168
|
+
element.addEventListener('keydown', (e) => {
|
169
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
170
|
+
e.preventDefault();
|
171
|
+
e.stopPropagation();
|
172
|
+
return false;
|
173
|
+
}
|
174
|
+
});
|
175
|
+
|
176
|
+
// Additional prevention for anchor links
|
177
|
+
if (element.tagName.toLowerCase() === 'a') {
|
178
|
+
// Remove or modify href to prevent scroll
|
179
|
+
const originalHref = element.getAttribute('href');
|
180
|
+
if (originalHref && (originalHref === '#' || originalHref.startsWith('#'))) {
|
181
|
+
element.setAttribute('data-original-href', originalHref);
|
182
|
+
element.removeAttribute('href');
|
183
|
+
element.setAttribute('role', 'button');
|
184
|
+
element.setAttribute('tabindex', '0');
|
185
|
+
}
|
186
|
+
}
|
187
|
+
|
188
|
+
element.removeAttribute('data-hs-a11y');
|
189
|
+
});
|
7
190
|
}
|
8
191
|
|
9
192
|
setupGeneralAccessibility();
|
@@ -2,76 +2,62 @@ export function init() {
|
|
2
2
|
const customValues = new Map();
|
3
3
|
let isInitialized = false;
|
4
4
|
|
5
|
-
// Configuration for performance optimization
|
6
5
|
const config = {
|
7
|
-
// Attributes to search for placeholders
|
8
6
|
searchAttributes: [
|
9
7
|
'href', 'src', 'alt', 'title', 'aria-label', 'data-src',
|
10
8
|
'data-href', 'action', 'placeholder', 'value'
|
11
9
|
],
|
12
|
-
// Elements to exclude from search for performance
|
13
10
|
excludeSelectors: [
|
14
11
|
'script', 'style', 'meta', 'link', 'title', 'head',
|
15
12
|
'[data-hs-custom="list"]', '[data-hs-custom="name"]', '[data-hs-custom="value"]'
|
16
13
|
],
|
17
|
-
// Phone number formatting options
|
18
14
|
phoneFormatting: {
|
19
|
-
// Attributes that should use tel: format
|
20
15
|
telAttributes: ['href'],
|
21
|
-
// Pattern to detect phone numbers (matches various formats)
|
22
16
|
phonePattern: /^[\+]?[1-9]?[\d\s\-\(\)\.]{7,15}$/,
|
23
|
-
// Default country code (US/Canada)
|
24
17
|
defaultCountryCode: '+1',
|
25
|
-
// Clean phone for tel: links (remove all non-digits except +)
|
26
18
|
cleanForTel: (phone) => {
|
27
19
|
const cleaned = phone.replace(/[^\d+]/g, '');
|
28
|
-
// If no country code, add default
|
29
20
|
if (!cleaned.startsWith('+')) {
|
30
21
|
return config.phoneFormatting.defaultCountryCode + cleaned;
|
31
22
|
}
|
32
23
|
return cleaned;
|
33
24
|
},
|
34
|
-
// Format for display (keep original formatting)
|
35
25
|
formatForDisplay: (phone) => phone
|
36
26
|
}
|
37
27
|
};
|
38
28
|
|
39
|
-
// Detect if a value looks like a phone number
|
40
29
|
function isPhoneNumber(value) {
|
41
30
|
return config.phoneFormatting.phonePattern.test(value.trim());
|
42
31
|
}
|
43
32
|
|
44
|
-
// Format value based on context (attribute vs text content)
|
45
33
|
function formatValueForContext(value, isAttribute, attributeName) {
|
46
34
|
if (isPhoneNumber(value)) {
|
47
|
-
// For href attributes, clean the phone number (no tel: prefix)
|
48
35
|
if (isAttribute && config.phoneFormatting.telAttributes.includes(attributeName)) {
|
49
36
|
return config.phoneFormatting.cleanForTel(value);
|
50
37
|
} else {
|
51
|
-
// For display, keep original formatting
|
52
38
|
return config.phoneFormatting.formatForDisplay(value);
|
53
39
|
}
|
54
40
|
}
|
55
41
|
return value;
|
56
42
|
}
|
57
43
|
|
58
|
-
// Extract custom values from data attributes
|
59
44
|
function extractCustomValues() {
|
45
|
+
const currentYear = new Date().getFullYear().toString();
|
46
|
+
customValues.set('{{year}}', currentYear);
|
47
|
+
|
60
48
|
const customList = document.querySelector('[data-hs-custom="list"]');
|
61
49
|
if (!customList) {
|
62
|
-
return
|
50
|
+
return customValues.size > 0;
|
63
51
|
}
|
64
52
|
|
65
53
|
const nameElements = customList.querySelectorAll('[data-hs-custom="name"]');
|
66
54
|
const valueElements = customList.querySelectorAll('[data-hs-custom="value"]');
|
67
55
|
|
68
|
-
// Build mapping from name/value pairs
|
69
56
|
for (let i = 0; i < Math.min(nameElements.length, valueElements.length); i++) {
|
70
57
|
const name = nameElements[i].textContent.trim();
|
71
58
|
const value = valueElements[i].textContent.trim();
|
72
59
|
|
73
60
|
if (name && value) {
|
74
|
-
// Store with lowercase key for case-insensitive matching
|
75
61
|
const key = `{{${name.toLowerCase()}}}`;
|
76
62
|
customValues.set(key, value);
|
77
63
|
}
|
@@ -80,18 +66,15 @@ export function init() {
|
|
80
66
|
return customValues.size > 0;
|
81
67
|
}
|
82
68
|
|
83
|
-
// Replace placeholders in text content
|
84
69
|
function replaceInText(text, isAttribute = false, attributeName = null) {
|
85
70
|
if (!text || typeof text !== 'string') return text;
|
86
71
|
|
87
72
|
let result = text;
|
88
73
|
|
89
74
|
customValues.forEach((value, placeholder) => {
|
90
|
-
// Create case-insensitive regex for exact placeholder match
|
91
75
|
const regex = new RegExp(placeholder.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi');
|
92
76
|
const matches = text.match(regex);
|
93
77
|
if (matches) {
|
94
|
-
// Format value based on context (phone numbers get special treatment)
|
95
78
|
const formattedValue = formatValueForContext(value, isAttribute, attributeName);
|
96
79
|
result = result.replace(regex, formattedValue);
|
97
80
|
}
|
@@ -100,7 +83,6 @@ export function init() {
|
|
100
83
|
return result;
|
101
84
|
}
|
102
85
|
|
103
|
-
// Replace placeholders in all attributes of an element
|
104
86
|
function replaceInAttributes(element) {
|
105
87
|
config.searchAttributes.forEach(attr => {
|
106
88
|
const value = element.getAttribute(attr);
|
@@ -113,26 +95,22 @@ export function init() {
|
|
113
95
|
});
|
114
96
|
}
|
115
97
|
|
116
|
-
// Check if element should be excluded from processing
|
117
98
|
function shouldExcludeElement(element) {
|
118
99
|
return config.excludeSelectors.some(selector => {
|
119
100
|
return element.matches(selector);
|
120
101
|
});
|
121
102
|
}
|
122
103
|
|
123
|
-
// Process text nodes for placeholder replacement
|
124
104
|
function processTextNodes(element) {
|
125
105
|
const walker = document.createTreeWalker(
|
126
106
|
element,
|
127
107
|
NodeFilter.SHOW_TEXT,
|
128
108
|
{
|
129
109
|
acceptNode: (node) => {
|
130
|
-
// Skip if parent element should be excluded
|
131
110
|
if (shouldExcludeElement(node.parentElement)) {
|
132
111
|
return NodeFilter.FILTER_REJECT;
|
133
112
|
}
|
134
113
|
|
135
|
-
// Only process text nodes with placeholder patterns
|
136
114
|
return node.textContent.includes('{{') && node.textContent.includes('}}')
|
137
115
|
? NodeFilter.FILTER_ACCEPT
|
138
116
|
: NodeFilter.FILTER_SKIP;
|
@@ -146,7 +124,6 @@ export function init() {
|
|
146
124
|
textNodes.push(node);
|
147
125
|
}
|
148
126
|
|
149
|
-
// Replace placeholders in collected text nodes
|
150
127
|
textNodes.forEach(textNode => {
|
151
128
|
const originalText = textNode.textContent;
|
152
129
|
const newText = replaceInText(originalText);
|
@@ -156,7 +133,6 @@ export function init() {
|
|
156
133
|
});
|
157
134
|
}
|
158
135
|
|
159
|
-
// Process all elements for attribute replacement
|
160
136
|
function processElements(container) {
|
161
137
|
const elements = container.querySelectorAll('*');
|
162
138
|
|
@@ -167,21 +143,14 @@ export function init() {
|
|
167
143
|
});
|
168
144
|
}
|
169
145
|
|
170
|
-
// Main replacement function
|
171
146
|
function performReplacements() {
|
172
147
|
if (customValues.size === 0) return;
|
173
148
|
|
174
|
-
// Process text content
|
175
149
|
processTextNodes(document.body);
|
176
|
-
|
177
|
-
// Process element attributes
|
178
150
|
processElements(document.body);
|
179
|
-
|
180
|
-
// Also check document root attributes
|
181
151
|
replaceInAttributes(document.documentElement);
|
182
152
|
}
|
183
153
|
|
184
|
-
// Remove the custom values list from DOM
|
185
154
|
function cleanupCustomList() {
|
186
155
|
const customList = document.querySelector('[data-hs-custom="list"]');
|
187
156
|
if (customList) {
|
@@ -189,7 +158,6 @@ export function init() {
|
|
189
158
|
}
|
190
159
|
}
|
191
160
|
|
192
|
-
// Handle dynamic content with MutationObserver
|
193
161
|
function setupDynamicContentHandler() {
|
194
162
|
const observer = new MutationObserver((mutations) => {
|
195
163
|
let hasNewContent = false;
|
@@ -198,7 +166,6 @@ export function init() {
|
|
198
166
|
if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
|
199
167
|
mutation.addedNodes.forEach((node) => {
|
200
168
|
if (node.nodeType === Node.ELEMENT_NODE) {
|
201
|
-
// Check if new content contains placeholders
|
202
169
|
const hasPlaceholders = node.textContent.includes('{{') && node.textContent.includes('}}');
|
203
170
|
const hasAttributePlaceholders = config.searchAttributes.some(attr => {
|
204
171
|
const value = node.getAttribute?.(attr);
|
@@ -214,7 +181,6 @@ export function init() {
|
|
214
181
|
});
|
215
182
|
|
216
183
|
if (hasNewContent && customValues.size > 0) {
|
217
|
-
// Debounce replacements for performance
|
218
184
|
clearTimeout(observer.timeout);
|
219
185
|
observer.timeout = setTimeout(() => {
|
220
186
|
performReplacements();
|
@@ -230,21 +196,14 @@ export function init() {
|
|
230
196
|
return observer;
|
231
197
|
}
|
232
198
|
|
233
|
-
// Initialize the custom values system
|
234
199
|
function initializeCustomValues() {
|
235
200
|
if (isInitialized) return;
|
236
201
|
|
237
|
-
// Extract custom values from data attributes
|
238
202
|
const hasCustomValues = extractCustomValues();
|
239
203
|
|
240
204
|
if (hasCustomValues) {
|
241
|
-
// Perform initial replacements
|
242
205
|
performReplacements();
|
243
|
-
|
244
|
-
// Clean up the custom list
|
245
206
|
cleanupCustomList();
|
246
|
-
|
247
|
-
// Set up dynamic content handling
|
248
207
|
setupDynamicContentHandler();
|
249
208
|
|
250
209
|
isInitialized = true;
|
@@ -261,6 +220,5 @@ export function init() {
|
|
261
220
|
}
|
262
221
|
}
|
263
222
|
|
264
|
-
// Initialize on page load
|
265
223
|
return initializeCustomValues();
|
266
224
|
}
|
package/autoInit/form.js
CHANGED
@@ -27,6 +27,302 @@ export function init() {
|
|
27
27
|
let originalErrorTemplate = null;
|
28
28
|
let isValidating = false;
|
29
29
|
|
30
|
+
// Simple Custom Select Component for Webflow
|
31
|
+
(function() {
|
32
|
+
'use strict';
|
33
|
+
|
34
|
+
// Initialize all custom selects on the page
|
35
|
+
function initCustomSelects() {
|
36
|
+
// Unwrap any divs inside select elements (Webflow component slots)
|
37
|
+
document.querySelectorAll('select > div').forEach(div => {
|
38
|
+
const select = div.parentElement;
|
39
|
+
while (div.firstChild) {
|
40
|
+
select.appendChild(div.firstChild);
|
41
|
+
}
|
42
|
+
div.remove();
|
43
|
+
});
|
44
|
+
|
45
|
+
const selectWrappers = document.querySelectorAll('[data-hs-form="select"]');
|
46
|
+
|
47
|
+
selectWrappers.forEach(wrapper => {
|
48
|
+
initSingleSelect(wrapper);
|
49
|
+
});
|
50
|
+
}
|
51
|
+
|
52
|
+
// Initialize a single custom select
|
53
|
+
function initSingleSelect(wrapper) {
|
54
|
+
// Find all required elements
|
55
|
+
const realSelect = wrapper.querySelector('select');
|
56
|
+
if (!realSelect) return;
|
57
|
+
|
58
|
+
const selectName = realSelect.getAttribute('name') || 'custom-select';
|
59
|
+
const customList = wrapper.querySelector('[data-hs-form="select-list"]');
|
60
|
+
const button = wrapper.querySelector('button') || wrapper.querySelector('[role="button"]');
|
61
|
+
|
62
|
+
if (!customList || !button) return;
|
63
|
+
|
64
|
+
// Get and clone the option template
|
65
|
+
const optionTemplate = customList.firstElementChild;
|
66
|
+
if (!optionTemplate) return;
|
67
|
+
|
68
|
+
const templateClone = optionTemplate.cloneNode(true);
|
69
|
+
optionTemplate.remove();
|
70
|
+
|
71
|
+
// Build options from real select
|
72
|
+
const realOptions = realSelect.querySelectorAll('option');
|
73
|
+
realOptions.forEach((option, index) => {
|
74
|
+
const optionElement = templateClone.cloneNode(true);
|
75
|
+
const textSpan = optionElement.querySelector('span');
|
76
|
+
|
77
|
+
if (textSpan) {
|
78
|
+
textSpan.textContent = option.textContent;
|
79
|
+
}
|
80
|
+
|
81
|
+
// Add attributes
|
82
|
+
optionElement.setAttribute('data-value', option.value);
|
83
|
+
optionElement.setAttribute('role', 'option');
|
84
|
+
optionElement.setAttribute('id', `${selectName}-option-${index}`);
|
85
|
+
optionElement.setAttribute('tabindex', '-1');
|
86
|
+
|
87
|
+
// Set selected state if this option is selected
|
88
|
+
if (option.selected) {
|
89
|
+
optionElement.setAttribute('aria-selected', 'true');
|
90
|
+
// Update button text
|
91
|
+
const buttonText = button.querySelector('span') || button;
|
92
|
+
if (buttonText.tagName === 'SPAN') {
|
93
|
+
buttonText.textContent = option.textContent;
|
94
|
+
}
|
95
|
+
} else {
|
96
|
+
optionElement.setAttribute('aria-selected', 'false');
|
97
|
+
}
|
98
|
+
|
99
|
+
customList.appendChild(optionElement);
|
100
|
+
});
|
101
|
+
|
102
|
+
// Add ARIA attributes
|
103
|
+
customList.setAttribute('role', 'listbox');
|
104
|
+
customList.setAttribute('id', `${selectName}-listbox`);
|
105
|
+
|
106
|
+
button.setAttribute('role', 'combobox');
|
107
|
+
button.setAttribute('aria-haspopup', 'listbox');
|
108
|
+
button.setAttribute('aria-controls', `${selectName}-listbox`);
|
109
|
+
button.setAttribute('aria-expanded', 'false');
|
110
|
+
button.setAttribute('id', `${selectName}-button`);
|
111
|
+
|
112
|
+
// Find and connect label if exists
|
113
|
+
const label = wrapper.querySelector('label') ||
|
114
|
+
document.querySelector(`label[for="${realSelect.id}"]`);
|
115
|
+
if (label) {
|
116
|
+
const labelId = label.id || `${selectName}-label`;
|
117
|
+
label.id = labelId;
|
118
|
+
// Ensure real select has proper ID for label connection
|
119
|
+
if (!realSelect.id) {
|
120
|
+
realSelect.id = `${selectName}-select`;
|
121
|
+
}
|
122
|
+
label.setAttribute('for', realSelect.id);
|
123
|
+
button.setAttribute('aria-labelledby', labelId);
|
124
|
+
}
|
125
|
+
|
126
|
+
// Track state
|
127
|
+
let currentIndex = -1;
|
128
|
+
let isOpen = false;
|
129
|
+
|
130
|
+
// Update expanded state
|
131
|
+
function updateExpandedState(expanded) {
|
132
|
+
isOpen = expanded;
|
133
|
+
button.setAttribute('aria-expanded', expanded.toString());
|
134
|
+
}
|
135
|
+
|
136
|
+
// Focus option by index
|
137
|
+
function focusOption(index) {
|
138
|
+
const options = customList.querySelectorAll('[role="option"]');
|
139
|
+
if (index < 0 || index >= options.length) return;
|
140
|
+
|
141
|
+
// Remove previous focus
|
142
|
+
options.forEach(opt => {
|
143
|
+
opt.classList.remove('focused');
|
144
|
+
opt.setAttribute('tabindex', '-1');
|
145
|
+
});
|
146
|
+
|
147
|
+
// Add new focus
|
148
|
+
currentIndex = index;
|
149
|
+
options[index].classList.add('focused');
|
150
|
+
options[index].setAttribute('tabindex', '0');
|
151
|
+
options[index].focus();
|
152
|
+
button.setAttribute('aria-activedescendant', options[index].id);
|
153
|
+
}
|
154
|
+
|
155
|
+
// Select option
|
156
|
+
function selectOption(optionElement) {
|
157
|
+
const value = optionElement.getAttribute('data-value');
|
158
|
+
const text = optionElement.querySelector('span')?.textContent || optionElement.textContent;
|
159
|
+
|
160
|
+
// Update real select
|
161
|
+
realSelect.value = value;
|
162
|
+
realSelect.dispatchEvent(new Event('change', { bubbles: true }));
|
163
|
+
|
164
|
+
// Update button text
|
165
|
+
const buttonText = button.querySelector('span') || button;
|
166
|
+
if (buttonText.tagName === 'SPAN') {
|
167
|
+
buttonText.textContent = text;
|
168
|
+
}
|
169
|
+
|
170
|
+
// Update aria-selected
|
171
|
+
customList.querySelectorAll('[role="option"]').forEach(opt => {
|
172
|
+
opt.setAttribute('aria-selected', 'false');
|
173
|
+
});
|
174
|
+
optionElement.setAttribute('aria-selected', 'true');
|
175
|
+
|
176
|
+
// Click the button to close
|
177
|
+
button.click();
|
178
|
+
}
|
179
|
+
|
180
|
+
// Button keyboard events
|
181
|
+
button.addEventListener('keydown', (e) => {
|
182
|
+
switch(e.key) {
|
183
|
+
case ' ':
|
184
|
+
case 'Enter':
|
185
|
+
e.preventDefault();
|
186
|
+
button.click();
|
187
|
+
break;
|
188
|
+
|
189
|
+
case 'ArrowDown':
|
190
|
+
e.preventDefault();
|
191
|
+
if (!isOpen) {
|
192
|
+
button.click();
|
193
|
+
} else {
|
194
|
+
focusOption(0);
|
195
|
+
}
|
196
|
+
break;
|
197
|
+
|
198
|
+
case 'ArrowUp':
|
199
|
+
e.preventDefault();
|
200
|
+
if (isOpen) {
|
201
|
+
const options = customList.querySelectorAll('[role="option"]');
|
202
|
+
focusOption(options.length - 1);
|
203
|
+
}
|
204
|
+
break;
|
205
|
+
|
206
|
+
case 'Escape':
|
207
|
+
if (isOpen) {
|
208
|
+
e.preventDefault();
|
209
|
+
button.click();
|
210
|
+
}
|
211
|
+
break;
|
212
|
+
}
|
213
|
+
});
|
214
|
+
|
215
|
+
// Option keyboard events (delegated)
|
216
|
+
customList.addEventListener('keydown', (e) => {
|
217
|
+
const option = e.target.closest('[role="option"]');
|
218
|
+
if (!option) return;
|
219
|
+
|
220
|
+
const options = Array.from(customList.querySelectorAll('[role="option"]'));
|
221
|
+
const currentIdx = options.indexOf(option);
|
222
|
+
|
223
|
+
switch(e.key) {
|
224
|
+
case 'ArrowDown':
|
225
|
+
e.preventDefault();
|
226
|
+
if (currentIdx < options.length - 1) {
|
227
|
+
focusOption(currentIdx + 1);
|
228
|
+
}
|
229
|
+
break;
|
230
|
+
|
231
|
+
case 'ArrowUp':
|
232
|
+
e.preventDefault();
|
233
|
+
if (currentIdx === 0) {
|
234
|
+
button.click();
|
235
|
+
button.focus();
|
236
|
+
} else {
|
237
|
+
focusOption(currentIdx - 1);
|
238
|
+
}
|
239
|
+
break;
|
240
|
+
|
241
|
+
case 'Enter':
|
242
|
+
case ' ':
|
243
|
+
e.preventDefault();
|
244
|
+
selectOption(option);
|
245
|
+
break;
|
246
|
+
|
247
|
+
case 'Escape':
|
248
|
+
e.preventDefault();
|
249
|
+
button.click();
|
250
|
+
button.focus();
|
251
|
+
break;
|
252
|
+
}
|
253
|
+
});
|
254
|
+
|
255
|
+
// Option click events
|
256
|
+
customList.addEventListener('click', (e) => {
|
257
|
+
const option = e.target.closest('[role="option"]');
|
258
|
+
if (option) {
|
259
|
+
selectOption(option);
|
260
|
+
}
|
261
|
+
});
|
262
|
+
|
263
|
+
// Track open/close state
|
264
|
+
const observer = new MutationObserver((mutations) => {
|
265
|
+
mutations.forEach((mutation) => {
|
266
|
+
if (mutation.type === 'attributes') {
|
267
|
+
// Check if dropdown is visible
|
268
|
+
const isVisible = !customList.hidden &&
|
269
|
+
customList.style.display !== 'none' &&
|
270
|
+
!customList.classList.contains('hidden');
|
271
|
+
|
272
|
+
updateExpandedState(isVisible);
|
273
|
+
|
274
|
+
if (!isVisible) {
|
275
|
+
currentIndex = -1;
|
276
|
+
button.removeAttribute('aria-activedescendant');
|
277
|
+
// Return focus to button if it was in the list
|
278
|
+
if (document.activeElement?.closest('[data-hs-form="select-list"]') === customList) {
|
279
|
+
button.focus();
|
280
|
+
}
|
281
|
+
}
|
282
|
+
}
|
283
|
+
});
|
284
|
+
});
|
285
|
+
|
286
|
+
// Observe the custom list for visibility changes
|
287
|
+
observer.observe(customList, {
|
288
|
+
attributes: true,
|
289
|
+
attributeFilter: ['hidden', 'style', 'class']
|
290
|
+
});
|
291
|
+
|
292
|
+
// Sync with real select changes
|
293
|
+
realSelect.addEventListener('change', () => {
|
294
|
+
const selectedOption = realSelect.options[realSelect.selectedIndex];
|
295
|
+
if (selectedOption) {
|
296
|
+
const customOption = customList.querySelector(`[data-value="${selectedOption.value}"]`);
|
297
|
+
if (customOption) {
|
298
|
+
// Update button text
|
299
|
+
const text = customOption.querySelector('span')?.textContent || customOption.textContent;
|
300
|
+
const buttonText = button.querySelector('span') || button;
|
301
|
+
if (buttonText.tagName === 'SPAN') {
|
302
|
+
buttonText.textContent = text;
|
303
|
+
}
|
304
|
+
|
305
|
+
// Update aria-selected
|
306
|
+
customList.querySelectorAll('[role="option"]').forEach(opt => {
|
307
|
+
opt.setAttribute('aria-selected', 'false');
|
308
|
+
});
|
309
|
+
customOption.setAttribute('aria-selected', 'true');
|
310
|
+
}
|
311
|
+
}
|
312
|
+
});
|
313
|
+
}
|
314
|
+
|
315
|
+
// Initialize on DOM ready
|
316
|
+
if (document.readyState === 'loading') {
|
317
|
+
document.addEventListener('DOMContentLoaded', initCustomSelects);
|
318
|
+
} else {
|
319
|
+
initCustomSelects();
|
320
|
+
}
|
321
|
+
|
322
|
+
// Reinitialize for dynamic content
|
323
|
+
window.initCustomSelects = initCustomSelects;
|
324
|
+
})();
|
325
|
+
|
30
326
|
const initializeForms = () => {
|
31
327
|
const forms = document.querySelectorAll(config.selectors.form);
|
32
328
|
forms.forEach(form => {
|
package/autoInit/navbar.js
CHANGED
@@ -363,6 +363,16 @@ function setupDynamicDropdowns() {
|
|
363
363
|
}
|
364
364
|
});
|
365
365
|
|
366
|
+
toggle.addEventListener("click", function(e) {
|
367
|
+
if (e.isTrusted) {
|
368
|
+
// This is a real user click - prevent it
|
369
|
+
e.preventDefault();
|
370
|
+
e.stopPropagation();
|
371
|
+
return false;
|
372
|
+
}
|
373
|
+
// Programmatic clicks (from hover/keyboard) proceed normally
|
374
|
+
});
|
375
|
+
|
366
376
|
document.addEventListener("click", function (e) {
|
367
377
|
if (!wrapper.contains(e.target) && isOpen) {
|
368
378
|
closeDropdown();
|
@@ -726,7 +736,7 @@ function setupMobileMenuButton() {
|
|
726
736
|
|
727
737
|
// Cleanup function for window.mobileMenuState
|
728
738
|
if (typeof window !== "undefined") {
|
729
|
-
window.addEventListener("
|
739
|
+
window.addEventListener("pagehide", () => {
|
730
740
|
if (window.mobileMenuState) {
|
731
741
|
delete window.mobileMenuState;
|
732
742
|
}
|
package/index.js
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
// Version:1.4.
|
1
|
+
// Version:1.4.2
|
2
2
|
|
3
3
|
const API_NAME = "hsmain";
|
4
4
|
|
@@ -31,7 +31,6 @@ const initializeHsMain = async () => {
|
|
31
31
|
navbar: true,
|
32
32
|
accessibility: true,
|
33
33
|
counter: true,
|
34
|
-
"custom-values": true,
|
35
34
|
form: true,
|
36
35
|
};
|
37
36
|
|
@@ -55,7 +54,6 @@ const initializeHsMain = async () => {
|
|
55
54
|
navbar: () => import("./autoInit/navbar.js"),
|
56
55
|
accessibility: () => import("./autoInit/accessibility.js"),
|
57
56
|
counter: () => import("./autoInit/counter.js"),
|
58
|
-
"custom-values": () => import("./autoInit/custom-values.js"),
|
59
57
|
form: () => import("./autoInit/form.js"),
|
60
58
|
};
|
61
59
|
|