UNPKG

26.2 kBJavaScriptView Raw
1import { Base } from "../shared/Base/Base.js";
2import { Panzoom } from "../Panzoom/Panzoom.js";
3
4import { extend } from "../shared/utils/extend.js";
5import { round } from "../shared/utils/round.js";
6import { throttle } from "../shared/utils/throttle.js";
7
8import { Plugins } from "./plugins/index.js";
9
10// Default language
11import en from "./l10n/en.js";
12
13const defaults = {
14 // Virtual slides. Each object should have at least `html` property that will be used to set content,
15 // example: `slides: [{html: 'First slide'}, {html: 'Second slide'}]`
16 slides: [],
17
18 // Number of slides to preload before/after visible slides
19 preload: 0,
20
21 // Number of slides to group into the page,
22 // if `auto` - group all slides that fit into the viewport
23 slidesPerPage: "auto",
24
25 // Index of initial page
26 initialPage: null,
27
28 // Index of initial slide
29 initialSlide: null,
30
31 // Panzoom friction while changing page
32 friction: 0.92,
33
34 // Should center active page
35 center: true,
36
37 // Should carousel scroll infinitely
38 infinite: true,
39
40 // Should the gap be filled before first and after last slide if `infinite: false`
41 fill: true,
42
43 // Should Carousel settle at any position after a swipe.
44 dragFree: false,
45
46 // Prefix for CSS classes, must be the same as the SCSS `$carousel-prefix` variable
47 prefix: "",
48
49 // Class names for DOM elements (without prefix)
50 classNames: {
51 viewport: "carousel__viewport",
52 track: "carousel__track",
53 slide: "carousel__slide",
54
55 // Classname toggled for slides inside current page
56 slideSelected: "is-selected",
57 },
58
59 // Localization of strings
60 l10n: en,
61};
62
63export class Carousel extends Base {
64 /**
65 * Carousel constructor
66 * @constructs Carousel
67 * @param {HTMLElement} $container - Carousel container
68 * @param {Object} [options] - Options for Carousel
69 */
70 constructor($container, options = {}) {
71 options = extend(true, {}, defaults, options);
72
73 super(options);
74
75 this.state = "init";
76
77 this.$container = $container;
78
79 if (!(this.$container instanceof HTMLElement)) {
80 throw new Error("No root element provided");
81 }
82
83 this.slideNext = throttle(this.slideNext.bind(this), 250, true);
84 this.slidePrev = throttle(this.slidePrev.bind(this), 250, true);
85
86 this.init();
87
88 $container.__Carousel = this;
89 }
90
91 /**
92 * Perform initialization
93 */
94 init() {
95 this.pages = [];
96 this.page = this.pageIndex = null;
97 this.prevPage = this.prevPageIndex = null;
98
99 this.attachPlugins(Carousel.Plugins);
100
101 this.trigger("init");
102
103 this.initLayout();
104
105 this.initSlides();
106
107 this.updateMetrics();
108
109 if (this.$track && this.pages.length) {
110 this.$track.style.transform = `translate3d(${this.pages[this.page].left * -1}px, 0px, 0) scale(1)`;
111 }
112
113 this.manageSlideVisiblity();
114
115 this.initPanzoom();
116
117 this.state = "ready";
118
119 this.trigger("ready");
120 }
121
122 /**
123 * Initialize layout; create necessary elements
124 */
125 initLayout() {
126 const prefix = this.option("prefix");
127 const classNames = this.option("classNames");
128
129 this.$viewport = this.option("viewport") || this.$container.querySelector(`.${prefix}${classNames.viewport}`);
130
131 if (!this.$viewport) {
132 this.$viewport = document.createElement("div");
133 this.$viewport.classList.add(...(prefix + classNames.viewport).split(" "));
134
135 this.$viewport.append(...this.$container.childNodes);
136
137 this.$container.appendChild(this.$viewport);
138 }
139
140 this.$track = this.option("track") || this.$container.querySelector(`.${prefix}${classNames.track}`);
141
142 if (!this.$track) {
143 this.$track = document.createElement("div");
144 this.$track.classList.add(...(prefix + classNames.track).split(" "));
145
146 this.$track.append(...this.$viewport.childNodes);
147
148 this.$viewport.appendChild(this.$track);
149 }
150 }
151
152 /**
153 * Fill `slides` array with objects from existing nodes and/or `slides` option
154 */
155 initSlides() {
156 this.slides = [];
157
158 // Get existing slides from the DOM
159 const elems = this.$viewport.querySelectorAll(`.${this.option("prefix")}${this.option("classNames.slide")}`);
160
161 elems.forEach((el) => {
162 const slide = {
163 $el: el,
164 isDom: true,
165 };
166
167 this.slides.push(slide);
168
169 this.trigger("createSlide", slide, this.slides.length);
170 });
171
172 // Add virtual slides, but do not create DOM elements yet,
173 // because they will be created dynamically based on current carousel position
174 if (Array.isArray(this.options.slides)) {
175 this.slides = extend(true, [...this.slides], this.options.slides);
176 }
177 }
178
179 /**
180 * Do all calculations related to slide size and paging
181 */
182 updateMetrics() {
183 // Calculate content width, viewport width
184 // ===
185 let contentWidth = 0;
186 let indexes = [];
187 let lastSlideWidth;
188
189 this.slides.forEach((slide, index) => {
190 const $el = slide.$el;
191 const slideWidth = slide.isDom || !lastSlideWidth ? this.getSlideMetrics($el) : lastSlideWidth;
192
193 slide.index = index;
194 slide.width = slideWidth;
195 slide.left = contentWidth;
196
197 lastSlideWidth = slideWidth;
198 contentWidth += slideWidth;
199
200 indexes.push(index);
201 });
202
203 let viewportWidth = Math.max(this.$track.offsetWidth, round(this.$track.getBoundingClientRect().width));
204
205 let viewportStyles = getComputedStyle(this.$track);
206 viewportWidth = viewportWidth - (parseFloat(viewportStyles.paddingLeft) + parseFloat(viewportStyles.paddingRight));
207
208 this.contentWidth = contentWidth;
209 this.viewportWidth = viewportWidth;
210
211 // Split slides into pages
212 // ===
213 const pages = [];
214 const slidesPerPage = this.option("slidesPerPage");
215
216 if (Number.isInteger(slidesPerPage) && contentWidth > viewportWidth) {
217 // Fixed number of slides in the page
218 for (let i = 0; i < this.slides.length; i += slidesPerPage) {
219 pages.push({
220 indexes: indexes.slice(i, i + slidesPerPage),
221 slides: this.slides.slice(i, i + slidesPerPage),
222 });
223 }
224 } else {
225 // Slides that fit inside viewport
226 let currentPage = 0;
227 let currentWidth = 0;
228
229 for (let i = 0; i < this.slides.length; i += 1) {
230 let slide = this.slides[i];
231
232 // Add next page
233 if (!pages.length || currentWidth + slide.width > viewportWidth) {
234 pages.push({
235 indexes: [],
236 slides: [],
237 });
238
239 currentPage = pages.length - 1;
240 currentWidth = 0;
241 }
242
243 currentWidth += slide.width;
244
245 pages[currentPage].indexes.push(i);
246 pages[currentPage].slides.push(slide);
247 }
248 }
249
250 const shouldCenter = this.option("center");
251 const shouldFill = this.option("fill");
252
253 // Calculate width and start position for each page
254 // ===
255 pages.forEach((page, index) => {
256 page.index = index;
257 page.width = page.slides.reduce((sum, slide) => sum + slide.width, 0);
258
259 page.left = page.slides[0].left;
260
261 if (shouldCenter) {
262 page.left += (viewportWidth - page.width) * 0.5 * -1;
263 }
264
265 if (shouldFill && !this.option("infiniteX", this.option("infinite")) && contentWidth > viewportWidth) {
266 page.left = Math.max(page.left, 0);
267 page.left = Math.min(page.left, contentWidth - viewportWidth);
268 }
269 });
270
271 // Merge pages
272 // ===
273 const rez = [];
274 let prevPage;
275
276 pages.forEach((page2) => {
277 const page = { ...page2 };
278
279 if (prevPage && page.left === prevPage.left) {
280 prevPage.width += page.width;
281
282 prevPage.slides = [...prevPage.slides, ...page.slides];
283 prevPage.indexes = [...prevPage.indexes, ...page.indexes];
284 } else {
285 page.index = rez.length;
286
287 prevPage = page;
288
289 rez.push(page);
290 }
291 });
292
293 this.pages = rez;
294
295 let page = this.page;
296
297 if (page === null) {
298 const initialSlide = this.option("initialSlide");
299
300 if (initialSlide !== null) {
301 page = this.findPageForSlide(initialSlide);
302 } else {
303 page = parseInt(this.option("initialPage", 0), 10) || 0;
304 }
305
306 if (!rez[page]) {
307 page = rez.length && page > rez.length ? rez[rez.length - 1].index : 0;
308 }
309
310 this.page = page;
311 this.pageIndex = page;
312 }
313
314 this.updatePanzoom();
315
316 this.trigger("refresh");
317 }
318
319 /**
320 * Calculate slide element width (including left, right margins)
321 * @param {Object} node
322 * @returns {Number} Width in px
323 */
324 getSlideMetrics(node) {
325 if (!node) {
326 const firstSlide = this.slides[0];
327
328 node = document.createElement("div");
329
330 node.dataset.isTestEl = 1;
331 node.style.visibility = "hidden";
332 node.classList.add(...(this.option("prefix") + this.option("classNames.slide")).split(" "));
333
334 // Assume all slides have the same custom class, if any
335 if (firstSlide.customClass) {
336 node.classList.add(...firstSlide.customClass.split(" "));
337 }
338
339 this.$track.prepend(node);
340 }
341
342 let width = Math.max(node.offsetWidth, round(node.getBoundingClientRect().width));
343
344 // Add left/right margin
345 const style = node.currentStyle || window.getComputedStyle(node);
346 width = width + (parseFloat(style.marginLeft) || 0) + (parseFloat(style.marginRight) || 0);
347
348 if (node.dataset.isTestEl) {
349 node.remove();
350 }
351
352 return width;
353 }
354
355 /**
356 *
357 * @param {Integer} index Index of the slide
358 * @returns {Integer|null} Index of the page if found, or null
359 */
360 findPageForSlide(index) {
361 index = parseInt(index, 10) || 0;
362
363 const page = this.pages.find((page) => {
364 return page.indexes.indexOf(index) > -1;
365 });
366
367 return page ? page.index : null;
368 }
369
370 /**
371 * Slide to next page, if possible
372 */
373 slideNext() {
374 this.slideTo(this.pageIndex + 1);
375 }
376
377 /**
378 * Slide to previous page, if possible
379 */
380 slidePrev() {
381 this.slideTo(this.pageIndex - 1);
382 }
383
384 /**
385 * Slides carousel to given page
386 * @param {Number} page - New index of active page
387 * @param {Object} [params] - Additional options
388 */
389 slideTo(page, params = {}) {
390 const { x = this.setPage(page, true) * -1, y = 0, friction = this.option("friction") } = params;
391
392 if (this.Panzoom.content.x === x && !this.Panzoom.velocity.x && friction) {
393 return;
394 }
395
396 this.Panzoom.panTo({
397 x,
398 y,
399 friction,
400 ignoreBounds: true,
401 });
402
403 if (this.state === "ready" && this.Panzoom.state === "ready") {
404 this.trigger("settle");
405 }
406 }
407
408 /**
409 * Initialise main Panzoom instance
410 */
411 initPanzoom() {
412 if (this.Panzoom) {
413 this.Panzoom.destroy();
414 }
415
416 // Create fresh object containing options for Pazoom instance
417 const options = extend(
418 true,
419 {},
420 {
421 // Track element will be set as Panzoom $content
422 content: this.$track,
423 wrapInner: false,
424 resizeParent: false,
425
426 // Disable any user interaction
427 zoom: false,
428 click: false,
429
430 // Right now, only horizontal navigation is supported
431 lockAxis: "x",
432
433 x: this.pages.length ? this.pages[this.page].left * -1 : 0,
434 centerOnStart: false,
435
436 // Make `textSelection` option more easy to customize
437 textSelection: () => this.option("textSelection", false),
438
439 // Disable dragging if content (e.g. all slides) fits inside viewport
440 panOnlyZoomed: function () {
441 return this.content.width <= this.viewport.width;
442 },
443 },
444 this.option("Panzoom")
445 );
446
447 // Create new Panzoom instance
448 this.Panzoom = new Panzoom(this.$container, options);
449
450 this.Panzoom.on({
451 // Bubble events
452 "*": (name, ...details) => this.trigger(`Panzoom.${name}`, ...details),
453 // The rest of events to be processed
454 afterUpdate: () => {
455 this.updatePage();
456 },
457 beforeTransform: this.onBeforeTransform.bind(this),
458 touchEnd: this.onTouchEnd.bind(this),
459 endAnimation: () => {
460 this.trigger("settle");
461 },
462 });
463
464 // The contents of the slides may cause the page scroll bar to appear, so the carousel width may change
465 // and slides have to be repositioned
466 this.updateMetrics();
467 this.manageSlideVisiblity();
468 }
469
470 updatePanzoom() {
471 if (!this.Panzoom) {
472 return;
473 }
474
475 this.Panzoom.content = {
476 ...this.Panzoom.content,
477 fitWidth: this.contentWidth,
478 origWidth: this.contentWidth,
479 width: this.contentWidth,
480 };
481
482 if (this.pages.length > 1 && this.option("infiniteX", this.option("infinite"))) {
483 this.Panzoom.boundX = null;
484 } else if (this.pages.length) {
485 this.Panzoom.boundX = {
486 from: this.pages[this.pages.length - 1].left * -1,
487 to: this.pages[0].left * -1,
488 };
489 }
490
491 if (this.option("infiniteY", this.option("infinite"))) {
492 this.Panzoom.boundY = null;
493 } else {
494 this.Panzoom.boundY = {
495 from: 0,
496 to: 0,
497 };
498 }
499
500 this.Panzoom.handleCursor();
501 }
502
503 manageSlideVisiblity() {
504 const contentWidth = this.contentWidth;
505 const viewportWidth = this.viewportWidth;
506
507 let currentX = this.Panzoom ? this.Panzoom.content.x * -1 : this.pages.length ? this.pages[this.page].left : 0;
508
509 const preload = this.option("preload");
510 const infinite = this.option("infiniteX", this.option("infinite"));
511
512 const paddingLeft = parseFloat(getComputedStyle(this.$viewport, null).getPropertyValue("padding-left"));
513 const paddingRight = parseFloat(getComputedStyle(this.$viewport, null).getPropertyValue("padding-right"));
514
515 // Check visibility of each slide
516 this.slides.forEach((slide) => {
517 let leftBoundary, rightBoundary;
518
519 let hasDiff = 0;
520
521 // #1 - slides in current viewport; this does not include infinite items
522 leftBoundary = currentX - paddingLeft;
523 rightBoundary = currentX + viewportWidth + paddingRight;
524
525 leftBoundary -= preload * (viewportWidth + paddingLeft + paddingRight);
526 rightBoundary += preload * (viewportWidth + paddingLeft + paddingRight);
527
528 const insideCurrentInterval = slide.left + slide.width > leftBoundary && slide.left < rightBoundary;
529
530 // #2 - infinite items inside current viewport; from previous interval
531 leftBoundary = currentX + contentWidth - paddingLeft;
532 rightBoundary = currentX + contentWidth + viewportWidth + paddingRight;
533
534 // Include slides that have to be preloaded
535 leftBoundary -= preload * (viewportWidth + paddingLeft + paddingRight);
536
537 const insidePrevInterval = infinite && slide.left + slide.width > leftBoundary && slide.left < rightBoundary;
538
539 // #2 - infinite items inside current viewport; from next interval
540 leftBoundary = currentX - contentWidth - paddingLeft;
541 rightBoundary = currentX - contentWidth + viewportWidth + paddingRight;
542
543 // Include slides that have to be preloaded
544 leftBoundary -= preload * (viewportWidth + paddingLeft + paddingRight);
545
546 const insideNextInterval = infinite && slide.left + slide.width > leftBoundary && slide.left < rightBoundary;
547
548 // Create virtual slides that should be visible or preloaded, remove others
549 if (insidePrevInterval || insideCurrentInterval || insideNextInterval) {
550 this.createSlideEl(slide);
551
552 if (insideCurrentInterval) {
553 hasDiff = 0;
554 }
555
556 if (insidePrevInterval) {
557 hasDiff = -1;
558 }
559
560 if (insideNextInterval) {
561 hasDiff = 1;
562 }
563
564 // Bring preloaded slides back to viewport, if needed
565 if (slide.left + slide.width > currentX && slide.left <= currentX + viewportWidth + paddingRight) {
566 hasDiff = 0;
567 }
568 } else {
569 this.removeSlideEl(slide);
570 }
571
572 slide.hasDiff = hasDiff;
573 });
574
575 // Reposition slides for continuity
576 let nextIndex = 0;
577 let nextPos = 0;
578
579 this.slides.forEach((slide, index) => {
580 let updatedX = 0;
581
582 if (slide.$el) {
583 if (index !== nextIndex || slide.hasDiff) {
584 updatedX = nextPos + slide.hasDiff * contentWidth;
585 } else {
586 nextPos = 0;
587 }
588
589 slide.$el.style.left = Math.abs(updatedX) > 0.1 ? `${nextPos + slide.hasDiff * contentWidth}px` : "";
590
591 nextIndex++;
592 } else {
593 nextPos += slide.width;
594 }
595 });
596
597 this.markSelectedSlides();
598 }
599
600 /**
601 * Creates main DOM element for virtual slides,
602 * lazy loads images inside regular slides
603 * @param {Object} slide
604 */
605 createSlideEl(slide) {
606 if (!slide) {
607 return;
608 }
609
610 if (slide.$el) {
611 let curentIndex = slide.$el.dataset.index;
612
613 if (!curentIndex || parseInt(curentIndex, 10) !== slide.index) {
614 slide.$el.dataset.index = slide.index;
615
616 // Lazy load images
617 // ===
618 slide.$el.querySelectorAll("[data-lazy-srcset]").forEach((node) => {
619 node.srcset = node.dataset.lazySrcset;
620 });
621
622 slide.$el.querySelectorAll("[data-lazy-src]").forEach((node) => {
623 let lazySrc = node.dataset.lazySrc;
624
625 if (node instanceof HTMLImageElement) {
626 node.src = lazySrc;
627 } else {
628 node.style.backgroundImage = `url('${lazySrc}')`;
629 }
630 });
631
632 // Lazy load slide background image
633 // ===
634 let lazySrc;
635
636 if ((lazySrc = slide.$el.dataset.lazySrc)) {
637 slide.$el.style.backgroundImage = `url('${lazySrc}')`;
638 }
639
640 slide.state = "ready";
641 }
642
643 return;
644 }
645
646 const div = document.createElement("div");
647
648 div.dataset.index = slide.index;
649 div.classList.add(...(this.option("prefix") + this.option("classNames.slide")).split(" "));
650
651 if (slide.customClass) {
652 div.classList.add(...slide.customClass.split(" "));
653 }
654
655 if (slide.html) {
656 div.innerHTML = slide.html;
657 }
658
659 const allElelements = [];
660
661 this.slides.forEach((slide, index) => {
662 if (slide.$el) {
663 allElelements.push(index);
664 }
665 });
666
667 // Find a place in DOM to insert an element
668 const goal = slide.index;
669 let refSlide = null;
670
671 if (allElelements.length) {
672 let refIndex = allElelements.reduce((prev, curr) =>
673 Math.abs(curr - goal) < Math.abs(prev - goal) ? curr : prev
674 );
675 refSlide = this.slides[refIndex];
676 }
677
678 this.$track.insertBefore(
679 div,
680 refSlide && refSlide.$el ? (refSlide.index < slide.index ? refSlide.$el.nextSibling : refSlide.$el) : null
681 );
682
683 slide.$el = div;
684
685 this.trigger("createSlide", slide, goal);
686
687 return slide;
688 }
689
690 /**
691 * Removes main DOM element of given slide
692 * @param {Object} slide
693 */
694 removeSlideEl(slide) {
695 if (slide.$el && !slide.isDom) {
696 this.trigger("removeSlide", slide);
697
698 slide.$el.remove();
699 slide.$el = null;
700 }
701 }
702
703 /**
704 * Toggles selected class name and aria-hidden attribute for slides based on visibility
705 */
706 markSelectedSlides() {
707 const selectedClass = this.option("classNames.slideSelected");
708 const attr = "aria-hidden";
709
710 this.slides.forEach((slide, index) => {
711 const $el = slide.$el;
712
713 if (!$el) {
714 return;
715 }
716
717 const page = this.pages[this.page];
718
719 if (page && page.indexes && page.indexes.indexOf(index) > -1) {
720 if (selectedClass && !$el.classList.contains(selectedClass)) {
721 $el.classList.add(selectedClass);
722 this.trigger("selectSlide", slide);
723 }
724
725 $el.removeAttribute(attr);
726 } else {
727 if (selectedClass && $el.classList.contains(selectedClass)) {
728 $el.classList.remove(selectedClass);
729 this.trigger("unselectSlide", slide);
730 }
731
732 $el.setAttribute(attr, true);
733 }
734 });
735 }
736
737 /**
738 * Perform all calculations and center current page
739 */
740 updatePage() {
741 this.updateMetrics();
742
743 this.slideTo(this.page, { friction: 0 });
744 }
745
746 /**
747 * Process `Panzoom.beforeTransform` event to remove slides moved out of viewport and
748 * to create necessary ones
749 */
750 onBeforeTransform() {
751 if (this.option("infiniteX", this.option("infinite"))) {
752 this.manageInfiniteTrack();
753 }
754
755 this.manageSlideVisiblity();
756 }
757
758 /**
759 * Seamlessly flip position of infinite carousel, if needed; this way x position stays low
760 */
761 manageInfiniteTrack() {
762 const contentWidth = this.contentWidth;
763 const viewportWidth = this.viewportWidth;
764
765 if (!this.option("infiniteX", this.option("infinite")) || this.pages.length < 2 || contentWidth < viewportWidth) {
766 return;
767 }
768
769 const panzoom = this.Panzoom;
770
771 let isFlipped = false;
772
773 if (panzoom.content.x < (contentWidth - viewportWidth) * -1) {
774 panzoom.content.x += contentWidth;
775
776 this.pageIndex = this.pageIndex - this.pages.length;
777
778 isFlipped = true;
779 }
780
781 if (panzoom.content.x > viewportWidth) {
782 panzoom.content.x -= contentWidth;
783
784 this.pageIndex = this.pageIndex + this.pages.length;
785
786 isFlipped = true;
787 }
788
789 if (isFlipped && panzoom.state === "pointerdown") {
790 panzoom.resetDragPosition();
791 }
792
793 return isFlipped;
794 }
795
796 /**
797 * Process `Panzoom.touchEnd` event; slide to next/prev page if needed
798 * @param {object} panzoom
799 */
800 onTouchEnd(panzoom, event) {
801 const dragFree = this.option("dragFree");
802
803 // If this is a quick horizontal flick, slide to next/prev slide
804 if (
805 !dragFree &&
806 this.pages.length > 1 &&
807 panzoom.dragOffset.time < 350 &&
808 Math.abs(panzoom.dragOffset.y) < 1 &&
809 Math.abs(panzoom.dragOffset.x) > 5
810 ) {
811 this[panzoom.dragOffset.x < 0 ? "slideNext" : "slidePrev"]();
812 return;
813 }
814
815 // Set the slide at the end of the animation as the current one,
816 // or slide to closest page
817 if (dragFree) {
818 const [, nextPageIndex] = this.getPageFromPosition(panzoom.transform.x * -1);
819 this.setPage(nextPageIndex);
820 } else {
821 this.slideToClosest();
822 }
823 }
824
825 /**
826 * Slides to the closest page (useful, if carousel is changed manually)
827 * @param {Object} [params] - Object containing additional options
828 */
829 slideToClosest(params = {}) {
830 let [, nextPageIndex] = this.getPageFromPosition(this.Panzoom.content.x * -1);
831
832 this.slideTo(nextPageIndex, params);
833 }
834
835 /**
836 * Returns index of closest page to given x position
837 * @param {Number} xPos
838 */
839 getPageFromPosition(xPos) {
840 const pageCount = this.pages.length;
841 const center = this.option("center");
842
843 if (center) {
844 xPos += this.viewportWidth * 0.5;
845 }
846
847 const interval = Math.floor(xPos / this.contentWidth);
848
849 xPos -= interval * this.contentWidth;
850
851 let slide = this.slides.find((slide) => slide.left <= xPos && slide.left + slide.width > xPos);
852
853 if (slide) {
854 let pageIndex = this.findPageForSlide(slide.index);
855
856 return [pageIndex, pageIndex + interval * pageCount];
857 }
858
859 return [0, 0];
860 }
861
862 /**
863 * Changes active page
864 * @param {Number} page - New index of active page
865 * @param {Boolean} toClosest - to closest page based on scroll distance (for infinite navigation)
866 */
867 setPage(page, toClosest) {
868 let nextPosition = 0;
869 let pageIndex = parseInt(page, 10) || 0;
870
871 const prevPage = this.page,
872 prevPageIndex = this.pageIndex,
873 pageCount = this.pages.length;
874
875 const contentWidth = this.contentWidth;
876 const viewportWidth = this.viewportWidth;
877
878 page = ((pageIndex % pageCount) + pageCount) % pageCount;
879
880 if (this.option("infiniteX", this.option("infinite")) && contentWidth > viewportWidth) {
881 const nextInterval = Math.floor(pageIndex / pageCount) || 0,
882 elemDimWidth = contentWidth;
883
884 nextPosition = this.pages[page].left + nextInterval * elemDimWidth;
885
886 if (toClosest === true && pageCount > 2) {
887 let currPosition = this.Panzoom.content.x * -1;
888
889 // * Find closest interval
890 const decreasedPosition = nextPosition - elemDimWidth,
891 increasedPosition = nextPosition + elemDimWidth,
892 diff1 = Math.abs(currPosition - nextPosition),
893 diff2 = Math.abs(currPosition - decreasedPosition),
894 diff3 = Math.abs(currPosition - increasedPosition);
895
896 if (diff3 < diff1 && diff3 <= diff2) {
897 nextPosition = increasedPosition;
898 pageIndex += pageCount;
899 } else if (diff2 < diff1 && diff2 < diff3) {
900 nextPosition = decreasedPosition;
901 pageIndex -= pageCount;
902 }
903 }
904 } else {
905 page = pageIndex = Math.max(0, Math.min(pageIndex, pageCount - 1));
906
907 nextPosition = this.pages.length ? this.pages[page].left : 0;
908 }
909
910 this.page = page;
911 this.pageIndex = pageIndex;
912
913 if (prevPage !== null && page !== prevPage) {
914 this.prevPage = prevPage;
915 this.prevPageIndex = prevPageIndex;
916
917 this.trigger("change", page, prevPage);
918 }
919
920 return nextPosition;
921 }
922
923 /**
924 * Clean up
925 */
926 destroy() {
927 this.state = "destroy";
928
929 this.slides.forEach((slide) => {
930 this.removeSlideEl(slide);
931 });
932
933 this.slides = [];
934
935 this.Panzoom.destroy();
936
937 this.detachPlugins();
938 }
939}
940
941// Expose version
942Carousel.version = "__VERSION__";
943
944// Static properties are a recent addition that dont work in all browsers yet
945Carousel.Plugins = Plugins;