@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.
@@ -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
  }
@@ -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-heroconfig') && navElement.getAttribute('data-hs-heroconfig') === 'advanced';
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-heroconfig') || 'appear';
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-heroconfig') || 'line'; // default to line if not specified
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-heroconfig') || 'line';
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-heroconfig') || 'word';
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: 479,
7
- tablet: 767,
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
- if (width <= config.breakpoints.desktop) return 'desktop';
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
- function forceClosePreventedModals() {
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
- if (shouldPreventModal(element) && element.x) {
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
- forceClosePreventedModals();
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
- isMenuOpen = !isMenuOpen;
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.30
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-navbar': true
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-navbar': () => import('./utils/navbar.js'),
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hortonstudio/main",
3
- "version": "1.2.30",
3
+ "version": "1.2.34",
4
4
  "description": "Animation and utility library for client websites",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -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
+ }