PageSourceSearch

https://www.elle.cz/js/modules/infiniteCarousel.js

js elle.cz collected 2026-10-02 07:55:48 UTC 10,255 bytes, 337 lines download raw bytes

1import {getHorizontalItemStep} from '../core/measure.js';
2
3const getCarouselStep = (track, itemSelector) => {
4  return getHorizontalItemStep(track, itemSelector);
5};
6
7const getOriginalItems = (track, itemSelector, cloneAttr) =>
8  Array.from(track.querySelectorAll(itemSelector)).filter(
9    (item) => !item.hasAttribute(cloneAttr)
10  );
11
12const removeClones = (track, cloneAttr) => {
13  track.querySelectorAll(`[${cloneAttr}]`).forEach((clone) => clone.remove());
14};
15
16const syncLoopScrollBounds = (scrollContainer, state) => {
17  if (!state.isEnabled || state.originalSetWidth <= 0) {
18    return;
19  }
20
21  const scrollLeft = scrollContainer.scrollLeft;
22  if (scrollLeft < state.originalSetWidth * 0.5) {
23    state.isSyncing = true;
24    scrollContainer.scrollLeft = scrollLeft + state.originalSetWidth;
25    state.isSyncing = false;
26    return;
27  }
28
29  if (scrollLeft > state.originalSetWidth * 1.5) {
30    state.isSyncing = true;
31    scrollContainer.scrollLeft = scrollLeft - state.originalSetWidth;
32    state.isSyncing = false;
33  }
34};
35
36const createCarouselHandlers = (scrollContainer, track, state) => {
37  const handlePointerDown = (event) => {
38    if (!state.isEnabled || event.button > 0) {
39      return;
40    }
41
42    // Keep touch scrolling native for smooth momentum on mobile.
43    if (event.pointerType === 'touch' || event.pointerType === 'pen') {
44      return;
45    }
46
47    state.activePointerId = event.pointerId;
48    state.pointerStartX = event.clientX;
49    state.pointerStartY = event.clientY;
50    state.scrollStartX = scrollContainer.scrollLeft;
51    state.isPointerDragging = false;
52    state.suppressNextClick = false;
53    scrollContainer.setPointerCapture(event.pointerId);
54  };
55
56  const handlePointerMove = (event) => {
57    if (!state.isEnabled || state.activePointerId !== event.pointerId) {
58      return;
59    }
60
61    const deltaX = event.clientX - state.pointerStartX;
62    const deltaY = event.clientY - state.pointerStartY;
63    if (!state.isPointerDragging) {
64      if (Math.abs(deltaX) < 5 || Math.abs(deltaX) <= Math.abs(deltaY)) {
65        return;
66      }
67      state.isPointerDragging = true;
68    }
69
70    event.preventDefault();
71    state.suppressNextClick = true;
72    scrollContainer.scrollLeft = state.scrollStartX - deltaX;
73    syncLoopScrollBounds(scrollContainer, state);
74  };
75
76  const handlePointerEnd = (event) => {
77    if (state.activePointerId !== event.pointerId) {
78      return;
79    }
80
81    if (scrollContainer.hasPointerCapture(event.pointerId)) {
82      scrollContainer.releasePointerCapture(event.pointerId);
83    }
84
85    state.activePointerId = null;
86    state.isPointerDragging = false;
87  };
88
89  const handleTrackClickCapture = (event) => {
90    if (!state.suppressNextClick) {
91      return;
92    }
93
94    event.preventDefault();
95    event.stopPropagation();
96    state.suppressNextClick = false;
97  };
98
99  const handleScroll = () => {
100    if (state.isSyncing) {
101      return;
102    }
103    syncLoopScrollBounds(scrollContainer, state);
104  };
105
106  return {
107    handlePointerDown,
108    handlePointerMove,
109    handlePointerEnd,
110    handleTrackClickCapture,
111    handleScroll
112  };
113};
114
115const attachHandlers = (scrollContainer, track, state, enablePointerDrag) => {
116  state.handlers = createCarouselHandlers(scrollContainer, track, state);
117
118  const {
119    handlePointerDown,
120    handlePointerMove,
121    handlePointerEnd,
122    handleTrackClickCapture,
123    handleScroll
124  } = state.handlers;
125
126  scrollContainer.addEventListener('scroll', handleScroll, {passive: true});
127
128  if (enablePointerDrag) {
129    scrollContainer.addEventListener('pointerdown', handlePointerDown);
130    scrollContainer.addEventListener('pointermove', handlePointerMove, {passive: false});
131    scrollContainer.addEventListener('pointerup', handlePointerEnd);
132    scrollContainer.addEventListener('pointercancel', handlePointerEnd);
133    track.addEventListener('click', handleTrackClickCapture, true);
134  }
135};
136
137const detachHandlers = (scrollContainer, track, state, enablePointerDrag) => {
138  const {
139    handlePointerDown,
140    handlePointerMove,
141    handlePointerEnd,
142    handleTrackClickCapture,
143    handleScroll
144  } = state.handlers;
145
146  if (!handleScroll) {
147    return;
148  }
149
150  scrollContainer.removeEventListener('scroll', handleScroll);
151
152  if (enablePointerDrag) {
153    scrollContainer.removeEventListener('pointerdown', handlePointerDown);
154    scrollContainer.removeEventListener('pointermove', handlePointerMove);
155    scrollContainer.removeEventListener('pointerup', handlePointerEnd);
156    scrollContainer.removeEventListener('pointercancel', handlePointerEnd);
157    track.removeEventListener('click', handleTrackClickCapture, true);
158  }
159
160  state.handlers = {};
161};
162
163/**
164 * Creates an infinite-loop carousel with clone-based seamless scrolling.
165 *
166 * @param {object} config
167 * @param {HTMLElement} config.scrollContainer - Element with overflow (the one that scrolls)
168 * @param {HTMLElement} config.track - Element containing items (can be same as scrollContainer)
169 * @param {string} config.itemSelector - CSS selector for original items
170 * @param {string} [config.cloneAttr='data-carousel-clone'] - Data attribute to mark clones
171 * @param {HTMLElement|null} [config.prevButton]
172 * @param {HTMLElement|null} [config.nextButton]
173 * @param {() => boolean} [config.shouldEnable] - When to enable infinite mode
174 * @param {(container: HTMLElement) => number} [config.getScrollAmount] - Scroll distance for chevron clicks
175 * @param {boolean} [config.enablePointerDrag=true] - Enable mouse/stylus drag scrolling
176 */
177export function createInfiniteCarousel({
178                                         scrollContainer,
179                                         track,
180                                         itemSelector,
181                                         cloneAttr = 'data-carousel-clone',
182                                         prevButton = null,
183                                         nextButton = null,
184                                         shouldEnable = () => true,
185                                         getScrollAmount = (container) => Math.max(120, Math.round(container.clientWidth * 0.55)),
186                                         enablePointerDrag = true
187                                       }) {
188  if (!scrollContainer || !track) {
189    return {
190      syncToViewport() {
191      }
192    };
193  }
194
195  const focusableDescendantSelector = [
196    'a[href]',
197    'area[href]',
198    'button:not([disabled])',
199    'input:not([disabled])',
200    'select:not([disabled])',
201    'textarea:not([disabled])',
202    'iframe',
203    'audio[controls]',
204    'video[controls]',
205    '[contenteditable]:not([contenteditable="false"])',
206    '[tabindex]:not([tabindex="-1"])'
207  ].join(',');
208
209  const hideCarouselClone = (clone) => {
210    clone.setAttribute('aria-hidden', 'true');
211    clone.setAttribute('inert', '');
212    clone.tabIndex = -1;
213
214    clone.querySelectorAll('h1, h2, h3, h4, h5, h6').forEach((heading) => {
215      const neutralHeading = document.createElement('div');
216      Array.from(heading.attributes).forEach((attribute) => {
217        neutralHeading.setAttribute(attribute.name, attribute.value);
218      });
219      neutralHeading.replaceChildren(...heading.childNodes);
220      heading.replaceWith(neutralHeading);
221    });
222
223    clone.querySelectorAll(focusableDescendantSelector).forEach((element) => {
224      element.setAttribute('tabindex', '-1');
225    });
226  };
227
228  const state = {
229    isEnabled: false,
230    originalSetWidth: 0,
231    isSyncing: false,
232    activePointerId: null,
233    pointerStartX: 0,
234    pointerStartY: 0,
235    scrollStartX: 0,
236    isPointerDragging: false,
237    suppressNextClick: false,
238    handlers: {}
239  };
240
241  const moveByChevron = (direction) => {
242    if (!state.isEnabled) {
243      return;
244    }
245
246    const amount = getScrollAmount(scrollContainer);
247    syncLoopScrollBounds(scrollContainer, state);
248    scrollContainer.scrollTo({
249      left: scrollContainer.scrollLeft + amount * direction,
250      behavior: 'smooth'
251    });
252  };
253
254  if (prevButton) {
255    prevButton.addEventListener('click', () => moveByChevron(-1));
256  }
257
258  if (nextButton) {
259    nextButton.addEventListener('click', () => moveByChevron(1));
260  }
261
262  const enable = () => {
263    if (state.isEnabled) {
264      return;
265    }
266
267    const originals = getOriginalItems(track, itemSelector, cloneAttr);
268    if (originals.length <= 1) {
269      return;
270    }
271
272    const prependFragment = document.createDocumentFragment();
273    const appendFragment = document.createDocumentFragment();
274
275    originals.forEach((item) => {
276      const prependClone = item.cloneNode(true);
277      prependClone.setAttribute(cloneAttr, 'true');
278      hideCarouselClone(prependClone);
279      prependFragment.appendChild(prependClone);
280
281      const appendClone = item.cloneNode(true);
282      appendClone.setAttribute(cloneAttr, 'true');
283      hideCarouselClone(appendClone);
284      appendFragment.appendChild(appendClone);
285    });
286
287    track.insertBefore(prependFragment, track.firstChild);
288    track.appendChild(appendFragment);
289
290    const step = getCarouselStep(track, itemSelector);
291    state.originalSetWidth = step * originals.length;
292    if (state.originalSetWidth <= 0) {
293      removeClones(track, cloneAttr);
294      return;
295    }
296
297    state.isEnabled = true;
298    attachHandlers(scrollContainer, track, state, enablePointerDrag);
299    scrollContainer.scrollLeft = state.originalSetWidth;
300  };
301
302  const disable = () => {
303    if (!state.isEnabled) {
304      return;
305    }
306
307    state.isEnabled = false;
308    detachHandlers(scrollContainer, track, state, enablePointerDrag);
309    removeClones(track, cloneAttr);
310    scrollContainer.scrollLeft = 0;
311    state.activePointerId = null;
312    state.isPointerDragging = false;
313    state.suppressNextClick = false;
314  };
315
316  const syncToViewport = () => {
317    if (!shouldEnable()) {
318      disable();
319      return;
320    }
321
322    if (!state.isEnabled) {
323      enable();
324      return;
325    }
326
327    const originals = getOriginalItems(track, itemSelector, cloneAttr);
328    const step = getCarouselStep(track, itemSelector);
329    state.originalSetWidth = step * originals.length;
330    if (state.originalSetWidth > 0) {
331      const normalizedOffset = scrollContainer.scrollLeft % state.originalSetWidth;
332      scrollContainer.scrollLeft = state.originalSetWidth + normalizedOffset;
333    }
334  };
335
336  return {syncToViewport, enable, disable};
337}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.