@hortonstudio/main 1.2.30 → 1.2.34
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/autoInit/modal.js +18 -8
- package/{utils → autoInit}/navbar.js +113 -3
- 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
|
+
}
|
package/autoInit/modal.js
CHANGED
@@ -3,9 +3,8 @@ function initModal() {
|
|
3
3
|
transitionDuration: 0.3,
|
4
4
|
blurOpacity: 0.5,
|
5
5
|
breakpoints: {
|
6
|
-
mobile:
|
7
|
-
tablet:
|
8
|
-
desktop: 991
|
6
|
+
mobile: 767,
|
7
|
+
tablet: 991
|
9
8
|
}
|
10
9
|
};
|
11
10
|
|
@@ -13,8 +12,7 @@ function initModal() {
|
|
13
12
|
const width = window.innerWidth;
|
14
13
|
if (width <= config.breakpoints.mobile) return 'mobile';
|
15
14
|
if (width <= config.breakpoints.tablet) return 'tablet';
|
16
|
-
|
17
|
-
return 'desktop-large';
|
15
|
+
return 'desktop';
|
18
16
|
}
|
19
17
|
|
20
18
|
function shouldPreventModal(element) {
|
@@ -55,11 +53,23 @@ function initModal() {
|
|
55
53
|
});
|
56
54
|
}
|
57
55
|
|
58
|
-
|
56
|
+
// Store modal states that were closed due to prevention
|
57
|
+
let preventedModalStates = new Map();
|
58
|
+
|
59
|
+
function handleBreakpointChange() {
|
59
60
|
document.querySelectorAll('[data-hs-modalprevent]').forEach(element => {
|
60
|
-
|
61
|
+
const elementKey = element.getAttribute('data-hs-modal') + '_' + (element.id || element.className);
|
62
|
+
const shouldPrevent = shouldPreventModal(element);
|
63
|
+
const wasStoredAsOpen = preventedModalStates.get(elementKey);
|
64
|
+
|
65
|
+
if (shouldPrevent && element.x) {
|
66
|
+
preventedModalStates.set(elementKey, true);
|
61
67
|
element.x = 0;
|
62
68
|
closeModal(element);
|
69
|
+
} else if (!shouldPrevent && wasStoredAsOpen) {
|
70
|
+
preventedModalStates.delete(elementKey);
|
71
|
+
element.x = 1;
|
72
|
+
openModal(element);
|
63
73
|
}
|
64
74
|
});
|
65
75
|
}
|
@@ -97,7 +107,7 @@ function initModal() {
|
|
97
107
|
|
98
108
|
// Handle window resize to check for prevented modals
|
99
109
|
window.addEventListener('resize', function() {
|
100
|
-
|
110
|
+
handleBreakpointChange();
|
101
111
|
});
|
102
112
|
|
103
113
|
return { result: 'modal initialized' };
|
@@ -2,6 +2,7 @@ export const init = () => {
|
|
2
2
|
setupDynamicDropdowns();
|
3
3
|
setupMobileMenuButton();
|
4
4
|
setupMobileMenuARIA();
|
5
|
+
setupMobileMenuBreakpointHandler();
|
5
6
|
return { result: 'navbar initialized' };
|
6
7
|
};
|
7
8
|
|
@@ -340,7 +341,7 @@ function addDesktopArrowNavigation() {
|
|
340
341
|
});
|
341
342
|
}
|
342
343
|
|
343
|
-
// Mobile menu button system
|
344
|
+
// Mobile menu button system with modal-like functionality
|
344
345
|
function setupMobileMenuButton() {
|
345
346
|
const menuButton = document.querySelector('[data-hs-nav="menubtn"]');
|
346
347
|
const mobileMenu = document.querySelector('[data-hs-nav="menu"]');
|
@@ -360,9 +361,28 @@ function setupMobileMenuButton() {
|
|
360
361
|
|
361
362
|
let isMenuOpen = false;
|
362
363
|
|
364
|
+
function shouldPreventMobileMenu() {
|
365
|
+
const menuSizeElement = document.querySelector('.menu-size.is-mobile');
|
366
|
+
if (!menuSizeElement) return false;
|
367
|
+
|
368
|
+
const computedStyle = window.getComputedStyle(menuSizeElement);
|
369
|
+
return computedStyle.display === 'none';
|
370
|
+
}
|
371
|
+
|
363
372
|
function openMenu() {
|
364
|
-
if (isMenuOpen) return;
|
373
|
+
if (isMenuOpen || shouldPreventMobileMenu()) return;
|
365
374
|
isMenuOpen = true;
|
375
|
+
|
376
|
+
// Add body overflow hidden class
|
377
|
+
document.body.classList.add('u-overflow-hidden');
|
378
|
+
|
379
|
+
// Add blur effect to modal blur elements
|
380
|
+
document.querySelectorAll('[data-hs-nav="modal-blur"]').forEach(element => {
|
381
|
+
element.style.display = 'block';
|
382
|
+
element.style.opacity = '0.5';
|
383
|
+
element.style.transition = 'opacity 0.3s ease';
|
384
|
+
});
|
385
|
+
|
366
386
|
menuButton.setAttribute('aria-expanded', 'true');
|
367
387
|
menuButton.setAttribute('aria-label', 'Close navigation menu');
|
368
388
|
mobileMenu.inert = false;
|
@@ -392,6 +412,19 @@ function setupMobileMenuButton() {
|
|
392
412
|
function closeMenu() {
|
393
413
|
if (!isMenuOpen) return;
|
394
414
|
isMenuOpen = false;
|
415
|
+
|
416
|
+
// Remove body overflow hidden class
|
417
|
+
document.body.classList.remove('u-overflow-hidden');
|
418
|
+
|
419
|
+
// Remove blur effect from modal blur elements
|
420
|
+
document.querySelectorAll('[data-hs-nav="modal-blur"]').forEach(element => {
|
421
|
+
element.style.opacity = '0';
|
422
|
+
element.style.transition = 'opacity 0.3s ease';
|
423
|
+
setTimeout(() => {
|
424
|
+
element.style.display = 'none';
|
425
|
+
}, 300);
|
426
|
+
});
|
427
|
+
|
395
428
|
if (mobileMenu.contains(document.activeElement)) {
|
396
429
|
menuButton.focus();
|
397
430
|
}
|
@@ -420,6 +453,8 @@ function setupMobileMenuButton() {
|
|
420
453
|
}
|
421
454
|
|
422
455
|
function toggleMenu() {
|
456
|
+
if (shouldPreventMobileMenu()) return;
|
457
|
+
|
423
458
|
if (isMenuOpen) {
|
424
459
|
closeMenu();
|
425
460
|
} else {
|
@@ -450,10 +485,38 @@ function setupMobileMenuButton() {
|
|
450
485
|
|
451
486
|
menuButton.addEventListener('click', function(e) {
|
452
487
|
if (!e.isTrusted) return;
|
488
|
+
|
489
|
+
if (shouldPreventMobileMenu()) return;
|
490
|
+
|
453
491
|
if (isMenuOpen && mobileMenu.contains(document.activeElement)) {
|
454
492
|
menuButton.focus();
|
455
493
|
}
|
456
|
-
|
494
|
+
|
495
|
+
const newMenuState = !isMenuOpen;
|
496
|
+
isMenuOpen = newMenuState;
|
497
|
+
|
498
|
+
// Handle body overflow class
|
499
|
+
if (isMenuOpen) {
|
500
|
+
document.body.classList.add('u-overflow-hidden');
|
501
|
+
} else {
|
502
|
+
document.body.classList.remove('u-overflow-hidden');
|
503
|
+
}
|
504
|
+
|
505
|
+
// Handle blur effect
|
506
|
+
document.querySelectorAll('[data-hs-nav="modal-blur"]').forEach(element => {
|
507
|
+
if (isMenuOpen) {
|
508
|
+
element.style.display = 'block';
|
509
|
+
element.style.opacity = '0.5';
|
510
|
+
element.style.transition = 'opacity 0.3s ease';
|
511
|
+
} else {
|
512
|
+
element.style.opacity = '0';
|
513
|
+
element.style.transition = 'opacity 0.3s ease';
|
514
|
+
setTimeout(() => {
|
515
|
+
element.style.display = 'none';
|
516
|
+
}, 300);
|
517
|
+
}
|
518
|
+
});
|
519
|
+
|
457
520
|
menuButton.setAttribute('aria-expanded', isMenuOpen);
|
458
521
|
menuButton.setAttribute('aria-label',
|
459
522
|
isMenuOpen ? 'Close navigation menu' : 'Open navigation menu'
|
@@ -487,6 +550,53 @@ function setupMobileMenuButton() {
|
|
487
550
|
});
|
488
551
|
}
|
489
552
|
});
|
553
|
+
|
554
|
+
// Store the menu state and functions for breakpoint handler
|
555
|
+
window.mobileMenuState = {
|
556
|
+
isMenuOpen: () => isMenuOpen,
|
557
|
+
closeMenu: closeMenu,
|
558
|
+
openMenu: openMenu
|
559
|
+
};
|
560
|
+
}
|
561
|
+
|
562
|
+
// Mobile menu breakpoint handler
|
563
|
+
function setupMobileMenuBreakpointHandler() {
|
564
|
+
let preventedMenuState = false;
|
565
|
+
|
566
|
+
function handleBreakpointChange() {
|
567
|
+
const menuSizeElement = document.querySelector('.menu-size.is-mobile');
|
568
|
+
if (!menuSizeElement) return;
|
569
|
+
|
570
|
+
const computedStyle = window.getComputedStyle(menuSizeElement);
|
571
|
+
const shouldPrevent = computedStyle.display === 'none';
|
572
|
+
|
573
|
+
if (!window.mobileMenuState) return;
|
574
|
+
|
575
|
+
if (shouldPrevent && window.mobileMenuState.isMenuOpen()) {
|
576
|
+
// Store that the menu was open before being prevented
|
577
|
+
preventedMenuState = true;
|
578
|
+
window.mobileMenuState.closeMenu();
|
579
|
+
} else if (!shouldPrevent && preventedMenuState) {
|
580
|
+
// Restore menu state if it was open before being prevented
|
581
|
+
preventedMenuState = false;
|
582
|
+
window.mobileMenuState.openMenu();
|
583
|
+
}
|
584
|
+
}
|
585
|
+
|
586
|
+
// Use ResizeObserver for more accurate detection
|
587
|
+
if (window.ResizeObserver) {
|
588
|
+
const resizeObserver = new ResizeObserver(handleBreakpointChange);
|
589
|
+
const menuSizeElement = document.querySelector('.menu-size.is-mobile');
|
590
|
+
if (menuSizeElement) {
|
591
|
+
resizeObserver.observe(menuSizeElement);
|
592
|
+
}
|
593
|
+
}
|
594
|
+
|
595
|
+
// Fallback to resize event
|
596
|
+
window.addEventListener('resize', handleBreakpointChange);
|
597
|
+
|
598
|
+
// Initial check
|
599
|
+
handleBreakpointChange();
|
490
600
|
}
|
491
601
|
|
492
602
|
function sanitizeForID(text) {
|
package/index.js
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
// Version:1.2.
|
1
|
+
// Version:1.2.32
|
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
|
+
}
|