@hortonstudio/main 1.9.9 → 1.9.11

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.
@@ -1,215 +1,256 @@
1
- // Infinite carousel slider
2
- const initInfiniteCarousel = (cleanup) => {
3
- const wrapper = document.querySelector('[data-hs-slider="wrapper"]');
4
- const nextBtn = document.querySelector('[data-hs-slider="next"]');
5
- const prevBtn = document.querySelector('[data-hs-slider="previous"]');
6
-
7
- if (!wrapper || !nextBtn || !prevBtn) return;
8
- const slides = wrapper.children;
9
- if (!slides?.length) return;
10
-
11
- const state = { currentIndex: 1, totalSlides: slides.length, isAnimating: false };
12
-
13
- wrapper.appendChild(slides[0].cloneNode(true));
14
- wrapper.insertBefore(slides[slides.length - 1].cloneNode(true), slides[0]);
15
-
16
- gsap.set(wrapper, { xPercent: -100 });
1
+ /**
2
+ * Pagination System
3
+ *
4
+ * Handles paginated lists with controls, counters, and dot navigation.
5
+ * Supports infinite looping, responsive layouts, and accessibility.
6
+ */
17
7
 
18
- const navigate = (direction) => {
19
- if (state.isAnimating) return;
20
- state.isAnimating = true;
21
- state.currentIndex += direction;
22
-
23
- const isLoop = (direction > 0 && state.currentIndex > state.totalSlides) ||
24
- (direction < 0 && state.currentIndex < 1);
25
-
26
- gsap.to(wrapper, {
27
- xPercent: -state.currentIndex * 100,
28
- duration: 0.5,
29
- ease: "power2.inOut",
30
- onComplete: () => {
31
- if (isLoop) {
32
- state.currentIndex = direction > 0 ? 1 : state.totalSlides;
33
- gsap.set(wrapper, { xPercent: -state.currentIndex * 100 });
8
+ export function init() {
9
+ const cleanup = {
10
+ observers: [],
11
+ handlers: []
12
+ };
13
+
14
+ // Initialize all pagination containers
15
+ try {
16
+ document.querySelectorAll('[data-hs-pagination="wrapper"]')
17
+ .forEach(container => {
18
+ try {
19
+ const instance = initPaginationInstance(container, cleanup);
20
+ if (!instance) {
21
+ console.warn('[hs-pagination] Failed to initialize container', container);
22
+ }
23
+ } catch (error) {
24
+ console.error('[hs-pagination] Error initializing container:', error, container);
34
25
  }
35
- state.isAnimating = false;
26
+ });
27
+ } catch (error) {
28
+ console.error('[hs-pagination] Critical error during initialization:', error);
29
+ }
30
+
31
+ return {
32
+ result: "pagination initialized",
33
+ destroy: () => {
34
+ try {
35
+ // Disconnect all observers
36
+ cleanup.observers.forEach(obs => {
37
+ try {
38
+ obs.disconnect();
39
+ } catch (error) {
40
+ console.error('[hs-pagination] Error disconnecting observer:', error);
41
+ }
42
+ });
43
+ cleanup.observers.length = 0;
44
+
45
+ // Remove all event listeners
46
+ cleanup.handlers.forEach(({ element, event, handler }) => {
47
+ try {
48
+ element.removeEventListener(event, handler);
49
+ } catch (error) {
50
+ console.error('[hs-pagination] Error removing event listener:', error);
51
+ }
52
+ });
53
+ cleanup.handlers.length = 0;
54
+ } catch (error) {
55
+ console.error('[hs-pagination] Critical error during cleanup:', error);
36
56
  }
37
- });
57
+ }
38
58
  };
59
+ }
39
60
 
40
- const nextHandler = () => navigate(1);
41
- const prevHandler = () => navigate(-1);
42
-
43
- nextBtn.addEventListener('click', nextHandler);
44
- prevBtn.addEventListener('click', prevHandler);
61
+ function initPaginationInstance(container, cleanup) {
62
+ const list = container.querySelector('[data-hs-pagination="list"]');
63
+ if (!list) {
64
+ console.warn('[hs-pagination] Missing required element: data-hs-pagination="list"');
65
+ return null;
66
+ }
45
67
 
46
- cleanup.handlers.push(
47
- { element: nextBtn, event: 'click', handler: nextHandler },
48
- { element: prevBtn, event: 'click', handler: prevHandler }
49
- );
50
- };
51
-
52
- // Infinite pagination slider
53
- const initInfinitePagination = (cleanup) => {
54
- document.querySelectorAll('[data-hs-slider="pagination"]')
55
- .forEach(container => {
56
- const list = container.querySelector('[data-hs-slider="pagination-list"]');
57
- if (list) initPaginationInstance(list, container, cleanup);
58
- });
59
- };
68
+ const wrapper = list.parentElement;
69
+ if (!wrapper) return null;
60
70
 
61
- const initPaginationInstance = (originalList, container, cleanup) => {
62
- const wrapper = originalList.parentElement;
63
- if (!wrapper || !container) return;
64
-
65
71
  const elements = {
66
- nextBtn: container.querySelector('[data-hs-slider="pagination-next"]'),
67
- prevBtn: container.querySelector('[data-hs-slider="pagination-previous"]'),
68
- counter: container.querySelector('[data-hs-slider="pagination-counter"]'),
69
- controls: container.querySelector('[data-hs-slider="pagination-controls"]'),
70
- dotsWrap: container.querySelector('[data-hs-slider="dots-wrap"]')
72
+ controls: container.querySelector('[data-hs-pagination="controls"]'),
73
+ counter: container.querySelector('[data-hs-pagination="counter"]'),
74
+ dotsWrap: container.querySelector('[data-hs-pagination="dots"]')
71
75
  };
72
-
73
- if (!elements.nextBtn || !elements.prevBtn) return;
74
-
76
+
77
+ // Early exit for infinite mode - no controls means no pagination
78
+ if (!elements.controls) {
79
+ return { initialized: false };
80
+ }
81
+
82
+ // Find next/previous buttons using data-site-clickable pattern
83
+ const nextClickable = container.querySelector('[data-hs-pagination="next"]');
84
+ const prevClickable = container.querySelector('[data-hs-pagination="previous"]');
85
+ const nextBtn = nextClickable?.children[0];
86
+ const prevBtn = prevClickable?.children[0];
87
+
88
+ if (!nextBtn || !prevBtn) {
89
+ console.warn('[hs-pagination] Missing required navigation buttons');
90
+ return null;
91
+ }
92
+
93
+ elements.nextBtn = nextBtn;
94
+ elements.prevBtn = prevBtn;
95
+
96
+ // Add ARIA attributes to buttons
75
97
  elements.nextBtn.setAttribute('aria-label', 'Go to next page');
76
98
  elements.prevBtn.setAttribute('aria-label', 'Go to previous page');
77
99
  if (elements.counter) {
78
100
  elements.counter.setAttribute('aria-live', 'polite');
79
101
  elements.counter.setAttribute('aria-label', 'Current page');
80
102
  }
81
-
82
- const config = elements.controls?.getAttribute('data-hs-config') || '';
83
-
84
- const configOptions = config.split(', ').map(opt => opt.trim());
85
- const isInfiniteMode = configOptions.includes('infinite');
86
-
87
- const desktopItems = !isInfiniteMode ? (parseInt(config.match(/show-(\d+)(?!-mobile)/)?.[1]) || 6) : 0;
88
- const mobileItems = !isInfiniteMode ? (parseInt(config.match(/show-(\d+)-mobile/)?.[1]) || desktopItems) : 0;
89
-
90
-
91
- // Early exit for infinite mode - disable pagination completely
92
- if (isInfiniteMode) {
93
- if (elements.controls) {
94
- elements.controls.style.display = 'none';
95
- elements.controls.setAttribute('aria-hidden', 'true');
96
- }
97
- if (elements.dotsWrap) {
98
- elements.dotsWrap.style.display = 'none';
99
- elements.dotsWrap.setAttribute('aria-hidden', 'true');
100
- }
101
- return;
102
- }
103
+
104
+ // Parse configuration from new attributes
105
+ const desktopItems = parseInt(elements.controls?.getAttribute('data-hs-pagination-show')) || 6;
106
+ const mobileItems = parseInt(elements.controls?.getAttribute('data-hs-pagination-show-mobile')) || desktopItems;
103
107
 
104
108
  const isMobileLayout = () => {
105
- const breakpoint = getComputedStyle(originalList).getPropertyValue('--data-hs-break').trim().replace(/"/g, '');
109
+ const breakpoint = getComputedStyle(list).getPropertyValue('--data-hs-break').trim().replace(/"/g, '');
106
110
  return breakpoint === 'mobile';
107
111
  };
108
- const allItems = Array.from(originalList.children);
112
+
113
+ const allItems = Array.from(list.children);
109
114
  const totalItems = allItems.length;
110
- if (!totalItems) return;
111
-
112
- const state = { totalPages: 1, currentIndex: 1, currentPage: 1, isAnimating: false, itemsPerPage: desktopItems };
115
+ if (!totalItems) return null;
116
+
117
+ const state = {
118
+ totalPages: 1,
119
+ currentIndex: 1,
120
+ currentPage: 1,
121
+ isAnimating: false,
122
+ itemsPerPage: desktopItems,
123
+ dotTemplates: { active: null, inactive: null }
124
+ };
113
125
  let wrapperChildren = [];
114
- let dotTemplates = { active: null, inactive: null };
115
126
 
116
- const initializeDots = () => {
117
- if (!elements.dotsWrap) {
118
- return;
127
+ // Create live region for announcements
128
+ const liveRegion = document.createElement('div');
129
+ liveRegion.className = 'sr-only';
130
+ liveRegion.setAttribute('aria-live', 'assertive');
131
+ liveRegion.setAttribute('aria-atomic', 'true');
132
+ liveRegion.style.cssText = 'position: absolute; left: -10000px; width: 1px; height: 1px; overflow: hidden;';
133
+ container.appendChild(liveRegion);
134
+
135
+ const updateCounter = () => {
136
+ if (elements.counter) {
137
+ elements.counter.textContent = `${state.currentPage} / ${state.totalPages}`;
138
+ }
139
+ };
140
+
141
+ const announcePageChange = () => {
142
+ liveRegion.textContent = `Page ${state.currentPage} of ${state.totalPages}`;
143
+ setTimeout(() => liveRegion.textContent = '', 1000);
144
+ };
145
+
146
+ const manageFocus = () => {
147
+ wrapperChildren.forEach((page, index) => {
148
+ page[index === state.currentIndex ? 'removeAttribute' : 'setAttribute']('inert', '');
149
+ });
150
+ };
151
+
152
+ const updateHeight = () => {
153
+ const targetPage = wrapperChildren[state.currentIndex];
154
+ if (targetPage && targetPage.offsetHeight !== undefined) {
155
+ wrapper.style.height = targetPage.offsetHeight + 'px';
119
156
  }
157
+ };
158
+
159
+ const initializeDots = () => {
160
+ if (!elements.dotsWrap) return;
120
161
 
121
- // Find template dots
162
+ // Find existing dots as templates
122
163
  const existingDots = Array.from(elements.dotsWrap.children);
164
+ if (!existingDots.length) return;
123
165
 
124
166
  // Identify active and inactive templates
125
- existingDots.forEach((dot) => {
126
- if (dot.classList.contains('is-active')) {
127
- dotTemplates.active = dot.cloneNode(true);
128
- } else {
129
- dotTemplates.inactive = dot.cloneNode(true);
130
- }
131
- });
167
+ const activeDot = existingDots.find(dot => dot.classList.contains('is-active'));
168
+ const inactiveDot = existingDots.find(dot => !dot.classList.contains('is-active'));
169
+
170
+ // Store templates (use same template for both if only one exists)
171
+ state.dotTemplates.active = activeDot ? activeDot.cloneNode(true) : existingDots[0].cloneNode(true);
172
+ state.dotTemplates.inactive = inactiveDot ? inactiveDot.cloneNode(true) : existingDots[0].cloneNode(true);
132
173
 
133
174
  // Clear existing dots
134
175
  elements.dotsWrap.innerHTML = '';
135
176
 
136
- // Handle single page or no pages case
137
- if (state.totalPages <= 1) {
138
- elements.dotsWrap.style.display = 'none';
139
- elements.dotsWrap.setAttribute('aria-hidden', 'true');
140
- return;
141
- }
142
-
143
- // Show dots container
144
- elements.dotsWrap.style.display = '';
145
- elements.dotsWrap.removeAttribute('aria-hidden');
177
+ // Add pagination accessibility attributes
178
+ elements.dotsWrap.setAttribute('role', 'group');
179
+ elements.dotsWrap.setAttribute('aria-label', 'Page navigation');
146
180
 
147
181
  // Create dots for each page
148
182
  for (let i = 1; i <= state.totalPages; i++) {
149
- const template = i === 1 ? dotTemplates.active : dotTemplates.inactive;
150
- if (template) {
151
- const dot = template.cloneNode(true);
152
- dot.setAttribute('data-page', i);
153
- dot.setAttribute('aria-label', `Go to page ${i}`);
154
- dot.style.cursor = 'pointer';
155
-
156
- // Add click handler for dot navigation
157
- const dotClickHandler = (e) => {
158
- e.preventDefault();
159
- const targetPage = parseInt(dot.getAttribute('data-page'));
160
- navigateToPage(targetPage);
161
- };
162
- dot.addEventListener('click', dotClickHandler);
163
- cleanup.handlers.push({ element: dot, event: 'click', handler: dotClickHandler });
164
-
165
- // Add keyboard support
166
- dot.setAttribute('tabindex', '0');
167
- dot.setAttribute('role', 'button');
168
- const dotKeydownHandler = (e) => {
169
- if (e.key === 'Enter' || e.key === ' ') {
170
- e.preventDefault();
171
- const targetPage = parseInt(dot.getAttribute('data-page'));
172
- navigateToPage(targetPage);
173
- }
174
- };
175
- dot.addEventListener('keydown', dotKeydownHandler);
176
- cleanup.handlers.push({ element: dot, event: 'keydown', handler: dotKeydownHandler });
183
+ const dot = (i === 1 ? state.dotTemplates.active : state.dotTemplates.inactive).cloneNode(true);
177
184
 
178
- elements.dotsWrap.appendChild(dot);
185
+ // Add accessibility attributes
186
+ dot.setAttribute('role', 'button');
187
+ dot.setAttribute('tabindex', '0');
188
+ dot.setAttribute('aria-label', `Go to page ${i}`);
189
+ dot.setAttribute('aria-current', i === 1 ? 'page' : 'false');
190
+ dot.setAttribute('data-page', i);
191
+
192
+ // Set initial active state
193
+ if (i === 1) {
194
+ dot.classList.add('is-active');
195
+ } else {
196
+ dot.classList.remove('is-active');
179
197
  }
198
+
199
+ // Click handler
200
+ const clickHandler = () => navigateToPage(i);
201
+ dot.addEventListener('click', clickHandler);
202
+ cleanup.handlers.push({ element: dot, event: 'click', handler: clickHandler });
203
+
204
+ // Keyboard handler
205
+ const keyHandler = (e) => {
206
+ if (e.key === 'Enter' || e.key === ' ') {
207
+ e.preventDefault();
208
+ navigateToPage(i);
209
+ }
210
+ };
211
+ dot.addEventListener('keydown', keyHandler);
212
+ cleanup.handlers.push({ element: dot, event: 'keydown', handler: keyHandler });
213
+
214
+ elements.dotsWrap.appendChild(dot);
180
215
  }
181
216
  };
182
217
 
183
218
  const updateActiveDot = () => {
184
- if (!elements.dotsWrap || state.totalPages <= 1) return;
219
+ if (!elements.dotsWrap) return;
185
220
 
186
221
  const dots = Array.from(elements.dotsWrap.children);
187
-
188
222
  dots.forEach((dot, index) => {
189
- const pageNumber = index + 1;
190
- const isActive = pageNumber === state.currentPage;
191
-
192
- if (isActive) {
223
+ const dotPage = index + 1;
224
+ if (dotPage === state.currentPage) {
193
225
  dot.classList.add('is-active');
194
226
  dot.setAttribute('aria-current', 'page');
195
227
  } else {
196
228
  dot.classList.remove('is-active');
197
- dot.removeAttribute('aria-current');
229
+ dot.setAttribute('aria-current', 'false');
198
230
  }
199
231
  });
200
232
  };
201
-
233
+
202
234
  const initializePagination = (forceItemsPerPage = null) => {
203
235
  const currentIsMobile = isMobileLayout();
204
236
  state.itemsPerPage = forceItemsPerPage || (currentIsMobile ? mobileItems : desktopItems);
205
237
  state.totalPages = Math.ceil(totalItems / state.itemsPerPage);
206
-
238
+
239
+ // Remove old dot event handlers to prevent memory leaks
240
+ if (elements.dotsWrap) {
241
+ cleanup.handlers = cleanup.handlers.filter(({ element }) => {
242
+ return !elements.dotsWrap.contains(element);
243
+ });
244
+ }
245
+
246
+ // Clean up previous page lists
207
247
  Array.from(wrapper.children).forEach(child => {
208
- if (child !== originalList) wrapper.removeChild(child);
248
+ if (child !== list) wrapper.removeChild(child);
209
249
  });
210
- originalList.innerHTML = '';
211
- allItems.forEach(item => originalList.appendChild(item));
212
-
250
+ list.innerHTML = '';
251
+ allItems.forEach(item => list.appendChild(item));
252
+
253
+ // Single page case
213
254
  if (state.totalPages <= 1) {
214
255
  if (elements.controls) {
215
256
  if (elements.controls.contains(document.activeElement)) document.activeElement.blur();
@@ -221,11 +262,12 @@ const initPaginationInstance = (originalList, container, cleanup) => {
221
262
  elements.dotsWrap.setAttribute('aria-hidden', 'true');
222
263
  }
223
264
  Object.assign(state, { totalPages: 1, currentIndex: 1, currentPage: 1, isAnimating: false });
224
- wrapper.style.cssText = `transform: translateX(0%); height: ${originalList.offsetHeight}px;`;
225
- wrapperChildren = [originalList];
265
+ wrapper.style.cssText = `transform: translateX(0%); height: ${list.offsetHeight}px;`;
266
+ wrapperChildren = [list];
226
267
  return 1;
227
268
  }
228
-
269
+
270
+ // Show controls and dots
229
271
  if (elements.controls) {
230
272
  elements.controls.style.display = '';
231
273
  elements.controls.removeAttribute('aria-hidden');
@@ -234,57 +276,42 @@ const initPaginationInstance = (originalList, container, cleanup) => {
234
276
  elements.dotsWrap.style.display = '';
235
277
  elements.dotsWrap.removeAttribute('aria-hidden');
236
278
  }
237
-
279
+
280
+ // Create page lists
238
281
  const pageLists = Array.from({ length: state.totalPages }, (_, page) => {
239
- const pageList = originalList.cloneNode(false);
282
+ const pageList = list.cloneNode(false);
240
283
  const startIndex = page * state.itemsPerPage;
241
284
  const endIndex = Math.min(startIndex + state.itemsPerPage, totalItems);
242
285
  allItems.slice(startIndex, endIndex).forEach(item => pageList.appendChild(item.cloneNode(true)));
243
286
  return pageList;
244
287
  });
245
-
246
- wrapper.insertBefore(pageLists[pageLists.length - 1].cloneNode(true), originalList);
288
+
289
+ // Insert cloned pages for infinite loop
290
+ wrapper.insertBefore(pageLists[pageLists.length - 1].cloneNode(true), list);
247
291
  pageLists.slice(1).forEach(page => wrapper.appendChild(page));
248
292
  wrapper.appendChild(pageLists[0].cloneNode(true));
249
-
250
- originalList.innerHTML = '';
251
- Array.from(pageLists[0].children).forEach(item => originalList.appendChild(item));
252
-
293
+
294
+ // Populate original list with first page
295
+ list.innerHTML = '';
296
+ Array.from(pageLists[0].children).forEach(item => list.appendChild(item));
297
+
253
298
  Object.assign(state, { currentIndex: 1, currentPage: 1, isAnimating: false });
254
299
  wrapperChildren = Array.from(wrapper.children);
255
300
  wrapper.style.transform = 'translateX(-100%)';
256
-
301
+
257
302
  updateCounter();
258
303
  updateHeight();
259
304
  manageFocus();
260
305
  initializeDots();
261
306
  return state.totalPages;
262
307
  };
263
- const liveRegion = document.createElement('div');
264
- liveRegion.className = 'sr-only';
265
- liveRegion.setAttribute('aria-live', 'assertive');
266
- liveRegion.setAttribute('aria-atomic', 'true');
267
- liveRegion.style.cssText = 'position: absolute; left: -10000px; width: 1px; height: 1px; overflow: hidden;';
268
- container.appendChild(liveRegion);
269
-
270
- const updateCounter = () => elements.counter && (elements.counter.textContent = `${state.currentPage} / ${state.totalPages}`);
271
-
272
- const announcePageChange = () => {
273
- liveRegion.textContent = `Page ${state.currentPage} of ${state.totalPages}`;
274
- setTimeout(() => liveRegion.textContent = '', 1000);
275
- };
276
-
277
- const manageFocus = () => wrapperChildren.forEach((page, index) => {
278
- page[index === state.currentIndex ? 'removeAttribute' : 'setAttribute']('inert', '');
279
- });
280
-
281
- const updateHeight = () => {
282
- const targetPage = wrapperChildren[state.currentIndex];
283
- if (targetPage) wrapper.style.height = targetPage.offsetHeight + 'px';
284
- };
308
+
285
309
  let currentLayoutIsMobile = isMobileLayout();
286
-
310
+
287
311
  const checkLayoutChange = () => {
312
+ // Prevent resize during animation
313
+ if (state.isAnimating) return;
314
+
288
315
  const newIsMobile = isMobileLayout();
289
316
  if (newIsMobile !== currentLayoutIsMobile) {
290
317
  currentLayoutIsMobile = newIsMobile;
@@ -293,25 +320,25 @@ const initPaginationInstance = (originalList, container, cleanup) => {
293
320
  updateHeight();
294
321
  }
295
322
  };
296
-
323
+
297
324
  const resizeObserver = new ResizeObserver(checkLayoutChange);
298
325
  resizeObserver.observe(wrapper);
299
326
  cleanup.observers.push(resizeObserver);
327
+
300
328
  initializePagination();
301
-
329
+
302
330
  const navigateToPage = (targetPage) => {
303
331
  if (state.isAnimating || state.totalPages <= 1 || targetPage === state.currentPage) return;
304
332
  state.isAnimating = true;
305
-
306
- // For multi-page jumps, we still use the same transition but update the index directly
333
+
307
334
  state.currentIndex = targetPage;
308
335
  state.currentPage = targetPage;
309
-
336
+
310
337
  updateCounter();
311
338
  announcePageChange();
312
339
  updateHeight();
313
340
  updateActiveDot();
314
-
341
+
315
342
  if (isMobileLayout() && elements.controls) {
316
343
  setTimeout(() => {
317
344
  const controlsBottom = elements.controls.getBoundingClientRect().bottom + window.pageYOffset;
@@ -320,14 +347,17 @@ const initPaginationInstance = (originalList, container, cleanup) => {
320
347
  window.scrollTo({ top: targetScrollPosition, behavior: 'smooth' });
321
348
  }, 50);
322
349
  }
323
-
350
+
324
351
  wrapper.style.transition = 'transform 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94)';
325
352
  wrapper.style.transform = `translateX(${-state.currentIndex * 100}%)`;
326
-
353
+
354
+ let transitionTimeout = null;
355
+
327
356
  const handleTransitionEnd = () => {
357
+ clearTimeout(transitionTimeout);
328
358
  wrapper.removeEventListener('transitionend', handleTransitionEnd);
329
359
  wrapper.style.transition = '';
330
-
360
+
331
361
  updateCounter();
332
362
  announcePageChange();
333
363
  updateHeight();
@@ -335,7 +365,10 @@ const initPaginationInstance = (originalList, container, cleanup) => {
335
365
  updateActiveDot();
336
366
  state.isAnimating = false;
337
367
  };
338
-
368
+
369
+ // Safety timeout in case transitionend never fires
370
+ transitionTimeout = setTimeout(handleTransitionEnd, 1000);
371
+
339
372
  wrapper.addEventListener('transitionend', handleTransitionEnd);
340
373
  };
341
374
 
@@ -343,15 +376,15 @@ const initPaginationInstance = (originalList, container, cleanup) => {
343
376
  if (state.isAnimating || state.totalPages <= 1) return;
344
377
  state.isAnimating = true;
345
378
  state.currentIndex += direction;
346
-
347
- state.currentPage = state.currentIndex > state.totalPages ? 1 :
348
- state.currentIndex < 1 ? state.totalPages : state.currentIndex;
349
-
379
+
380
+ state.currentPage = state.currentIndex > state.totalPages ? 1 :
381
+ state.currentIndex < 1 ? state.totalPages : state.currentIndex;
382
+
350
383
  updateCounter();
351
384
  announcePageChange();
352
385
  updateHeight();
353
386
  updateActiveDot();
354
-
387
+
355
388
  if (isMobileLayout() && elements.controls) {
356
389
  setTimeout(() => {
357
390
  const controlsBottom = elements.controls.getBoundingClientRect().bottom + window.pageYOffset;
@@ -360,14 +393,17 @@ const initPaginationInstance = (originalList, container, cleanup) => {
360
393
  window.scrollTo({ top: targetScrollPosition, behavior: 'smooth' });
361
394
  }, 50);
362
395
  }
363
-
396
+
364
397
  wrapper.style.transition = 'transform 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94)';
365
398
  wrapper.style.transform = `translateX(${-state.currentIndex * 100}%)`;
366
-
399
+
400
+ let transitionTimeout = null;
401
+
367
402
  const handleTransitionEnd = () => {
403
+ clearTimeout(transitionTimeout);
368
404
  wrapper.removeEventListener('transitionend', handleTransitionEnd);
369
405
  wrapper.style.transition = '';
370
-
406
+
371
407
  if (state.currentIndex > state.totalPages) {
372
408
  state.currentIndex = 1;
373
409
  state.currentPage = 1;
@@ -377,7 +413,7 @@ const initPaginationInstance = (originalList, container, cleanup) => {
377
413
  state.currentPage = state.totalPages;
378
414
  wrapper.style.transform = `translateX(${-state.totalPages * 100}%)`;
379
415
  }
380
-
416
+
381
417
  updateCounter();
382
418
  announcePageChange();
383
419
  updateHeight();
@@ -385,9 +421,13 @@ const initPaginationInstance = (originalList, container, cleanup) => {
385
421
  updateActiveDot();
386
422
  state.isAnimating = false;
387
423
  };
388
-
424
+
425
+ // Safety timeout in case transitionend never fires
426
+ transitionTimeout = setTimeout(handleTransitionEnd, 1000);
427
+
389
428
  wrapper.addEventListener('transitionend', handleTransitionEnd);
390
429
  };
430
+
391
431
  const nextHandler = () => navigate(1);
392
432
  const prevHandler = () => navigate(-1);
393
433
 
@@ -398,31 +438,8 @@ const initPaginationInstance = (originalList, container, cleanup) => {
398
438
  { element: elements.nextBtn, event: 'click', handler: nextHandler },
399
439
  { element: elements.prevBtn, event: 'click', handler: prevHandler }
400
440
  );
401
- };
402
-
403
- export const init = () => {
404
- const cleanup = {
405
- observers: [],
406
- handlers: []
407
- };
408
-
409
- initInfiniteCarousel(cleanup);
410
- initInfinitePagination(cleanup);
411
441
 
412
- return {
413
- result: "slider initialized",
414
- destroy: () => {
415
- // Disconnect all observers
416
- cleanup.observers.forEach(obs => obs.disconnect());
417
- cleanup.observers.length = 0;
418
-
419
- // Remove all event listeners
420
- cleanup.handlers.forEach(({ element, event, handler }) => {
421
- element.removeEventListener(event, handler);
422
- });
423
- cleanup.handlers.length = 0;
424
- }
425
- };
426
- };
442
+ return { initialized: true };
443
+ }
427
444
 
428
- export const version = "1.0.0";
445
+ export const version = "1.0.0";