@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.
- package/autoInit/accessibility/README.md +9 -1
- package/autoInit/accessibility/accessibility.js +2 -1
- package/autoInit/accessibility/functions/pagination/README.md +452 -0
- package/{utils/slider/slider.js → autoInit/accessibility/functions/pagination/pagination.js} +254 -237
- package/autoInit/site-settings/README.md +176 -29
- package/autoInit/site-settings/site-settings.js +29 -9
- package/index.js +0 -2
- package/package.json +1 -1
- package/utils/before-after/README.md +352 -75
- package/utils/slider/README.md +0 -299
package/{utils/slider/slider.js → autoInit/accessibility/functions/pagination/pagination.js}
RENAMED
|
@@ -1,215 +1,256 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
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
|
-
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
47
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
const
|
|
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(
|
|
109
|
+
const breakpoint = getComputedStyle(list).getPropertyValue('--data-hs-break').trim().replace(/"/g, '');
|
|
106
110
|
return breakpoint === 'mobile';
|
|
107
111
|
};
|
|
108
|
-
|
|
112
|
+
|
|
113
|
+
const allItems = Array.from(list.children);
|
|
109
114
|
const totalItems = allItems.length;
|
|
110
|
-
if (!totalItems) return;
|
|
111
|
-
|
|
112
|
-
const state = {
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
|
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.
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
//
|
|
137
|
-
|
|
138
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
190
|
-
|
|
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.
|
|
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 !==
|
|
248
|
+
if (child !== list) wrapper.removeChild(child);
|
|
209
249
|
});
|
|
210
|
-
|
|
211
|
-
allItems.forEach(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: ${
|
|
225
|
-
wrapperChildren = [
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
251
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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";
|