PageSourceSearch

https://sol.no/view-resources/public/common/SwipeHelper.js?v=1790840215-L4

js sol.no collected 2026-10-01 11:35:33 UTC 16,125 bytes, 420 lines download raw bytes

1/**
2 * SwipeHelper
3 * Acts on a container displaying items overflowing horizontally or vertically.
4 * User can swipe / scroll normally or use click-handlers delivered by this class.
5 */
6
7if (!window.Dac) window.Dac = {};
8// eslint-disable-next-line prefer-const
9let { Dac } = window;
10Dac.SwipeHelper = class {
11
12    constructor(container, options) {
13        this.debug = window.location.toString().includes('debug=');
14        this.log('Creating instance ...');
15        this.settings = {
16            isHorizontal: options.isHorizontal,
17            css: {
18                hidden: 'nav-hidden'
19            },
20            navWidth: 0,
21            preloadNextImage: !!options.preloadNextImage,
22            intersectionObserver: {
23                threshold: typeof options.threshold === 'undefined' ? 0.5 : options.threshold
24            },
25            itemsSelector: options.itemsSelector
26        };
27        this.callbacks = {
28            navUpdate: []
29        };
30        const navItems = options.navItemsSelector ? container.querySelector(options.navItemsSelector) : null;
31        const previewItems = options.previewItemsSelector ? container.querySelector(options.previewItemsSelector) : null;
32        this.elements = {
33            nav: {
34                forward: container.querySelector(options.navItems.forwardSelector),
35                backward: container.querySelector(options.navItems.backwardSelector)
36            },
37            itemsContainer: this.settings.isHorizontal ? container.querySelector(options.itemsContainerSelector) : document.querySelector(options.itemsContainerSelector),
38            items: container.querySelectorAll(this.settings.itemsSelector),
39            navItems: navItems ? [...navItems.children] : null,
40            previewItems: previewItems ? [...previewItems.children] : null
41        };
42        this.noIndexList = [];
43        this.domIndex = null;
44
45        if (options.noIndexItemsSelector) {
46            const noIndexElements = container.querySelectorAll(options.noIndexItemsSelector);
47            if (noIndexElements && noIndexElements.length) {
48                this.log(`Will exclude ${ noIndexElements.length } elements from indexing ...`);
49                for (const el of noIndexElements) {
50                    const index = Array.from(this.elements.items).indexOf(el);
51                    if (index !== -1) {
52                        this.noIndexList.push(index);
53                        this.log(` - Excluding index ${ index }`);
54                    }
55                }
56                this.log(`Did exclude ${ this.noIndexList.length } elements from indexing ...`);
57            }
58        }
59
60        if (
61            !this.validateElement(this.elements.itemsContainer)
62            || !this.validateElement(this.elements.items)
63        ) {
64            this.log('Missing required dom-element(s).', true);
65            return;
66        }
67        if (this.elements.items.length < 2) {
68            this.log('To few elements. No need for the helper.', true);
69            return;
70        }
71        this.items = [...this.elements.items].map((el) => ({ visible: false, element: el }));
72        this.index = null;
73        this.observer = null;
74        this.observeElements(this.elements.itemsContainer, this.elements.items);
75        this.setupNavigation();
76        this.autoScrollInterval = null;
77        this.autoScroll = options.autoScroll;
78        this.elementWidth = this.items[0].element.scrollWidth;
79        this.currentLeft = 0;
80        this.isAutoscrolling = false;
81        this.preloadedIndexes = [];
82        this.registerAutoscroll();
83    }
84
85    addNavUpdate(callback) {
86        this.log('Adding navigation update callback ...');
87        this.callbacks.navUpdate.push(callback);
88    }
89
90    reflow() {
91        this.log(`Will reflow items`);
92        this.unobserve(this.elements.items);
93        this.elements.items = this.elements.itemsContainer.querySelectorAll(this.settings.itemsSelector);
94        this.items = [...this.elements.items].map((el) => ({ visible: false, element: el }));
95        this.observeElements(this.elements.itemsContainer, this.elements.items);
96    }
97
98    log(msg, isCritical = false) {
99        if (isCritical) {
100            console.warn('[SwipeHelper]', msg);
101        }
102        if (!this.debug) { return; }
103        console.log('[SwipeHelper]', msg);
104    }
105
106    // NodeList or element
107    validateElement(element) {
108        return element && (element.length || element.nodeType === 1);
109    }
110
111    // Only use autoscrolling when element is visible
112    registerAutoscroll() {
113        if (!this.autoScroll || !this.autoScroll.enabled) {
114            return;
115        }
116        if (!this.autoScroll.interval) {
117            this.autoScroll.interval = 4000;
118        }
119        const options = {
120            root: null,
121            rootMargin: '0px',
122            threshold: 0
123        };
124        const observer = new IntersectionObserver((entries, theObserver) => {
125            if (entries[0].isIntersecting) {
126                this.startAutoScroll();
127            } else {
128                this.stopAutoScroll();
129            }
130        }, options);
131        observer.observe(this.elements.itemsContainer);
132    }
133
134    startAutoScroll() {
135        if (this.isAutoscrolling || !this.settings.isHorizontal) {
136            return;
137        }
138        this.isAutoscrolling = true;
139        this.log('Starting autoscroll ...');
140
141        this.autoScrollInterval = setInterval(() => {
142            this.autoScroll.scrolling = true;
143
144            if (this.autoScroll.reset) {
145                this.elements.itemsContainer.scrollTo({
146                    top: 0,
147                    left: 0,
148                    behavior: 'smooth'
149                });
150                this.autoScroll.reset = false;
151            } else {
152                this.elements.itemsContainer.scrollTo({
153                    top: 0,
154                    left: this.elements.itemsContainer.scrollLeft + this.elementWidth,
155                    behavior: 'smooth'
156                });
157            }
158            setTimeout(() => {
159                this.autoScroll.scrolling = false;
160            }, 1000);
161        }, parseInt(this.autoScroll.interval, 10));
162
163        const scrolled = () => {
164            if (!this.autoScroll.scrolling) {
165                this.log('Will disable autoscroll');
166                this.stopAutoScroll();
167                this.elements.itemsContainer.removeEventListener('scroll', scrolled);
168            }
169        };
170
171        this.elements.itemsContainer.addEventListener('scroll', scrolled);
172    }
173
174    stopAutoScroll() {
175        if (!this.isAutoscrolling) {
176            return;
177        }
178        this.log('Stopping autoscroll ...');
179        clearInterval(this.autoScrollInterval);
180        this.autoScrollInterval = null;
181        this.isAutoscrolling = false;
182    }
183
184    setupNavigation() {
185        if (this.elements.nav.forward && this.elements.nav.backward) {
186            this.log(`Will register events for navigation.`);
187            this.settings.navWidth = this.elements.nav.backward.offsetWidth;
188            this.elements.nav.backward.addEventListener('click', (event) => {
189                event.preventDefault();
190                event.stopPropagation();
191                this.navigate(-1);
192            }, false);
193            this.elements.nav.forward.addEventListener('click', (event) => {
194                event.preventDefault();
195                event.stopPropagation();
196                this.navigate(1);
197            }, false);
198        }
199        if (this.elements.navItems) {
200            for (const item of this.elements.navItems) {
201                const index = this.elements.navItems.indexOf(item);
202                item.addEventListener('click', (event) => {
203                    this.navigateToIndex(index, 'auto');
204                }, false);
205            }
206        }
207        if (this.elements.previewItems) {
208            for (const item of this.elements.previewItems) {
209                const index = this.elements.previewItems.indexOf(item);
210                item.addEventListener('click', (event) => {
211                    this.navigateToIndex(index, 'auto');
212                }, false);
213            }
214        }
215    }
216
217    navigateToIndex(index, behavior) {
218        this.log(`Navigate to index: ${ index }`);
219        if (!this.items[index]) {
220            return;
221        }
222        this.navigateToItem(this.items[index], behavior);
223    }
224
225    navigate(steps) {
226        const visibleItems = this.items.filter((itm) => itm.visible);
227        if (!visibleItems.length) {
228            this.log('Cannot navigate. Did not find any visible items ...', true);
229            return;
230        }
231        // Calculate index based on the first visible item:
232        const index = this.items.indexOf(visibleItems.shift()) + steps;
233        if (index >= 0 && index < this.items.length) {
234            this.navigateToItem(this.items[index]);
235        }
236    }
237
238    navigateToItem(item, behavior = 'smooth') {
239        if (this.settings.isHorizontal) {
240            const leftPos = Math.max(0, item.element.offsetLeft - this.settings.navWidth);
241            this.elements.itemsContainer.scrollTo({
242                top: 0,
243                left: leftPos,
244                behavior
245            });
246        } else {
247            if (this.elements.itemsContainer === document.body) {
248                window.scrollTo({
249                    top: item.element.offsetTop,
250                    left: 0,
251                    behavior
252                });
253            } else {
254                this.elements.itemsContainer.scrollTo({
255                    top: item.element.offsetTop,
256                    left: 0,
257                    behavior
258                });
259            }
260        }
261    }
262
263    // this.noIndexList holds a list of items that should not be counted in the public index.
264    internalToPublicIndex(internalIndex, isForward = true) {
265        let result = internalIndex;
266        if (this.noIndexList.length) {
267            let offset = 0;
268            for (const noIndex of this.noIndexList) {
269                if ((!isForward && noIndex < internalIndex) || (isForward && noIndex <= internalIndex)) {
270                    offset++;
271                }
272            }
273            result -= offset;
274        }
275        return result;
276    }
277
278    publicToInternalIndex(publicIndex) {
279        let result = publicIndex;
280        if (this.noIndexList.length) {
281            let offset = 0;
282            for (const noIndex of this.noIndexList) {
283                if (noIndex <= result + offset) {
284                    offset++;
285                }
286            }
287            result += offset;
288        }
289        return result;
290    }
291
292    observeElements(root, items) {
293        this.log(`Will register IntersectionObserver for ${ items.length } items`);
294        const options = {
295            root: this.settings.isHorizontal ? root : null,
296            rootMargin: '0px',
297            threshold: this.settings.intersectionObserver.threshold
298        };
299        this.observer = new IntersectionObserver((entries, theObserver) => {
300            const touchedIndexes = [];
301            let visibleIndex = null;
302            for (const entry of entries) {
303                const index = Array.from(entry.target.parentNode.querySelectorAll(this.settings.itemsSelector)).indexOf(entry.target);
304                if (index === -1) {
305                    this.unobserve(items);
306                    return;
307                }
308                this.items[index].visible = entry.isIntersecting;
309                touchedIndexes.push(index);
310                if (this.items[index].visible) {
311                    visibleIndex = index;
312                }
313                if (this.settings.preloadNextImage && entry.isIntersecting && this.items[index + 1] && !this.preloadedIndexes.includes(index + 1)) {
314                    this.preloadAtIndex(index + 1);
315                }
316                this.log(`IntersectionObserver triggered at index ${ index }. Setting visibility to ${ this.items[index].visible }.`);
317            }
318            if (visibleIndex !== null) {
319                const direction = visibleIndex >= this.domIndex ? 'next' : 'prev';
320                let noIndexDistance = null;
321                // Find distance to next noIndex item. Use direction to decide if we should look forward or backward:
322                if (this.noIndexList.length) {
323                    noIndexDistance = this.noIndexList.map((noIndex) => noIndex - visibleIndex).filter((dist) => (direction === 'next' ? dist >= 0 : dist <= 0));
324                    if (noIndexDistance.length) {
325                        noIndexDistance = direction === 'next' ? Math.min(...noIndexDistance) : Math.max(...noIndexDistance);
326                    } else {
327                        noIndexDistance = null;
328                    }
329                }
330                // Set noIndexDistance to a positive int:
331                if (noIndexDistance !== null && noIndexDistance < 0) {
332                    noIndexDistance = Math.abs(noIndexDistance);
333                }
334
335                this.updateNavItems(this.internalToPublicIndex(visibleIndex, visibleIndex > this.index), {
336                    originalIndex: visibleIndex,
337                    direction,
338                    noIndexDistance
339                });
340            }
341            if (touchedIndexes.includes(0) || touchedIndexes.includes(this.items.length - 1)) {
342                this.updateGui();
343                if (this.autoScroll && this.autoScroll.enabled && this.items[this.items.length - 1].visible) {
344                    this.autoScroll.reset = true;
345                }
346            }
347            this.domIndex = visibleIndex;
348        }, options);
349        for (const item of items) {
350            this.observer.observe(item);
351        }
352    }
353
354    unobserve(items) {
355        this.log(`Will unobserve ${ items.length } items`);
356        for (const item of items) {
357            this.observer.unobserve(item);
358        }
359    }
360
361    // Remove "loading"-attribute to force browser to load image.
362    preloadAtIndex(index) {
363        this.preloadedIndexes.push(index);
364        if (!this.items[index] || !this.items[index].element) { return; }
365        const img = this.items[index].element.querySelector('img');
366        if (img) {
367            this.log(`Will preload image at index ${ index }`);
368            img.removeAttribute('loading');
369        }
370    }
371
372    // Update nav-elements based on visibility of first / last item
373    updateGui() {
374        if (this.elements.nav.forward && this.elements.nav.backward) {
375            this.log('Updating navigation-GUI ...');
376            if (this.items[0].visible) {
377                this.elements.nav.backward.classList.add(this.settings.css.hidden);
378            } else {
379                this.elements.nav.backward.classList.remove(this.settings.css.hidden);
380            }
381            if (this.items[this.items.length - 1].visible) {
382                this.elements.nav.forward.classList.add(this.settings.css.hidden);
383            } else {
384                this.elements.nav.forward.classList.remove(this.settings.css.hidden);
385            }
386        }
387    }
388
389    updateNavItems(index, { originalIndex, direction, noIndexDistance }) {
390        const selector = 'selected';
391        if (this.elements.navItems) {
392            if (this.elements.navItems[this.index]) {
393                this.elements.navItems[this.index].classList.remove(selector);
394            }
395            if (this.elements.navItems[index]) {
396                this.elements.navItems[index].classList.add(selector);
397            }
398        }
399        if (this.elements.previewItems) {
400            if (this.elements.previewItems[this.index]) {
401                this.elements.previewItems[this.index].classList.remove(selector);
402            }
403            if (this.elements.previewItems[index]) {
404                this.elements.previewItems[index].classList.add(selector);
405            }
406        }
407        this.index = index;
408
409        for (const fn of this.callbacks.navUpdate) {
410            fn(this.index, {
411                isNoIndex: this.noIndexList.includes(originalIndex),
412                direction,
413                domIndex: originalIndex,
414                noIndexDistance
415            });
416        }
417
418    }
419
420};

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.