PageSourceSearch

https://www.nykopingsguiden.se/js/components/siema.min.js

js nykopingsguiden.se collected 2026-10-02 08:04:48 UTC 23,534 bytes, 719 lines download raw bytes

1/**
2 * Hi :-) This is a class representing a Siema.
3 */
4console.log('roo 6');
5class Siema {
6    /**
7     * Create a Siema.
8     * @param {Object} options - Optional settings object.
9     */
10    constructor(options) {
11        // Merge defaults with user's settings
12        this.config = Siema.mergeSettings(options);
13
14        // Resolve selector's type
15        this.selector = typeof this.config.selector === 'string' ? document.querySelector(this.config.selector) : this.config.selector;
16
17        // Early throw if selector doesn't exists
18        if (this.selector === null) {
19            throw new Error('Something wrong with your selector 😭');
20        }
21
22        // update perPage number dependable of user value
23        this.resolveSlidesNumber();
24
25        // Create global references
26        this.selectorWidth = this.selector.offsetWidth;
27        this.innerElements = [].slice.call(this.selector.children);
28        this.currentSlide = this.config.loop ?
29            this.config.startIndex % this.innerElements.length :
30            Math.max(0, Math.min(this.config.startIndex, this.innerElements.length - this.perPage));
31        this.transformProperty = Siema.webkitOrNot();
32
33        // Bind all event handlers for referencability
34        ['resizeHandler', 'touchstartHandler', 'touchendHandler', 'touchmoveHandler', 'mousedownHandler', 'mouseupHandler', 'mouseleaveHandler', 'mousemoveHandler', 'clickHandler'].forEach(method => {
35            this[method] = this[method].bind(this);
36        });
37
38        // Build markup and apply required styling to elements
39        this.init();
40    }
41
42
43    /**
44     * Overrides default settings with custom ones.
45     * @param {Object} options - Optional settings object.
46     * @returns {Object} - Custom Siema settings.
47     */
48    static mergeSettings(options) {
49        const settings = {
50            selector: '.siema',
51            duration: 200,
52            easing: 'ease-out',
53            perPage: 1,
54            startIndex: 0,
55            draggable: true,
56            multipleDrag: true,
57            threshold: 20,
58            loop: false,
59            rtl: false,
60		    dynamicWidth: false,
61            onInit: () => { },
62            onChange: () => { },
63        };
64
65        const userSttings = options;
66        for (const attrname in userSttings) {
67            settings[attrname] = userSttings[attrname];
68        }
69
70        return settings;
71    }
72
73
74    /**
75     * Determine if browser supports unprefixed transform property.
76     * Google Chrome since version 26 supports prefix-less transform
77     * @returns {string} - Transform property supported by client.
78     */
79    static webkitOrNot() {
80        const style = document.documentElement.style;
81        if (typeof style.transform === 'string') {
82            return 'transform';
83        }
84        return 'WebkitTransform';
85    }
86
87    /**
88     * Attaches listeners to required events.
89     */
90    attachEvents() {
91        // Resize element on window resize
92        window.addEventListener('resize', this.resizeHandler);
93
94        // If element is draggable / swipable, add event handlers
95        if (this.config.draggable) {
96            // Keep track pointer hold and dragging distance
97            this.pointerDown = false;
98            this.drag = {
99                startX: 0,
100                endX: 0,
101                startY: 0,
102                letItGo: null,
103                preventClick: false,
104            };
105
106            // Touch events
107            this.selector.addEventListener('touchstart', this.touchstartHandler);
108            this.selector.addEventListener('touchend', this.touchendHandler);
109            this.selector.addEventListener('touchmove', this.touchmoveHandler);
110
111            // Mouse events
112            this.selector.addEventListener('mousedown', this.mousedownHandler);
113            this.selector.addEventListener('mouseup', this.mouseupHandler);
114            this.selector.addEventListener('mouseleave', this.mouseleaveHandler);
115            this.selector.addEventListener('mousemove', this.mousemoveHandler);
116
117            // Click
118            this.selector.addEventListener('click', this.clickHandler);
119        }
120    }
121
122
123    /**
124     * Detaches listeners from required events.
125     */
126    detachEvents() {
127        window.removeEventListener('resize', this.resizeHandler);
128        this.selector.removeEventListener('touchstart', this.touchstartHandler);
129        this.selector.removeEventListener('touchend', this.touchendHandler);
130        this.selector.removeEventListener('touchmove', this.touchmoveHandler);
131        this.selector.removeEventListener('mousedown', this.mousedownHandler);
132        this.selector.removeEventListener('mouseup', this.mouseupHandler);
133        this.selector.removeEventListener('mouseleave', this.mouseleaveHandler);
134        this.selector.removeEventListener('mousemove', this.mousemoveHandler);
135        this.selector.removeEventListener('click', this.clickHandler);
136    }
137
138
139    /**
140     * Builds the markup and attaches listeners to required events.
141     */
142    init() {
143        this.attachEvents();
144
145        // hide everything out of selector's boundaries
146        this.selector.style.overflow = 'hidden';
147
148        // rtl or ltr
149        this.selector.style.direction = this.config.rtl ? 'rtl' : 'ltr';
150
151        // build a frame and slide to a currentSlide
152        this.buildSliderFrame();
153
154        this.config.onInit.call(this);
155    }
156
157
158    /**
159     * Build a sliderFrame and slide to a current item.
160     */
161	buildSliderFrame() {
162		this.slideCount = this.config.loop
163			? this.innerElements.length + (2 * this.perPage)
164			: this.innerElements.length;
165
166		this.sliderFrame = document.createElement('div');
167		this.sliderFrame.style.display = 'flex';
168		this.sliderFrame.style.width = this.config.dynamicWidth
169			? `${100 * this.slideCount}%`
170			: `${(this.selectorWidth / this.perPage * this.slideCount) + 2}px`;
171
172		this.enableTransition();
173
174		if (this.config.draggable) {
175			this.selector.style.cursor = '-webkit-grab';
176		}
177
178		const docFragment = document.createDocumentFragment();
179
180		if (this.config.loop) {
181			for (let i = this.innerElements.length - this.perPage; i < this.innerElements.length; i++) {
182				const element = this.buildSliderFrameItem(this.innerElements[i].cloneNode(true));
183				docFragment.appendChild(element);
184			}
185		}
186
187		for (let i = 0; i < this.innerElements.length; i++) {
188			const element = this.buildSliderFrameItem(this.innerElements[i]);
189			docFragment.appendChild(element);
190		}
191
192		if (this.config.loop) {
193			for (let i = 0; i < this.perPage; i++) {
194				const element = this.buildSliderFrameItem(this.innerElements[i].cloneNode(true));
195				docFragment.appendChild(element);
196			}
197		}
198
199		this.sliderFrame.appendChild(docFragment);
200		this.selector.innerHTML = '';
201		this.selector.appendChild(this.sliderFrame);
202
203		this.slideToCurrent();
204	}
205
206
207
208	buildSliderFrameItem(elm) {
209		const elementContainer = document.createElement('div');
210
211		if (this.config.dynamicWidth) {
212			elementContainer.style.flexBasis = `${100 / this.slideCount}%`;
213			elementContainer.style.flexShrink = '0';
214			elementContainer.style.flexGrow = '0';
215		} else {
216			elementContainer.style.cssFloat = this.config.rtl ? 'right' : 'left';
217			elementContainer.style.float = this.config.rtl ? 'right' : 'left';
218			elementContainer.style.width = `${this.config.loop
219				? 100 / (this.innerElements.length + (this.perPage * 2))
220				: 100 / this.innerElements.length}%`;
221		}
222
223		elementContainer.appendChild(elm);
224		return elementContainer;
225	}
226
227
228
229
230    /**
231     * Determinates slides number accordingly to clients viewport.
232     */
233    resolveSlidesNumber() {
234        if (typeof this.config.perPage === 'number') {
235            this.perPage = this.config.perPage;
236        }
237        else if (typeof this.config.perPage === 'object') {
238            this.perPage = 1;
239            for (const viewport in this.config.perPage) {
240                if (window.innerWidth >= viewport) {
241                    this.perPage = this.config.perPage[viewport];
242                }
243            }
244        }
245    }
246
247
248    /**
249     * Go to previous slide.
250     * @param {number} [howManySlides=1] - How many items to slide backward.
251     * @param {function} callback - Optional callback function.
252     */
253    prev(howManySlides = 1, callback) {
254        // early return when there is nothing to slide
255        if (this.innerElements.length <= this.perPage) {
256            return;
257        }
258
259        const beforeChange = this.currentSlide;
260
261        if (this.config.loop) {
262            const isNewIndexClone = this.currentSlide - howManySlides < 0;
263            if (isNewIndexClone) {
264                this.disableTransition();
265
266                const mirrorSlideIndex = this.currentSlide + this.innerElements.length;
267                const mirrorSlideIndexOffset = this.perPage;
268                const moveTo = mirrorSlideIndex + mirrorSlideIndexOffset;
269                const offset = (this.config.rtl ? 1 : -1) * moveTo * (this.selectorWidth / this.perPage);
270                const dragDistance = this.config.draggable ? this.drag.endX - this.drag.startX : 0;
271
272                this.sliderFrame.style[this.transformProperty] = `translate3d(${offset + dragDistance}px, 0, 0)`;
273                this.currentSlide = mirrorSlideIndex - howManySlides;
274            }
275            else {
276                this.currentSlide = this.currentSlide - howManySlides;
277            }
278        }
279        else {
280            this.currentSlide = Math.max(this.currentSlide - howManySlides, 0);
281        }
282
283        if (beforeChange !== this.currentSlide) {
284            this.slideToCurrent(this.config.loop);
285            this.config.onChange.call(this);
286            if (callback) {
287                callback.call(this);
288            }
289        }
290    }
291
292
293    /**
294     * Go to next slide.
295     * @param {number} [howManySlides=1] - How many items to slide forward.
296     * @param {function} callback - Optional callback function.
297     */
298    next(howManySlides = 1, callback) {
299        // early return when there is nothing to slide
300        if (this.innerElements.length <= this.perPage) {
301            return;
302        }
303
304        const beforeChange = this.currentSlide;
305
306        if (this.config.loop) {
307            const isNewIndexClone = this.currentSlide + howManySlides > this.innerElements.length - this.perPage;
308            if (isNewIndexClone) {
309                this.disableTransition();
310
311                const mirrorSlideIndex = this.currentSlide - this.innerElements.length;
312                const mirrorSlideIndexOffset = this.perPage;
313                const moveTo = mirrorSlideIndex + mirrorSlideIndexOffset;
314                const offset = (this.config.rtl ? 1 : -1) * moveTo * (this.selectorWidth / this.perPage);
315                const dragDistance = this.config.draggable ? this.drag.endX - this.drag.startX : 0;
316
317                this.sliderFrame.style[this.transformProperty] = `translate3d(${offset + dragDistance}px, 0, 0)`;
318                this.currentSlide = mirrorSlideIndex + howManySlides;
319            }
320            else {
321                this.currentSlide = this.currentSlide + howManySlides;
322            }
323        }
324        else {
325            this.currentSlide = Math.min(this.currentSlide + howManySlides, this.innerElements.length - this.perPage);
326        }
327        if (beforeChange !== this.currentSlide) {
328            this.slideToCurrent(this.config.loop);
329            this.config.onChange.call(this);
330            if (callback) {
331                callback.call(this);
332            }
333        }
334    }
335
336
337    /**
338     * Disable transition on sliderFrame.
339     */
340    disableTransition() {
341        this.sliderFrame.style.webkitTransition = `all 0ms ${this.config.easing}`;
342        this.sliderFrame.style.transition = `all 0ms ${this.config.easing}`;
343    }
344
345
346    /**
347     * Enable transition on sliderFrame.
348     */
349    enableTransition() {
350        this.sliderFrame.style.webkitTransition = `all ${this.config.duration}ms ${this.config.easing}`;
351        this.sliderFrame.style.transition = `all ${this.config.duration}ms ${this.config.easing}`;
352    }
353
354
355    /**
356     * Go to slide with particular index
357     * @param {number} index - Item index to slide to.
358     * @param {function} callback - Optional callback function.
359     */
360    goTo(index, callback) {
361        if (this.innerElements.length <= this.perPage) {
362            return;
363        }
364        const beforeChange = this.currentSlide;
365        this.currentSlide = this.config.loop ?
366            index % this.innerElements.length :
367            Math.min(Math.max(index, 0), this.innerElements.length - this.perPage);
368        if (beforeChange !== this.currentSlide) {
369            this.slideToCurrent();
370            this.config.onChange.call(this);
371            if (callback) {
372                callback.call(this);
373            }
374        }
375    }
376
377
378    /**
379     * Moves sliders frame to position of currently active slide
380     */
381    slideToCurrent(enableTransition) {
382		const currentSlide = this.config.loop
383			? this.currentSlide + this.perPage
384			: this.currentSlide;
385
386		let offset;
387
388		if (this.config.dynamicWidth) {
389			offset = (this.config.rtl ? 1 : -1) * currentSlide * (100 / this.slideCount);
390			this.sliderFrame.style[this.transformProperty] = `translate3d(${offset}%, 0, 0)`;
391		} else {
392			offset = (this.config.rtl ? 1 : -1) * currentSlide * (this.selectorWidth / this.perPage);
393			this.sliderFrame.style[this.transformProperty] = `translate3d(${offset}px, 0, 0)`;
394		}
395
396		if (enableTransition) {
397			requestAnimationFrame(() => {
398				requestAnimationFrame(() => {
399					this.enableTransition();
400					this.sliderFrame.style[this.transformProperty] = this.config.dynamicWidth
401						? `translate3d(${offset}%, 0, 0)`
402						: `translate3d(${offset}px, 0, 0)`;
403				});
404			});
405		}
406	}
407
408
409    /**
410     * Recalculate drag /swipe event and reposition the frame of a slider
411     */
412    updateAfterDrag() {
413        const movement = (this.config.rtl ? -1 : 1) * (this.drag.endX - this.drag.startX);
414        const movementDistance = Math.abs(movement);
415        const howManySliderToSlide = this.config.multipleDrag ? Math.ceil(movementDistance / (this.selectorWidth / this.perPage)) : 1;
416
417        const slideToNegativeClone = movement > 0 && this.currentSlide - howManySliderToSlide < 0;
418        const slideToPositiveClone = movement < 0 && this.currentSlide + howManySliderToSlide > this.innerElements.length - this.perPage;
419
420        if (movement > 0 && movementDistance > this.config.threshold && this.innerElements.length > this.perPage) {
421            this.prev(howManySliderToSlide);
422        }
423        else if (movement < 0 && movementDistance > this.config.threshold && this.innerElements.length > this.perPage) {
424            this.next(howManySliderToSlide);
425        }
426        this.slideToCurrent(slideToNegativeClone || slideToPositiveClone);
427    }
428
429
430    /**
431     * When window resizes, resize slider components as well
432     */
433    resizeHandler() {
434        // update perPage number dependable of user value
435        this.resolveSlidesNumber();
436
437        // relcalculate currentSlide
438        // prevent hiding items when browser width increases
439        if (this.currentSlide + this.perPage > this.innerElements.length) {
440            this.currentSlide = this.innerElements.length <= this.perPage ? 0 : this.innerElements.length - this.perPage;
441        }
442
443        this.selectorWidth = this.selector.offsetWidth;
444
445        this.buildSliderFrame();
446    }
447
448
449    /**
450     * Clear drag after touchend and mouseup event
451     */
452    clearDrag() {
453        this.drag = {
454            startX: 0,
455            endX: 0,
456            startY: 0,
457            letItGo: null,
458            preventClick: this.drag.preventClick
459        };
460    }
461
462
463    /**
464     * touchstart event handler
465     */
466    touchstartHandler(e) {
467        // Prevent dragging / swiping on inputs, selects and textareas
468        const ignoreSiema = ['TEXTAREA', 'OPTION', 'INPUT', 'SELECT'].indexOf(e.target.nodeName) !== -1;
469        if (ignoreSiema) {
470            return;
471        }
472
473        e.stopPropagation();
474        this.pointerDown = true;
475        this.drag.startX = e.touches[0].pageX;
476        this.drag.startY = e.touches[0].pageY;
477    }
478
479
480    /**
481     * touchend event handler
482     */
483    touchendHandler(e) {
484        e.stopPropagation();
485        this.pointerDown = false;
486        this.enableTransition();
487        if (this.drag.endX) {
488            this.updateAfterDrag();
489        }
490        this.clearDrag();
491    }
492
493
494    /**
495     * touchmove event handler
496     */
497    touchmoveHandler(e) {
498        e.stopPropagation();
499
500        if (this.drag.letItGo === null) {
501            this.drag.letItGo = Math.abs(this.drag.startY - e.touches[0].pageY) < Math.abs(this.drag.startX - e.touches[0].pageX);
502        }
503
504        if (this.pointerDown && this.drag.letItGo) {
505            e.preventDefault();
506            this.drag.endX = e.touches[0].pageX;
507            this.sliderFrame.style.webkitTransition = `all 0ms ${this.config.easing}`;
508            this.sliderFrame.style.transition = `all 0ms ${this.config.easing}`;
509
510            const currentSlide = this.config.loop ? this.currentSlide + this.perPage : this.currentSlide;
511            const currentOffset = currentSlide * (this.selectorWidth / this.perPage);
512            const dragOffset = (this.drag.endX - this.drag.startX);
513            const offset = this.config.rtl ? currentOffset + dragOffset : currentOffset - dragOffset;
514            this.sliderFrame.style[this.transformProperty] = `translate3d(${(this.config.rtl ? 1 : -1) * offset}px, 0, 0)`;
515        }
516    }
517
518
519    /**
520     * mousedown event handler
521     */
522    mousedownHandler(e) {
523        // Prevent dragging / swiping on inputs, selects and textareas
524        const ignoreSiema = ['TEXTAREA', 'OPTION', 'INPUT', 'SELECT'].indexOf(e.target.nodeName) !== -1;
525        if (ignoreSiema) {
526            return;
527        }
528
529        e.preventDefault();
530        e.stopPropagation();
531        this.pointerDown = true;
532        this.drag.startX = e.pageX;
533    }
534
535
536    /**
537     * mouseup event handler
538     */
539    mouseupHandler(e) {
540        e.stopPropagation();
541        this.pointerDown = false;
542        this.selector.style.cursor = '-webkit-grab';
543        this.enableTransition();
544        if (this.drag.endX) {
545            this.updateAfterDrag();
546        }
547        this.clearDrag();
548    }
549
550
551    /**
552     * mousemove event handler
553     */
554    mousemoveHandler(e) {
555        e.preventDefault();
556        if (this.pointerDown) {
557            // if dragged element is a link
558            // mark preventClick prop as a true
559            // to detemine about browser redirection later on
560            if (e.target.nodeName === 'A') {
561                this.drag.preventClick = true;
562            }
563
564            this.drag.endX = e.pageX;
565            this.selector.style.cursor = '-webkit-grabbing';
566            this.sliderFrame.style.webkitTransition = `all 0ms ${this.config.easing}`;
567            this.sliderFrame.style.transition = `all 0ms ${this.config.easing}`;
568
569            const currentSlide = this.config.loop ? this.currentSlide + this.perPage : this.currentSlide;
570            const currentOffset = currentSlide * (this.selectorWidth / this.perPage);
571            const dragOffset = (this.drag.endX - this.drag.startX);
572            const offset = this.config.rtl ? currentOffset + dragOffset : currentOffset - dragOffset;
573            this.sliderFrame.style[this.transformProperty] = `translate3d(${(this.config.rtl ? 1 : -1) * offset}px, 0, 0)`;
574        }
575    }
576
577
578    /**
579     * mouseleave event handler
580     */
581    mouseleaveHandler(e) {
582        if (this.pointerDown) {
583            this.pointerDown = false;
584            this.selector.style.cursor = '-webkit-grab';
585            this.drag.endX = e.pageX;
586            this.drag.preventClick = false;
587            this.enableTransition();
588            this.updateAfterDrag();
589            this.clearDrag();
590        }
591    }
592
593
594    /**
595     * click event handler
596     */
597    clickHandler(e) {
598        // if the dragged element is a link
599        // prevent browsers from folowing the link
600        if (this.drag.preventClick) {
601            e.preventDefault();
602        }
603        this.drag.preventClick = false;
604    }
605
606
607    /**
608     * Remove item from carousel.
609     * @param {number} index - Item index to remove.
610     * @param {function} callback - Optional callback to call after remove.
611     */
612    remove(index, callback) {
613        if (index < 0 || index >= this.innerElements.length) {
614            throw new Error('Item to remove doesn\'t exist 😭');
615        }
616
617        // Shift sliderFrame back by one item when:
618        // 1. Item with lower index than currenSlide is removed.
619        // 2. Last item is removed.
620        const lowerIndex = index < this.currentSlide;
621        const lastItem = this.currentSlide + this.perPage - 1 === index;
622
623        if (lowerIndex || lastItem) {
624            this.currentSlide--;
625        }
626
627        this.innerElements.splice(index, 1);
628
629        // build a frame and slide to a currentSlide
630        this.buildSliderFrame();
631
632        if (callback) {
633            callback.call(this);
634        }
635    }
636
637
638    /**
639     * Insert item to carousel at particular index.
640     * @param {HTMLElement} item - Item to insert.
641     * @param {number} index - Index of new new item insertion.
642     * @param {function} callback - Optional callback to call after insert.
643     */
644    insert(item, index, callback) {
645        if (index < 0 || index > this.innerElements.length + 1) {
646            throw new Error('Unable to inset it at this index 😭');
647        }
648        if (this.innerElements.indexOf(item) !== -1) {
649            throw new Error('The same item in a carousel? Really? Nope 😭');
650        }
651
652        // Avoid shifting content
653        const shouldItShift = index <= this.currentSlide > 0 && this.innerElements.length;
654        this.currentSlide = shouldItShift ? this.currentSlide + 1 : this.currentSlide;
655
656        this.innerElements.splice(index, 0, item);
657
658        // build a frame and slide to a currentSlide
659        this.buildSliderFrame();
660
661        if (callback) {
662            callback.call(this);
663        }
664    }
665
666
667    /**
668     * Prepernd item to carousel.
669     * @param {HTMLElement}
669 item - Item to prepend.
670     * @param {function} callback - Optional callback to call after prepend.
671     */
672    prepend(item, callback) {
673        this.insert(item, 0);
674        if (callback) {
675            callback.call(this);
676        }
677    }
678
679
680    /**
681     * Append item to carousel.
682     * @param {HTMLElement} item - Item to append.
683     * @param {function} callback - Optional callback to call after append.
684     */
685    append(item, callback) {
686        this.insert(item, this.innerElements.length + 1);
687        if (callback) {
688            callback.call(this);
689        }
690    }
691
692
693    /**
694     * Removes listeners and optionally restores to initial markup
695     * @param {boolean} restoreMarkup - Determinants about restoring an initial markup.
696     * @param {function} callback - Optional callback function.
697     */
698    destroy(restoreMarkup = false, callback) {
699        this.detachEvents();
700
701        this.selector.style.cursor = 'auto';
702
703        if (restoreMarkup) {
704            const slides = document.createDocumentFragment();
705            for (let i = 0; i < this.innerElements.length; i++) {
706                slides.appendChild(this.innerElements[i]);
707            }
708            this.selector.innerHTML = '';
709            this.selector.appendChild(slides);
710            this.selector.removeAttribute('style');
711        }
712
713        if (callback) {
714            callback.call(this);
715        }
716    }
717}
718
719window.Siema = Siema;

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.