@hortonstudio/main 1.2.31 → 1.2.35
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/.claude/settings.local.json +3 -1
- package/animations/hero.js +5 -9
- package/autoInit/accessibility.js +71 -0
- package/{utils → autoInit}/navbar.js +231 -22
- package/index.js +9 -5
- package/package.json +1 -1
- package/utils/before-after.js +387 -0
@@ -37,7 +37,9 @@
|
|
37
37
|
"Bash(grep:*)",
|
38
38
|
"Bash(git reset:*)",
|
39
39
|
"Bash(git checkout:*)",
|
40
|
-
"Bash(rg:*)"
|
40
|
+
"Bash(rg:*)",
|
41
|
+
"Bash(/usr/local/lib/node_modules/@anthropic-ai/claude-code/vendor/ripgrep/arm64-darwin/rg -n \"data-hs-heroconfig\" /Users/devan/Documents/custom-code/main/animations/hero.js)",
|
42
|
+
"Bash(/usr/local/lib/node_modules/@anthropic-ai/claude-code/vendor/ripgrep/arm64-darwin/rg -n \"data-hs-config\" /Users/devan/Documents/custom-code/main/animations/hero.js)"
|
41
43
|
],
|
42
44
|
"deny": []
|
43
45
|
}
|
package/animations/hero.js
CHANGED
@@ -205,7 +205,7 @@ export async function init() {
|
|
205
205
|
const appearElements = document.querySelectorAll('[data-hs-hero="appear"]');
|
206
206
|
|
207
207
|
// Check if nav has advanced config
|
208
|
-
const hasAdvancedNav = navElement && navElement.hasAttribute('data-hs-
|
208
|
+
const hasAdvancedNav = navElement && navElement.hasAttribute('data-hs-config') && navElement.getAttribute('data-hs-config') === 'advanced';
|
209
209
|
|
210
210
|
// First child elements - only select if advanced nav is enabled
|
211
211
|
const navButton = [];
|
@@ -224,7 +224,7 @@ export async function init() {
|
|
224
224
|
subheadingElements.forEach(el => {
|
225
225
|
if (el.firstElementChild) {
|
226
226
|
// Get the heroconfig attribute to determine animation type (default to appear)
|
227
|
-
const heroConfig = el.getAttribute('data-hs-
|
227
|
+
const heroConfig = el.getAttribute('data-hs-config') || 'appear';
|
228
228
|
|
229
229
|
if (heroConfig === 'appear') {
|
230
230
|
subheadingAppearElements.push(el.firstElementChild);
|
@@ -243,7 +243,7 @@ export async function init() {
|
|
243
243
|
headingElements.forEach(el => {
|
244
244
|
if (el.firstElementChild) {
|
245
245
|
// Get the heroconfig attribute to determine animation type
|
246
|
-
const heroConfig = el.getAttribute('data-hs-
|
246
|
+
const heroConfig = el.getAttribute('data-hs-config') || 'line'; // default to line if not specified
|
247
247
|
|
248
248
|
if (heroConfig === 'appear') {
|
249
249
|
headingAppearElements.push(el.firstElementChild);
|
@@ -319,7 +319,7 @@ export async function init() {
|
|
319
319
|
if (heading.length > 0) {
|
320
320
|
headingSplitElements.forEach((parent, index) => {
|
321
321
|
const textElement = heading[index];
|
322
|
-
const splitType = parent.getAttribute('data-hs-
|
322
|
+
const splitType = parent.getAttribute('data-hs-config') || 'line';
|
323
323
|
|
324
324
|
let splitConfig = {};
|
325
325
|
let elementsClass = '';
|
@@ -360,7 +360,7 @@ export async function init() {
|
|
360
360
|
if (subheading.length > 0) {
|
361
361
|
subheadingSplitElements.forEach((parent, index) => {
|
362
362
|
const textElement = subheading[index];
|
363
|
-
const splitType = parent.getAttribute('data-hs-
|
363
|
+
const splitType = parent.getAttribute('data-hs-config') || 'word';
|
364
364
|
|
365
365
|
let splitConfig = {};
|
366
366
|
let elementsClass = '';
|
@@ -513,9 +513,7 @@ export async function init() {
|
|
513
513
|
ease: config.headingSplit.ease,
|
514
514
|
onComplete: () => {
|
515
515
|
if (split && split.revert) {
|
516
|
-
setTimeout(() => {
|
517
516
|
split.revert();
|
518
|
-
}, 100);
|
519
517
|
}
|
520
518
|
}
|
521
519
|
},
|
@@ -534,9 +532,7 @@ export async function init() {
|
|
534
532
|
ease: config.subheadingSplit.ease,
|
535
533
|
onComplete: () => {
|
536
534
|
if (split && split.revert) {
|
537
|
-
setTimeout(() => {
|
538
535
|
split.revert();
|
539
|
-
}, 100);
|
540
536
|
}
|
541
537
|
}
|
542
538
|
},
|
@@ -0,0 +1,71 @@
|
|
1
|
+
export function init() {
|
2
|
+
function setupStatsAccessibility() {
|
3
|
+
const statsElements = document.querySelectorAll('[data-hs-accessibility="stats"]');
|
4
|
+
|
5
|
+
statsElements.forEach(element => {
|
6
|
+
// Get all text content from the element and its children
|
7
|
+
const textContent = extractTextContent(element);
|
8
|
+
|
9
|
+
if (textContent) {
|
10
|
+
element.setAttribute('aria-label', textContent);
|
11
|
+
element.setAttribute('role', 'img');
|
12
|
+
|
13
|
+
// Debug: log what we found
|
14
|
+
console.log('Stats element aria-label:', textContent);
|
15
|
+
}
|
16
|
+
});
|
17
|
+
}
|
18
|
+
|
19
|
+
function extractTextContent(element) {
|
20
|
+
const textParts = [];
|
21
|
+
|
22
|
+
function processNode(node) {
|
23
|
+
// Skip elements with aria-hidden="true"
|
24
|
+
if (node.nodeType === Node.ELEMENT_NODE &&
|
25
|
+
node.getAttribute('aria-hidden') === 'true') {
|
26
|
+
return;
|
27
|
+
}
|
28
|
+
|
29
|
+
// If element has data-original-text, use that instead of current text
|
30
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
31
|
+
const originalText = node.getAttribute('data-original-text');
|
32
|
+
if (originalText) {
|
33
|
+
textParts.push(originalText);
|
34
|
+
return; // Don't process children
|
35
|
+
}
|
36
|
+
}
|
37
|
+
|
38
|
+
// Process text nodes
|
39
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
40
|
+
const text = node.textContent.trim();
|
41
|
+
if (text && text.length > 0) {
|
42
|
+
textParts.push(text);
|
43
|
+
}
|
44
|
+
}
|
45
|
+
|
46
|
+
// Process child nodes
|
47
|
+
if (node.childNodes) {
|
48
|
+
node.childNodes.forEach(processNode);
|
49
|
+
}
|
50
|
+
}
|
51
|
+
|
52
|
+
processNode(element);
|
53
|
+
|
54
|
+
// Combine all text parts with spaces and clean up
|
55
|
+
return textParts
|
56
|
+
.join(' ')
|
57
|
+
.replace(/\s+/g, ' ')
|
58
|
+
.trim();
|
59
|
+
}
|
60
|
+
|
61
|
+
function init() {
|
62
|
+
// Wait a bit for counter scripts to initialize and set data-original-text
|
63
|
+
setTimeout(() => {
|
64
|
+
setupStatsAccessibility();
|
65
|
+
}, 100);
|
66
|
+
|
67
|
+
return { result: 'accessibility initialized' };
|
68
|
+
}
|
69
|
+
|
70
|
+
return init();
|
71
|
+
}
|
@@ -1,10 +1,83 @@
|
|
1
|
+
// Global accessibility state
|
2
|
+
let supportsInert = null;
|
3
|
+
let screenReaderLiveRegion = null;
|
4
|
+
|
1
5
|
export const init = () => {
|
6
|
+
setupAccessibilityFeatures();
|
2
7
|
setupDynamicDropdowns();
|
3
8
|
setupMobileMenuButton();
|
4
9
|
setupMobileMenuARIA();
|
10
|
+
setupMobileMenuBreakpointHandler();
|
5
11
|
return { result: 'navbar initialized' };
|
6
12
|
};
|
7
13
|
|
14
|
+
// Accessibility features setup
|
15
|
+
function setupAccessibilityFeatures() {
|
16
|
+
// Check inert support once
|
17
|
+
supportsInert = 'inert' in HTMLElement.prototype;
|
18
|
+
|
19
|
+
// Create screen reader live region
|
20
|
+
screenReaderLiveRegion = document.createElement('div');
|
21
|
+
screenReaderLiveRegion.setAttribute('aria-live', 'polite');
|
22
|
+
screenReaderLiveRegion.setAttribute('aria-atomic', 'true');
|
23
|
+
screenReaderLiveRegion.className = 'u-sr-only';
|
24
|
+
screenReaderLiveRegion.style.cssText = 'position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;';
|
25
|
+
document.body.appendChild(screenReaderLiveRegion);
|
26
|
+
}
|
27
|
+
|
28
|
+
// Inert polyfill for browsers that don't support it
|
29
|
+
function setElementInert(element, isInert) {
|
30
|
+
if (supportsInert) {
|
31
|
+
element.inert = isInert;
|
32
|
+
} else {
|
33
|
+
// Polyfill: manage tabindex for all focusable elements
|
34
|
+
const focusableSelectors = 'a, button, input, select, textarea, [tabindex]:not([tabindex="-1"])';
|
35
|
+
const focusableElements = element.querySelectorAll(focusableSelectors);
|
36
|
+
|
37
|
+
if (isInert) {
|
38
|
+
// Store original tabindex values and disable
|
39
|
+
focusableElements.forEach(el => {
|
40
|
+
const currentTabindex = el.getAttribute('tabindex');
|
41
|
+
el.setAttribute('data-inert-tabindex', currentTabindex || '0');
|
42
|
+
el.setAttribute('tabindex', '-1');
|
43
|
+
});
|
44
|
+
element.setAttribute('data-inert', 'true');
|
45
|
+
} else {
|
46
|
+
// Restore original tabindex values
|
47
|
+
focusableElements.forEach(el => {
|
48
|
+
const originalTabindex = el.getAttribute('data-inert-tabindex');
|
49
|
+
if (originalTabindex === '0') {
|
50
|
+
el.removeAttribute('tabindex');
|
51
|
+
} else if (originalTabindex) {
|
52
|
+
el.setAttribute('tabindex', originalTabindex);
|
53
|
+
}
|
54
|
+
el.removeAttribute('data-inert-tabindex');
|
55
|
+
});
|
56
|
+
element.removeAttribute('data-inert');
|
57
|
+
}
|
58
|
+
}
|
59
|
+
}
|
60
|
+
|
61
|
+
// Screen reader announcements
|
62
|
+
function announceToScreenReader(message) {
|
63
|
+
if (screenReaderLiveRegion) {
|
64
|
+
screenReaderLiveRegion.textContent = message;
|
65
|
+
|
66
|
+
// Clear after a delay to allow for repeat announcements
|
67
|
+
setTimeout(() => {
|
68
|
+
screenReaderLiveRegion.textContent = '';
|
69
|
+
}, 1000);
|
70
|
+
}
|
71
|
+
}
|
72
|
+
|
73
|
+
// Extract menu name from element text or aria-label
|
74
|
+
function getMenuName(element) {
|
75
|
+
const text = element.textContent?.trim() ||
|
76
|
+
element.getAttribute('aria-label') ||
|
77
|
+
'menu';
|
78
|
+
return text.replace(/^(Open|Close)\s+/i, '').replace(/\s+(menu|navigation)$/i, '');
|
79
|
+
}
|
80
|
+
|
8
81
|
// Desktop dropdown system
|
9
82
|
function setupDynamicDropdowns() {
|
10
83
|
const dropdownWrappers = document.querySelectorAll('[data-hs-nav="dropdown"]');
|
@@ -67,6 +140,11 @@ function setupDynamicDropdowns() {
|
|
67
140
|
menuItems.forEach(item => {
|
68
141
|
item.setAttribute('tabindex', '0');
|
69
142
|
});
|
143
|
+
|
144
|
+
// Announce to screen readers
|
145
|
+
const menuName = getMenuName(toggle);
|
146
|
+
announceToScreenReader(`${menuName} menu opened`);
|
147
|
+
|
70
148
|
const clickEvent = new MouseEvent('click', {
|
71
149
|
bubbles: true,
|
72
150
|
cancelable: true,
|
@@ -88,6 +166,11 @@ function setupDynamicDropdowns() {
|
|
88
166
|
menuItems.forEach(item => {
|
89
167
|
item.setAttribute('tabindex', '-1');
|
90
168
|
});
|
169
|
+
|
170
|
+
// Announce to screen readers
|
171
|
+
const menuName = getMenuName(toggle);
|
172
|
+
announceToScreenReader(`${menuName} menu closed`);
|
173
|
+
|
91
174
|
const clickEvent = new MouseEvent('click', {
|
92
175
|
bubbles: true,
|
93
176
|
cancelable: true,
|
@@ -340,7 +423,7 @@ function addDesktopArrowNavigation() {
|
|
340
423
|
});
|
341
424
|
}
|
342
425
|
|
343
|
-
// Mobile menu button system
|
426
|
+
// Mobile menu button system with modal-like functionality
|
344
427
|
function setupMobileMenuButton() {
|
345
428
|
const menuButton = document.querySelector('[data-hs-nav="menubtn"]');
|
346
429
|
const mobileMenu = document.querySelector('[data-hs-nav="menu"]');
|
@@ -356,16 +439,39 @@ function setupMobileMenuButton() {
|
|
356
439
|
mobileMenu.id = menuId;
|
357
440
|
mobileMenu.setAttribute('role', 'dialog');
|
358
441
|
mobileMenu.setAttribute('aria-modal', 'true');
|
359
|
-
mobileMenu
|
442
|
+
setElementInert(mobileMenu, true);
|
360
443
|
|
361
444
|
let isMenuOpen = false;
|
362
445
|
|
446
|
+
function shouldPreventMobileMenu() {
|
447
|
+
const menuHideElement = document.querySelector('.menu-hide.is-mobile');
|
448
|
+
if (!menuHideElement) return false;
|
449
|
+
|
450
|
+
const computedStyle = window.getComputedStyle(menuHideElement);
|
451
|
+
return computedStyle.display === 'none';
|
452
|
+
}
|
453
|
+
|
363
454
|
function openMenu() {
|
364
|
-
if (isMenuOpen) return;
|
455
|
+
if (isMenuOpen || shouldPreventMobileMenu()) return;
|
365
456
|
isMenuOpen = true;
|
457
|
+
|
458
|
+
// Add body overflow hidden class
|
459
|
+
document.body.classList.add('u-overflow-hidden');
|
460
|
+
|
461
|
+
// Add blur effect to modal blur elements
|
462
|
+
document.querySelectorAll('[data-hs-nav="modal-blur"]').forEach(element => {
|
463
|
+
element.style.display = 'block';
|
464
|
+
element.style.opacity = '0.5';
|
465
|
+
element.style.transition = 'opacity 0.3s ease';
|
466
|
+
});
|
467
|
+
|
366
468
|
menuButton.setAttribute('aria-expanded', 'true');
|
367
469
|
menuButton.setAttribute('aria-label', 'Close navigation menu');
|
368
|
-
mobileMenu
|
470
|
+
setElementInert(mobileMenu, false);
|
471
|
+
|
472
|
+
// Announce to screen readers
|
473
|
+
const menuName = getMenuName(menuButton);
|
474
|
+
announceToScreenReader(`${menuName} opened`);
|
369
475
|
|
370
476
|
// Prevent tabbing outside navbar using tabindex management
|
371
477
|
const navbarWrapper = document.querySelector('[data-hs-nav="wrapper"]') ||
|
@@ -392,12 +498,29 @@ function setupMobileMenuButton() {
|
|
392
498
|
function closeMenu() {
|
393
499
|
if (!isMenuOpen) return;
|
394
500
|
isMenuOpen = false;
|
501
|
+
|
502
|
+
// Remove body overflow hidden class
|
503
|
+
document.body.classList.remove('u-overflow-hidden');
|
504
|
+
|
505
|
+
// Remove blur effect from modal blur elements
|
506
|
+
document.querySelectorAll('[data-hs-nav="modal-blur"]').forEach(element => {
|
507
|
+
element.style.opacity = '0';
|
508
|
+
element.style.transition = 'opacity 0.3s ease';
|
509
|
+
setTimeout(() => {
|
510
|
+
element.style.display = 'none';
|
511
|
+
}, 300);
|
512
|
+
});
|
513
|
+
|
395
514
|
if (mobileMenu.contains(document.activeElement)) {
|
396
515
|
menuButton.focus();
|
397
516
|
}
|
398
517
|
menuButton.setAttribute('aria-expanded', 'false');
|
399
518
|
menuButton.setAttribute('aria-label', 'Open navigation menu');
|
400
|
-
mobileMenu
|
519
|
+
setElementInert(mobileMenu, true);
|
520
|
+
|
521
|
+
// Announce to screen readers
|
522
|
+
const menuName = getMenuName(menuButton);
|
523
|
+
announceToScreenReader(`${menuName} closed`);
|
401
524
|
|
402
525
|
// Restore tabbing to entire page using tabindex management
|
403
526
|
const elementsToRestore = document.querySelectorAll('[data-mobile-menu-tabindex]');
|
@@ -420,6 +543,8 @@ function setupMobileMenuButton() {
|
|
420
543
|
}
|
421
544
|
|
422
545
|
function toggleMenu() {
|
546
|
+
if (shouldPreventMobileMenu()) return;
|
547
|
+
|
423
548
|
if (isMenuOpen) {
|
424
549
|
closeMenu();
|
425
550
|
} else {
|
@@ -450,15 +575,43 @@ function setupMobileMenuButton() {
|
|
450
575
|
|
451
576
|
menuButton.addEventListener('click', function(e) {
|
452
577
|
if (!e.isTrusted) return;
|
578
|
+
|
579
|
+
if (shouldPreventMobileMenu()) return;
|
580
|
+
|
453
581
|
if (isMenuOpen && mobileMenu.contains(document.activeElement)) {
|
454
582
|
menuButton.focus();
|
455
583
|
}
|
456
|
-
|
584
|
+
|
585
|
+
const newMenuState = !isMenuOpen;
|
586
|
+
isMenuOpen = newMenuState;
|
587
|
+
|
588
|
+
// Handle body overflow class
|
589
|
+
if (isMenuOpen) {
|
590
|
+
document.body.classList.add('u-overflow-hidden');
|
591
|
+
} else {
|
592
|
+
document.body.classList.remove('u-overflow-hidden');
|
593
|
+
}
|
594
|
+
|
595
|
+
// Handle blur effect
|
596
|
+
document.querySelectorAll('[data-hs-nav="modal-blur"]').forEach(element => {
|
597
|
+
if (isMenuOpen) {
|
598
|
+
element.style.display = 'block';
|
599
|
+
element.style.opacity = '0.5';
|
600
|
+
element.style.transition = 'opacity 0.3s ease';
|
601
|
+
} else {
|
602
|
+
element.style.opacity = '0';
|
603
|
+
element.style.transition = 'opacity 0.3s ease';
|
604
|
+
setTimeout(() => {
|
605
|
+
element.style.display = 'none';
|
606
|
+
}, 300);
|
607
|
+
}
|
608
|
+
});
|
609
|
+
|
457
610
|
menuButton.setAttribute('aria-expanded', isMenuOpen);
|
458
611
|
menuButton.setAttribute('aria-label',
|
459
612
|
isMenuOpen ? 'Close navigation menu' : 'Open navigation menu'
|
460
613
|
);
|
461
|
-
mobileMenu
|
614
|
+
setElementInert(mobileMenu, !isMenuOpen);
|
462
615
|
|
463
616
|
// Handle tabindex management for external clicks
|
464
617
|
if (isMenuOpen) {
|
@@ -487,6 +640,53 @@ function setupMobileMenuButton() {
|
|
487
640
|
});
|
488
641
|
}
|
489
642
|
});
|
643
|
+
|
644
|
+
// Store the menu state and functions for breakpoint handler
|
645
|
+
window.mobileMenuState = {
|
646
|
+
isMenuOpen: () => isMenuOpen,
|
647
|
+
closeMenu: closeMenu,
|
648
|
+
openMenu: openMenu
|
649
|
+
};
|
650
|
+
}
|
651
|
+
|
652
|
+
// Mobile menu breakpoint handler
|
653
|
+
function setupMobileMenuBreakpointHandler() {
|
654
|
+
let preventedMenuState = false;
|
655
|
+
|
656
|
+
function handleBreakpointChange() {
|
657
|
+
const menuHideElement = document.querySelector('.menu-hide.is-mobile');
|
658
|
+
if (!menuHideElement) return;
|
659
|
+
|
660
|
+
const computedStyle = window.getComputedStyle(menuHideElement);
|
661
|
+
const shouldPrevent = computedStyle.display === 'none';
|
662
|
+
|
663
|
+
if (!window.mobileMenuState) return;
|
664
|
+
|
665
|
+
if (shouldPrevent && window.mobileMenuState.isMenuOpen()) {
|
666
|
+
// Store that the menu was open before being prevented
|
667
|
+
preventedMenuState = true;
|
668
|
+
window.mobileMenuState.closeMenu();
|
669
|
+
} else if (!shouldPrevent && preventedMenuState) {
|
670
|
+
// Restore menu state if it was open before being prevented
|
671
|
+
preventedMenuState = false;
|
672
|
+
window.mobileMenuState.openMenu();
|
673
|
+
}
|
674
|
+
}
|
675
|
+
|
676
|
+
// Use ResizeObserver for more accurate detection
|
677
|
+
if (window.ResizeObserver) {
|
678
|
+
const resizeObserver = new ResizeObserver(handleBreakpointChange);
|
679
|
+
const menuHideElement = document.querySelector('.menu-hide.is-mobile');
|
680
|
+
if (menuHideElement) {
|
681
|
+
resizeObserver.observe(menuHideElement);
|
682
|
+
}
|
683
|
+
}
|
684
|
+
|
685
|
+
// Fallback to resize event
|
686
|
+
window.addEventListener('resize', handleBreakpointChange);
|
687
|
+
|
688
|
+
// Initial check
|
689
|
+
handleBreakpointChange();
|
490
690
|
}
|
491
691
|
|
492
692
|
function sanitizeForID(text) {
|
@@ -518,30 +718,38 @@ function setupMobileMenuARIA() {
|
|
518
718
|
button.setAttribute('aria-expanded', 'false');
|
519
719
|
button.setAttribute('aria-controls', listId);
|
520
720
|
|
521
|
-
|
522
|
-
|
523
|
-
|
524
|
-
dropdownList = button.parentElement?.nextElementSibling;
|
525
|
-
}
|
721
|
+
// Look for dropdown list in the same container as the button
|
722
|
+
let dropdownList = null;
|
723
|
+
const buttonContainer = button.closest('.menu-card_dropdown, .menu_contain, [data-hs-nav="menu"]');
|
526
724
|
|
527
|
-
if (
|
528
|
-
|
529
|
-
|
530
|
-
|
531
|
-
|
532
|
-
|
533
|
-
|
725
|
+
if (buttonContainer) {
|
726
|
+
// First try to find a list element within the same container
|
727
|
+
dropdownList = buttonContainer.querySelector('.menu-card_list, .dropdown-list, [role="menu"]');
|
728
|
+
|
729
|
+
// If not found, look for any element with multiple links that's not the button itself
|
730
|
+
if (!dropdownList || !dropdownList.querySelector('a')) {
|
731
|
+
const allListElements = buttonContainer.querySelectorAll('div, ul, nav');
|
732
|
+
dropdownList = Array.from(allListElements).find(el =>
|
733
|
+
el.querySelectorAll('a').length > 1 &&
|
734
|
+
!el.contains(button) &&
|
735
|
+
el !== button
|
736
|
+
);
|
737
|
+
}
|
534
738
|
}
|
535
739
|
|
536
740
|
if (dropdownList && dropdownList.querySelector('a')) {
|
537
741
|
dropdownList.id = listId;
|
538
|
-
dropdownList
|
742
|
+
setElementInert(dropdownList, true);
|
539
743
|
|
540
744
|
button.addEventListener('click', function() {
|
541
745
|
const isExpanded = button.getAttribute('aria-expanded') === 'true';
|
542
746
|
const newState = !isExpanded;
|
543
747
|
button.setAttribute('aria-expanded', newState);
|
544
|
-
dropdownList
|
748
|
+
setElementInert(dropdownList, !newState);
|
749
|
+
|
750
|
+
// Announce to screen readers
|
751
|
+
const menuName = getMenuName(button);
|
752
|
+
announceToScreenReader(`${menuName} submenu ${newState ? 'opened' : 'closed'}`);
|
545
753
|
});
|
546
754
|
}
|
547
755
|
});
|
@@ -565,7 +773,8 @@ function setupMobileMenuArrowNavigation(menuContainer) {
|
|
565
773
|
return Array.from(allElements).filter(el => {
|
566
774
|
let current = el;
|
567
775
|
while (current && current !== menuContainer) {
|
568
|
-
|
776
|
+
// Check both native inert and polyfill inert
|
777
|
+
if (current.inert === true || current.getAttribute('data-inert') === 'true') {
|
569
778
|
return false;
|
570
779
|
}
|
571
780
|
current = current.parentElement;
|
package/index.js
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
// Version:1.2.
|
1
|
+
// Version:1.2.35
|
2
2
|
|
3
3
|
const API_NAME = 'hsmain';
|
4
4
|
|
@@ -18,12 +18,14 @@ const initializeHsMain = async () => {
|
|
18
18
|
const utilityModules = {
|
19
19
|
'data-hs-util-toc': true,
|
20
20
|
'data-hs-util-progress': true,
|
21
|
-
'data-hs-util-
|
21
|
+
'data-hs-util-ba': true
|
22
22
|
};
|
23
23
|
|
24
24
|
const autoInitModules = {
|
25
25
|
'smooth-scroll': true,
|
26
|
-
'modal': true
|
26
|
+
'modal': true,
|
27
|
+
'navbar': true,
|
28
|
+
'accessibility': true
|
27
29
|
};
|
28
30
|
|
29
31
|
const allDataAttributes = { ...animationModules, ...utilityModules };
|
@@ -47,9 +49,11 @@ const initializeHsMain = async () => {
|
|
47
49
|
'data-hs-anim-transition': () => import('./animations/transition.js'),
|
48
50
|
'data-hs-util-toc': () => import('./utils/toc.js'),
|
49
51
|
'data-hs-util-progress': () => import('./utils/scroll-progress.js'),
|
50
|
-
'data-hs-util-
|
52
|
+
'data-hs-util-ba': () => import('./utils/before-after.js'),
|
51
53
|
'smooth-scroll': () => import('./autoInit/smooth-scroll.js'),
|
52
|
-
'modal': () => import('./autoInit/modal.js')
|
54
|
+
'modal': () => import('./autoInit/modal.js'),
|
55
|
+
'navbar': () => import('./autoInit/navbar.js'),
|
56
|
+
'accessibility': () => import('./autoInit/accessibility.js')
|
53
57
|
};
|
54
58
|
|
55
59
|
let scripts = [...document.querySelectorAll(`script[type="module"][src="${import.meta.url}"]`)];
|
package/package.json
CHANGED
@@ -0,0 +1,387 @@
|
|
1
|
+
export function init() {
|
2
|
+
const config = {
|
3
|
+
defaultMode: 'split',
|
4
|
+
sliderPosition: 50,
|
5
|
+
touchSensitivity: 1,
|
6
|
+
keyboardStep: 5,
|
7
|
+
autoPlay: false,
|
8
|
+
autoPlayInterval: 5000
|
9
|
+
};
|
10
|
+
|
11
|
+
let instances = [];
|
12
|
+
let currentSlideIndex = {};
|
13
|
+
let isDragging = false;
|
14
|
+
let dragInstance = null;
|
15
|
+
|
16
|
+
function updateConfig(newConfig) {
|
17
|
+
Object.assign(config, newConfig);
|
18
|
+
}
|
19
|
+
|
20
|
+
function initInstance(wrapper) {
|
21
|
+
const instanceId = instances.length;
|
22
|
+
const items = Array.from(wrapper.children);
|
23
|
+
|
24
|
+
if (items.length === 0) return;
|
25
|
+
|
26
|
+
const instance = {
|
27
|
+
id: instanceId,
|
28
|
+
wrapper,
|
29
|
+
items,
|
30
|
+
currentIndex: 0,
|
31
|
+
mode: config.defaultMode,
|
32
|
+
sliderPosition: config.sliderPosition,
|
33
|
+
previousSliderPosition: config.sliderPosition
|
34
|
+
};
|
35
|
+
|
36
|
+
instances.push(instance);
|
37
|
+
currentSlideIndex[instanceId] = 0;
|
38
|
+
|
39
|
+
setupInstance(instance);
|
40
|
+
showSlide(instance, 0);
|
41
|
+
|
42
|
+
return instance;
|
43
|
+
}
|
44
|
+
|
45
|
+
function setupInstance(instance) {
|
46
|
+
const { wrapper, items } = instance;
|
47
|
+
|
48
|
+
items.forEach((item, index) => {
|
49
|
+
item.style.display = index === 0 ? 'block' : 'none';
|
50
|
+
|
51
|
+
// Set default clip path for after image
|
52
|
+
const afterImage = item.querySelector('[data-hs-ba="image-after"]');
|
53
|
+
if (afterImage) {
|
54
|
+
afterImage.style.clipPath = `polygon(${config.sliderPosition}% 0%, 100% 0%, 100% 100%, ${config.sliderPosition}% 100%)`;
|
55
|
+
}
|
56
|
+
|
57
|
+
setupItemInteractions(instance, item, index);
|
58
|
+
});
|
59
|
+
|
60
|
+
setupKeyboardNavigation(instance);
|
61
|
+
}
|
62
|
+
|
63
|
+
function setupItemInteractions(instance, item, itemIndex) {
|
64
|
+
const modeButtons = item.querySelectorAll('[data-hs-ba^="mode-"]');
|
65
|
+
const leftArrow = item.querySelector('[data-hs-ba="left"]');
|
66
|
+
const rightArrow = item.querySelector('[data-hs-ba="right"]');
|
67
|
+
const slider = item.querySelector('[data-hs-ba="slider"]');
|
68
|
+
const pagination = item.querySelector('[data-hs-ba="pagination"]');
|
69
|
+
|
70
|
+
modeButtons.forEach(button => {
|
71
|
+
const mode = button.getAttribute('data-hs-ba').replace('mode-', '');
|
72
|
+
button.addEventListener('click', (e) => {
|
73
|
+
e.preventDefault();
|
74
|
+
|
75
|
+
// If clicking split mode when already in split mode, reset to default position
|
76
|
+
if (mode === 'split' && instance.mode === 'split') {
|
77
|
+
instance.sliderPosition = config.sliderPosition;
|
78
|
+
}
|
79
|
+
|
80
|
+
setMode(instance, itemIndex, mode);
|
81
|
+
});
|
82
|
+
});
|
83
|
+
|
84
|
+
if (leftArrow) {
|
85
|
+
leftArrow.addEventListener('click', (e) => {
|
86
|
+
e.preventDefault();
|
87
|
+
navigateSlide(instance, -1);
|
88
|
+
});
|
89
|
+
}
|
90
|
+
|
91
|
+
if (rightArrow) {
|
92
|
+
rightArrow.addEventListener('click', (e) => {
|
93
|
+
e.preventDefault();
|
94
|
+
navigateSlide(instance, 1);
|
95
|
+
});
|
96
|
+
}
|
97
|
+
|
98
|
+
if (slider) {
|
99
|
+
slider.style.cursor = 'grab';
|
100
|
+
setupSliderDragging(instance, slider, itemIndex);
|
101
|
+
}
|
102
|
+
|
103
|
+
if (pagination) {
|
104
|
+
setupPagination(instance, pagination);
|
105
|
+
}
|
106
|
+
}
|
107
|
+
|
108
|
+
function setMode(instance, itemIndex, mode) {
|
109
|
+
const item = instance.items[itemIndex];
|
110
|
+
const afterImage = item.querySelector('[data-hs-ba="image-after"]');
|
111
|
+
const slider = item.querySelector('.ba-slider');
|
112
|
+
const sliderHandle = item.querySelector('[data-hs-ba="slider"]');
|
113
|
+
const modeButtons = item.querySelectorAll('[data-hs-ba^="mode-"]');
|
114
|
+
|
115
|
+
modeButtons.forEach(btn => {
|
116
|
+
const btnMode = btn.getAttribute('data-hs-ba').replace('mode-', '');
|
117
|
+
if (btnMode === mode) {
|
118
|
+
btn.classList.remove('light');
|
119
|
+
} else {
|
120
|
+
btn.classList.add('light');
|
121
|
+
}
|
122
|
+
});
|
123
|
+
|
124
|
+
switch (mode) {
|
125
|
+
case 'before':
|
126
|
+
if (afterImage) afterImage.style.clipPath = 'polygon(100% 0%, 100% 0%, 100% 100%, 100% 100%)';
|
127
|
+
if (slider) slider.style.display = 'none';
|
128
|
+
break;
|
129
|
+
case 'after':
|
130
|
+
if (afterImage) afterImage.style.clipPath = 'polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)';
|
131
|
+
if (slider) slider.style.display = 'none';
|
132
|
+
break;
|
133
|
+
case 'split':
|
134
|
+
if (afterImage) afterImage.style.clipPath = `polygon(${instance.sliderPosition}% 0%, 100% 0%, 100% 100%, ${instance.sliderPosition}% 100%)`;
|
135
|
+
if (sliderHandle) sliderHandle.style.left = `${instance.sliderPosition}%`;
|
136
|
+
if (slider) slider.style.display = 'flex';
|
137
|
+
break;
|
138
|
+
}
|
139
|
+
|
140
|
+
instance.mode = mode;
|
141
|
+
}
|
142
|
+
|
143
|
+
function setupSliderDragging(instance, slider, itemIndex) {
|
144
|
+
const item = instance.items[itemIndex];
|
145
|
+
const imageWrap = item.querySelector('.ba-image_wrap');
|
146
|
+
|
147
|
+
if (!imageWrap) return;
|
148
|
+
|
149
|
+
function startDrag(e) {
|
150
|
+
isDragging = true;
|
151
|
+
dragInstance = { instance, itemIndex, imageWrap, slider };
|
152
|
+
document.body.style.userSelect = 'none';
|
153
|
+
document.body.style.cursor = 'grabbing';
|
154
|
+
slider.style.cursor = 'grabbing';
|
155
|
+
e.preventDefault();
|
156
|
+
}
|
157
|
+
|
158
|
+
function handleDrag(clientX) {
|
159
|
+
if (!isDragging || !dragInstance) return;
|
160
|
+
|
161
|
+
const rect = dragInstance.imageWrap.getBoundingClientRect();
|
162
|
+
const x = clientX - rect.left;
|
163
|
+
const percentage = Math.max(0, Math.min(100, (x / rect.width) * 100));
|
164
|
+
|
165
|
+
// Move the slider element directly
|
166
|
+
dragInstance.slider.style.left = `${percentage}%`;
|
167
|
+
|
168
|
+
// Update the clip path
|
169
|
+
updateSliderPosition(dragInstance.instance, dragInstance.itemIndex, percentage);
|
170
|
+
dragInstance.instance.sliderPosition = percentage;
|
171
|
+
}
|
172
|
+
|
173
|
+
function endDrag() {
|
174
|
+
isDragging = false;
|
175
|
+
if (dragInstance && dragInstance.slider) {
|
176
|
+
dragInstance.slider.style.cursor = 'grab';
|
177
|
+
}
|
178
|
+
dragInstance = null;
|
179
|
+
document.body.style.userSelect = '';
|
180
|
+
document.body.style.cursor = '';
|
181
|
+
}
|
182
|
+
|
183
|
+
slider.addEventListener('mousedown', startDrag);
|
184
|
+
slider.addEventListener('touchstart', startDrag, { passive: false });
|
185
|
+
|
186
|
+
document.addEventListener('mousemove', (e) => handleDrag(e.clientX));
|
187
|
+
document.addEventListener('touchmove', (e) => {
|
188
|
+
if (isDragging) {
|
189
|
+
e.preventDefault();
|
190
|
+
handleDrag(e.touches[0].clientX);
|
191
|
+
}
|
192
|
+
}, { passive: false });
|
193
|
+
|
194
|
+
document.addEventListener('mouseup', endDrag);
|
195
|
+
document.addEventListener('touchend', endDrag);
|
196
|
+
|
197
|
+
imageWrap.addEventListener('click', (e) => {
|
198
|
+
if (instance.mode !== 'split') return;
|
199
|
+
|
200
|
+
const rect = imageWrap.getBoundingClientRect();
|
201
|
+
const x = e.clientX - rect.left;
|
202
|
+
const percentage = (x / rect.width) * 100;
|
203
|
+
|
204
|
+
slider.style.left = `${percentage}%`;
|
205
|
+
updateSliderPosition(instance, itemIndex, percentage);
|
206
|
+
instance.sliderPosition = percentage;
|
207
|
+
});
|
208
|
+
}
|
209
|
+
|
210
|
+
function updateSliderPosition(instance, itemIndex, percentage) {
|
211
|
+
const item = instance.items[itemIndex];
|
212
|
+
const afterImage = item.querySelector('[data-hs-ba="image-after"]');
|
213
|
+
|
214
|
+
if (afterImage) {
|
215
|
+
afterImage.style.clipPath = `polygon(${percentage}% 0%, 100% 0%, 100% 100%, ${percentage}% 100%)`;
|
216
|
+
}
|
217
|
+
}
|
218
|
+
|
219
|
+
function navigateSlide(instance, direction) {
|
220
|
+
const newIndex = instance.currentIndex + direction;
|
221
|
+
const maxIndex = instance.items.length - 1;
|
222
|
+
|
223
|
+
let targetIndex;
|
224
|
+
if (newIndex > maxIndex) {
|
225
|
+
targetIndex = 0;
|
226
|
+
} else if (newIndex < 0) {
|
227
|
+
targetIndex = maxIndex;
|
228
|
+
} else {
|
229
|
+
targetIndex = newIndex;
|
230
|
+
}
|
231
|
+
|
232
|
+
showSlide(instance, targetIndex);
|
233
|
+
}
|
234
|
+
|
235
|
+
function showSlide(instance, index) {
|
236
|
+
if (index === instance.currentIndex) return;
|
237
|
+
|
238
|
+
// Reset split position to default when switching items
|
239
|
+
instance.sliderPosition = config.sliderPosition;
|
240
|
+
|
241
|
+
instance.items.forEach((item, i) => {
|
242
|
+
item.style.display = i === index ? 'block' : 'none';
|
243
|
+
|
244
|
+
// Update clip path for the new active item
|
245
|
+
if (i === index) {
|
246
|
+
const afterImage = item.querySelector('[data-hs-ba="image-after"]');
|
247
|
+
const sliderHandle = item.querySelector('[data-hs-ba="slider"]');
|
248
|
+
|
249
|
+
if (afterImage) {
|
250
|
+
// Apply default slider position to new item
|
251
|
+
if (instance.mode === 'split') {
|
252
|
+
afterImage.style.clipPath = `polygon(${instance.sliderPosition}% 0%, 100% 0%, 100% 100%, ${instance.sliderPosition}% 100%)`;
|
253
|
+
}
|
254
|
+
}
|
255
|
+
|
256
|
+
if (sliderHandle && instance.mode === 'split') {
|
257
|
+
// Position slider at default position
|
258
|
+
sliderHandle.style.left = `${instance.sliderPosition}%`;
|
259
|
+
}
|
260
|
+
}
|
261
|
+
});
|
262
|
+
|
263
|
+
instance.currentIndex = index;
|
264
|
+
currentSlideIndex[instance.id] = index;
|
265
|
+
|
266
|
+
updatePagination(instance);
|
267
|
+
setMode(instance, index, instance.mode);
|
268
|
+
}
|
269
|
+
|
270
|
+
function setupPagination(instance, pagination) {
|
271
|
+
const dots = Array.from(pagination.querySelectorAll('.ba-pag_dot'));
|
272
|
+
|
273
|
+
dots.forEach((dot, index) => {
|
274
|
+
dot.addEventListener('click', () => {
|
275
|
+
showSlide(instance, index);
|
276
|
+
});
|
277
|
+
|
278
|
+
dot.style.cursor = 'pointer';
|
279
|
+
dot.setAttribute('role', 'button');
|
280
|
+
dot.setAttribute('tabindex', '0');
|
281
|
+
dot.setAttribute('aria-label', `Go to slide ${index + 1}`);
|
282
|
+
|
283
|
+
dot.addEventListener('keydown', (e) => {
|
284
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
285
|
+
e.preventDefault();
|
286
|
+
showSlide(instance, index);
|
287
|
+
}
|
288
|
+
});
|
289
|
+
});
|
290
|
+
}
|
291
|
+
|
292
|
+
function updatePagination(instance) {
|
293
|
+
instance.items.forEach((item, itemIndex) => {
|
294
|
+
const pagination = item.querySelector('[data-hs-ba="pagination"]');
|
295
|
+
if (!pagination) return;
|
296
|
+
|
297
|
+
const dots = pagination.querySelectorAll('.ba-pag_dot');
|
298
|
+
dots.forEach((dot, dotIndex) => {
|
299
|
+
if (dotIndex === instance.currentIndex) {
|
300
|
+
dot.classList.add('active');
|
301
|
+
dot.setAttribute('aria-current', 'true');
|
302
|
+
} else {
|
303
|
+
dot.classList.remove('active');
|
304
|
+
dot.removeAttribute('aria-current');
|
305
|
+
}
|
306
|
+
});
|
307
|
+
});
|
308
|
+
}
|
309
|
+
|
310
|
+
function setupKeyboardNavigation(instance) {
|
311
|
+
instance.wrapper.addEventListener('keydown', (e) => {
|
312
|
+
switch (e.key) {
|
313
|
+
case 'ArrowLeft':
|
314
|
+
e.preventDefault();
|
315
|
+
if (instance.mode === 'split') {
|
316
|
+
const newPos = Math.max(0, instance.sliderPosition - config.keyboardStep);
|
317
|
+
updateSliderPosition(instance, instance.currentIndex, newPos);
|
318
|
+
instance.sliderPosition = newPos;
|
319
|
+
} else {
|
320
|
+
navigateSlide(instance, -1);
|
321
|
+
}
|
322
|
+
break;
|
323
|
+
case 'ArrowRight':
|
324
|
+
e.preventDefault();
|
325
|
+
if (instance.mode === 'split') {
|
326
|
+
const newPos = Math.min(100, instance.sliderPosition + config.keyboardStep);
|
327
|
+
updateSliderPosition(instance, instance.currentIndex, newPos);
|
328
|
+
instance.sliderPosition = newPos;
|
329
|
+
} else {
|
330
|
+
navigateSlide(instance, 1);
|
331
|
+
}
|
332
|
+
break;
|
333
|
+
case 'ArrowUp':
|
334
|
+
e.preventDefault();
|
335
|
+
navigateSlide(instance, -1);
|
336
|
+
break;
|
337
|
+
case 'ArrowDown':
|
338
|
+
e.preventDefault();
|
339
|
+
navigateSlide(instance, 1);
|
340
|
+
break;
|
341
|
+
case '1':
|
342
|
+
setMode(instance, instance.currentIndex, 'before');
|
343
|
+
break;
|
344
|
+
case '2':
|
345
|
+
setMode(instance, instance.currentIndex, 'split');
|
346
|
+
break;
|
347
|
+
case '3':
|
348
|
+
setMode(instance, instance.currentIndex, 'after');
|
349
|
+
break;
|
350
|
+
}
|
351
|
+
});
|
352
|
+
|
353
|
+
instance.wrapper.setAttribute('tabindex', '0');
|
354
|
+
instance.wrapper.setAttribute('role', 'application');
|
355
|
+
instance.wrapper.setAttribute('aria-label', 'Before and after image comparison');
|
356
|
+
}
|
357
|
+
|
358
|
+
function init() {
|
359
|
+
const wrappers = document.querySelectorAll('[data-hs-ba="wrapper"]');
|
360
|
+
|
361
|
+
wrappers.forEach(wrapper => {
|
362
|
+
initInstance(wrapper);
|
363
|
+
});
|
364
|
+
|
365
|
+
return { result: 'before-after initialized' };
|
366
|
+
}
|
367
|
+
|
368
|
+
if (typeof window !== 'undefined') {
|
369
|
+
window.hsmain = window.hsmain || {};
|
370
|
+
window.hsmain.utilBeforeAfter = {
|
371
|
+
init,
|
372
|
+
config,
|
373
|
+
updateConfig,
|
374
|
+
instances,
|
375
|
+
showSlide: (instanceId, index) => {
|
376
|
+
const instance = instances[instanceId];
|
377
|
+
if (instance) showSlide(instance, index);
|
378
|
+
},
|
379
|
+
setMode: (instanceId, mode) => {
|
380
|
+
const instance = instances[instanceId];
|
381
|
+
if (instance) setMode(instance, instance.currentIndex, mode);
|
382
|
+
}
|
383
|
+
};
|
384
|
+
}
|
385
|
+
|
386
|
+
return init();
|
387
|
+
}
|