carousel.js 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933
  1. /**
  2. * Vue 3 Carousel 0.8.1
  3. * (c) 2024
  4. * @license MIT
  5. */
  6. (function (global, factory) {
  7. typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('vue')) :
  8. typeof define === 'function' && define.amd ? define(['exports', 'vue'], factory) :
  9. (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.VueCarousel = {}, global.Vue));
  10. })(this, (function (exports, vue) { 'use strict';
  11. const SNAP_ALIGN_OPTIONS = ['center', 'start', 'end', 'center-even', 'center-odd'];
  12. const BREAKPOINT_MODE_OPTIONS = ['viewport', 'carousel'];
  13. const DIR_OPTIONS = [
  14. 'ltr',
  15. 'left-to-right',
  16. 'rtl',
  17. 'right-to-left',
  18. 'ttb',
  19. 'top-to-bottom',
  20. 'btt',
  21. 'bottom-to-top',
  22. ];
  23. const I18N_DEFAULT_CONFIG = {
  24. ariaNextSlide: 'Navigate to next slide',
  25. ariaPreviousSlide: 'Navigate to previous slide',
  26. ariaNavigateToSlide: 'Navigate to slide {slideNumber}',
  27. ariaGallery: 'Gallery',
  28. itemXofY: 'Item {currentSlide} of {slidesCount}',
  29. iconArrowUp: 'Arrow pointing upwards',
  30. iconArrowDown: 'Arrow pointing downwards',
  31. iconArrowRight: 'Arrow pointing to the right',
  32. iconArrowLeft: 'Arrow pointing to the left',
  33. };
  34. const DEFAULT_CONFIG = {
  35. enabled: true,
  36. itemsToShow: 1,
  37. itemsToScroll: 1,
  38. modelValue: 0,
  39. transition: 300,
  40. autoplay: 0,
  41. gap: 0,
  42. height: 'auto',
  43. wrapAround: false,
  44. pauseAutoplayOnHover: false,
  45. mouseDrag: true,
  46. touchDrag: true,
  47. snapAlign: SNAP_ALIGN_OPTIONS[0],
  48. dir: DIR_OPTIONS[0],
  49. breakpointMode: BREAKPOINT_MODE_OPTIONS[0],
  50. breakpoints: undefined,
  51. i18n: I18N_DEFAULT_CONFIG,
  52. };
  53. const carouselProps = {
  54. // enable/disable the carousel component
  55. enabled: {
  56. default: DEFAULT_CONFIG.enabled,
  57. type: Boolean,
  58. },
  59. // count of items to showed per view
  60. itemsToShow: {
  61. default: DEFAULT_CONFIG.itemsToShow,
  62. type: Number,
  63. },
  64. // count of items to be scrolled
  65. itemsToScroll: {
  66. default: DEFAULT_CONFIG.itemsToScroll,
  67. type: Number,
  68. },
  69. // control infinite scrolling mode
  70. wrapAround: {
  71. default: DEFAULT_CONFIG.wrapAround,
  72. type: Boolean,
  73. },
  74. // control the gap between slides
  75. gap: {
  76. default: DEFAULT_CONFIG.gap,
  77. type: Number,
  78. },
  79. // control the gap between slides
  80. height: {
  81. default: DEFAULT_CONFIG.height,
  82. type: [Number, String],
  83. },
  84. // control snap position alignment
  85. snapAlign: {
  86. default: DEFAULT_CONFIG.snapAlign,
  87. validator(value) {
  88. return SNAP_ALIGN_OPTIONS.includes(value);
  89. },
  90. },
  91. // sliding transition time in ms
  92. transition: {
  93. default: DEFAULT_CONFIG.transition,
  94. type: Number,
  95. },
  96. // controls the breakpoint mode relative to the carousel container or the viewport
  97. breakpointMode: {
  98. default: DEFAULT_CONFIG.breakpointMode,
  99. validator(value) {
  100. return BREAKPOINT_MODE_OPTIONS.includes(value);
  101. },
  102. },
  103. // an object to store breakpoints
  104. breakpoints: {
  105. default: DEFAULT_CONFIG.breakpoints,
  106. type: Object,
  107. },
  108. // time to auto advance slides in ms
  109. autoplay: {
  110. default: DEFAULT_CONFIG.autoplay,
  111. type: Number,
  112. },
  113. // pause autoplay when mouse hover over the carousel
  114. pauseAutoplayOnHover: {
  115. default: DEFAULT_CONFIG.pauseAutoplayOnHover,
  116. type: Boolean,
  117. },
  118. // slide number number of initial slide
  119. modelValue: {
  120. default: undefined,
  121. type: Number,
  122. },
  123. // toggle mouse dragging.
  124. mouseDrag: {
  125. default: DEFAULT_CONFIG.mouseDrag,
  126. type: Boolean,
  127. },
  128. // toggle mouse dragging.
  129. touchDrag: {
  130. default: DEFAULT_CONFIG.touchDrag,
  131. type: Boolean,
  132. },
  133. // control snap position alignment
  134. dir: {
  135. default: DEFAULT_CONFIG.dir,
  136. validator(value) {
  137. // The value must match one of these strings
  138. return DIR_OPTIONS.includes(value);
  139. },
  140. },
  141. // aria-labels and additional text labels
  142. i18n: {
  143. default: DEFAULT_CONFIG.i18n,
  144. type: Object,
  145. },
  146. };
  147. /**
  148. * Determines the maximum slide index based on the configuration.
  149. *
  150. * @param {Args} args - The carousel configuration and slide count.
  151. * @returns {number} The maximum slide index.
  152. */
  153. function getMaxSlideIndex({ config, slidesCount }) {
  154. var _a;
  155. const { snapAlign = 'N/A', wrapAround, itemsToShow = 1 } = config;
  156. // If wrapAround is enabled, the max index is the last slide
  157. if (wrapAround) {
  158. return Math.max(slidesCount - 1, 0);
  159. }
  160. // Map snapAlign values to calculation logic
  161. const snapAlignCalculations = {
  162. start: Math.ceil(slidesCount - itemsToShow),
  163. end: Math.ceil(slidesCount - 1),
  164. center: slidesCount - Math.ceil((itemsToShow - 0.5) / 2),
  165. 'center-odd': slidesCount - Math.ceil((itemsToShow - 0.5) / 2),
  166. 'center-even': slidesCount - Math.ceil(itemsToShow / 2),
  167. };
  168. // Compute the max index based on snapAlign, or default to 0
  169. const calculateMaxIndex = (_a = snapAlignCalculations[snapAlign]) !== null && _a !== void 0 ? _a : 0;
  170. // Return the result ensuring it's non-negative
  171. return Math.max(calculateMaxIndex, 0);
  172. }
  173. /**
  174. * Determines the minimum slide index based on the configuration.
  175. *
  176. * @param {Args} args - The carousel configuration and slide count.
  177. * @returns {number} The minimum slide index.
  178. */
  179. function getMinSlideIndex({ config, slidesCount }) {
  180. var _a;
  181. const { snapAlign = 'N/A', wrapAround, itemsToShow = 1 } = config;
  182. // If wrapAround is enabled or itemsToShow exceeds slidesCount, the minimum index is always 0
  183. if (wrapAround || itemsToShow > slidesCount) {
  184. return 0;
  185. }
  186. // Map of snapAlign to offset calculations
  187. const snapAlignCalculations = {
  188. start: 0,
  189. end: Math.floor(itemsToShow - 1),
  190. center: Math.floor((itemsToShow - 1) / 2),
  191. 'center-odd': Math.floor((itemsToShow - 1) / 2),
  192. 'center-even': Math.floor((itemsToShow - 2) / 2),
  193. };
  194. // Return the calculated offset or default to 0 for invalid snapAlign values
  195. return (_a = snapAlignCalculations[snapAlign]) !== null && _a !== void 0 ? _a : 0;
  196. }
  197. function getNumberInRange({ val, max, min }) {
  198. if (max < min) {
  199. return val;
  200. }
  201. return Math.min(Math.max(val, min), max);
  202. }
  203. const calculateOffset = (snapAlign, itemsToShow) => {
  204. var _a;
  205. const offsetMap = {
  206. start: 0,
  207. center: (itemsToShow - 1) / 2,
  208. 'center-odd': (itemsToShow - 1) / 2,
  209. 'center-even': (itemsToShow - 2) / 2,
  210. end: itemsToShow - 1,
  211. };
  212. return (_a = offsetMap[snapAlign]) !== null && _a !== void 0 ? _a : 0; // Fallback to 0 for unknown snapAlign
  213. };
  214. function getScrolledIndex({ config, currentSlide, slidesCount }) {
  215. const { snapAlign = 'N/A', wrapAround, itemsToShow = 1 } = config;
  216. // Calculate the offset based on snapAlign
  217. const offset = calculateOffset(snapAlign, itemsToShow);
  218. // Compute the index with or without wrapAround
  219. if (!wrapAround) {
  220. return getNumberInRange({
  221. val: currentSlide - offset,
  222. max: slidesCount - itemsToShow,
  223. min: 0,
  224. });
  225. }
  226. return currentSlide - offset;
  227. }
  228. function getSlidesVNodes(vNode) {
  229. if (!vNode)
  230. return [];
  231. return vNode.reduce((acc, node) => {
  232. var _a;
  233. if (node.type === vue.Fragment) {
  234. return [...acc, ...getSlidesVNodes(node.children)];
  235. }
  236. if (((_a = node.type) === null || _a === void 0 ? void 0 : _a.name) === 'CarouselSlide') {
  237. return [...acc, node];
  238. }
  239. return acc;
  240. }, []);
  241. }
  242. function mapNumberToRange({ val, max, min = 0 }) {
  243. const mod = max - min + 1;
  244. return ((val - min) % mod + mod) % mod + min;
  245. }
  246. /**
  247. * return a throttle version of the function
  248. * Throttling
  249. *
  250. */
  251. // eslint-disable-next-line no-unused-vars
  252. function throttle(fn) {
  253. let isRunning = false;
  254. return function (...args) {
  255. if (!isRunning) {
  256. isRunning = true;
  257. requestAnimationFrame(() => {
  258. fn.apply(this, args);
  259. isRunning = false;
  260. });
  261. }
  262. };
  263. }
  264. /**
  265. * return a debounced version of the function
  266. * @param fn
  267. * @param delay
  268. */
  269. // eslint-disable-next-line no-unused-vars
  270. function debounce(fn, delay) {
  271. let timerId;
  272. return function (...args) {
  273. if (timerId) {
  274. clearTimeout(timerId);
  275. }
  276. timerId = setTimeout(() => {
  277. fn(...args);
  278. timerId = null;
  279. }, delay);
  280. };
  281. }
  282. function i18nFormatter(string = '', values = {}) {
  283. return Object.entries(values).reduce((acc, [key, value]) => acc.replace(`{${key}}`, String(value)), string);
  284. }
  285. var ARIAComponent = vue.defineComponent({
  286. name: 'ARIA',
  287. setup() {
  288. const config = vue.inject('config', vue.reactive(Object.assign({}, DEFAULT_CONFIG)));
  289. const currentSlide = vue.inject('currentSlide', vue.ref(0));
  290. const slidesCount = vue.inject('slidesCount', vue.ref(0));
  291. return () => vue.h('div', {
  292. class: ['carousel__liveregion', 'carousel__sr-only'],
  293. 'aria-live': 'polite',
  294. 'aria-atomic': 'true',
  295. }, i18nFormatter(config.i18n['itemXofY'], {
  296. currentSlide: currentSlide.value + 1,
  297. slidesCount: slidesCount.value,
  298. }));
  299. },
  300. });
  301. var Carousel = vue.defineComponent({
  302. name: 'Carousel',
  303. props: carouselProps,
  304. emits: [
  305. 'init',
  306. 'drag',
  307. 'slide-start',
  308. 'loop',
  309. 'update:modelValue',
  310. 'slide-end',
  311. 'before-init',
  312. ],
  313. setup(props, { slots, emit, expose }) {
  314. var _a;
  315. const root = vue.ref(null);
  316. const viewport = vue.ref(null);
  317. const slides = vue.ref([]);
  318. const slideSize = vue.ref(0);
  319. const slidesCount = vue.ref(0);
  320. const fallbackConfig = vue.computed(() => (Object.assign(Object.assign(Object.assign({}, DEFAULT_CONFIG), props), { i18n: Object.assign(Object.assign({}, DEFAULT_CONFIG.i18n), props.i18n), breakpoints: undefined })));
  321. // current active config
  322. const config = vue.reactive(Object.assign({}, fallbackConfig.value));
  323. // slides
  324. const currentSlideIndex = vue.ref((_a = props.modelValue) !== null && _a !== void 0 ? _a : 0);
  325. const prevSlideIndex = vue.ref(0);
  326. const middleSlideIndex = vue.ref(0);
  327. const maxSlideIndex = vue.ref(0);
  328. const minSlideIndex = vue.ref(0);
  329. let autoplayTimer = null;
  330. let transitionTimer = null;
  331. let resizeObserver = null;
  332. const effectiveSlideSize = vue.computed(() => slideSize.value + config.gap);
  333. const normalizeDir = vue.computed(() => {
  334. const dir = config.dir || 'lrt';
  335. const dirMap = {
  336. 'left-to-right': 'ltr',
  337. 'right-to-left': 'rtl',
  338. 'top-to-bottom': 'ttb',
  339. 'bottom-to-top': 'btt',
  340. };
  341. return dirMap[dir] || dir;
  342. });
  343. const isVertical = vue.computed(() => ['ttb', 'btt'].includes(normalizeDir.value));
  344. vue.provide('config', config);
  345. vue.provide('slidesCount', slidesCount);
  346. vue.provide('currentSlide', currentSlideIndex);
  347. vue.provide('maxSlide', maxSlideIndex);
  348. vue.provide('minSlide', minSlideIndex);
  349. vue.provide('slideSize', slideSize);
  350. vue.provide('isVertical', isVertical);
  351. vue.provide('normalizeDir', normalizeDir);
  352. function updateBreakpointsConfig() {
  353. var _a;
  354. // Determine the width source based on the 'breakpointMode' config
  355. const widthSource = (config.breakpointMode === 'carousel'
  356. ? (_a = root.value) === null || _a === void 0 ? void 0 : _a.getBoundingClientRect().width
  357. : window.innerWidth) || 0;
  358. const breakpointsArray = Object.keys(props.breakpoints || {})
  359. .map((key) => Number(key))
  360. .sort((a, b) => +b - +a);
  361. let newConfig = Object.assign({}, fallbackConfig.value);
  362. breakpointsArray.some((breakpoint) => {
  363. var _a;
  364. if (widthSource >= breakpoint) {
  365. newConfig = Object.assign(Object.assign({}, newConfig), (_a = props.breakpoints) === null || _a === void 0 ? void 0 : _a[breakpoint]);
  366. return true;
  367. }
  368. return false;
  369. });
  370. Object.assign(config, newConfig);
  371. }
  372. const handleResize = debounce(() => {
  373. updateBreakpointsConfig();
  374. updateSlidesData();
  375. updateSlideSize();
  376. }, 16);
  377. /**
  378. * Setup functions
  379. */
  380. function updateSlideSize() {
  381. if (!viewport.value)
  382. return;
  383. const rect = viewport.value.getBoundingClientRect();
  384. // Calculate the total gap space
  385. const totalGap = (config.itemsToShow - 1) * config.gap;
  386. // Calculate size based on orientation
  387. if (isVertical.value) {
  388. slideSize.value = (rect.height - totalGap) / config.itemsToShow;
  389. }
  390. else {
  391. slideSize.value = (rect.width - totalGap) / config.itemsToShow;
  392. }
  393. }
  394. function updateSlidesData() {
  395. if (slidesCount.value <= 0)
  396. return;
  397. middleSlideIndex.value = Math.ceil((slidesCount.value - 1) / 2);
  398. maxSlideIndex.value = getMaxSlideIndex({ config, slidesCount: slidesCount.value });
  399. minSlideIndex.value = getMinSlideIndex({ config, slidesCount: slidesCount.value });
  400. if (!config.wrapAround) {
  401. currentSlideIndex.value = getNumberInRange({
  402. val: currentSlideIndex.value,
  403. max: maxSlideIndex.value,
  404. min: minSlideIndex.value,
  405. });
  406. }
  407. }
  408. vue.onMounted(() => {
  409. vue.nextTick(() => updateSlideSize());
  410. // Overcome some edge cases
  411. setTimeout(() => updateSlideSize(), 1000);
  412. updateBreakpointsConfig();
  413. initAutoplay();
  414. window.addEventListener('resize', handleResize, { passive: true });
  415. resizeObserver = new ResizeObserver(handleResize);
  416. if (root.value) {
  417. resizeObserver.observe(root.value);
  418. }
  419. emit('init');
  420. });
  421. vue.onUnmounted(() => {
  422. if (transitionTimer) {
  423. clearTimeout(transitionTimer);
  424. }
  425. if (autoplayTimer) {
  426. clearInterval(autoplayTimer);
  427. }
  428. if (resizeObserver && root.value) {
  429. resizeObserver.unobserve(root.value);
  430. resizeObserver = null;
  431. }
  432. window.removeEventListener('resize', handleResize, {
  433. passive: true,
  434. });
  435. });
  436. /**
  437. * Carousel Event listeners
  438. */
  439. let isTouch = false;
  440. const startPosition = { x: 0, y: 0 };
  441. const dragged = vue.reactive({ x: 0, y: 0 });
  442. const isHover = vue.ref(false);
  443. const isDragging = vue.ref(false);
  444. const handleMouseEnter = () => {
  445. isHover.value = true;
  446. };
  447. const handleMouseLeave = () => {
  448. isHover.value = false;
  449. };
  450. function handleDragStart(event) {
  451. // Prevent drag initiation on input elements or if already sliding
  452. const targetTagName = event.target.tagName;
  453. if (['INPUT', 'TEXTAREA', 'SELECT'].includes(targetTagName) || isSliding.value) {
  454. return;
  455. }
  456. // Detect if the event is a touchstart or mousedown event
  457. isTouch = event.type === 'touchstart';
  458. // For mouse events, prevent default to avoid text selection
  459. if (!isTouch) {
  460. event.preventDefault();
  461. if (event.button !== 0) {
  462. // Ignore non-left-click mouse events
  463. return;
  464. }
  465. }
  466. // Initialize start positions for the drag
  467. startPosition.x = isTouch ? event.touches[0].clientX : event.clientX;
  468. startPosition.y = isTouch ? event.touches[0].clientY : event.clientY;
  469. // Attach event listeners for dragging and drag end
  470. const moveEvent = isTouch ? 'touchmove' : 'mousemove';
  471. const endEvent = isTouch ? 'touchend' : 'mouseup';
  472. document.addEventListener(moveEvent, handleDragging, { passive: false });
  473. document.addEventListener(endEvent, handleDragEnd, { passive: true });
  474. }
  475. const handleDragging = throttle((event) => {
  476. isDragging.value = true;
  477. // Get the current position based on the interaction type (touch or mouse)
  478. const currentX = isTouch ? event.touches[0].clientX : event.clientX;
  479. const currentY = isTouch ? event.touches[0].clientY : event.clientY;
  480. // Calculate deltas for X and Y axes
  481. const deltaX = currentX - startPosition.x;
  482. const deltaY = currentY - startPosition.y;
  483. // Update dragged state reactively
  484. dragged.x = deltaX;
  485. dragged.y = deltaY;
  486. // Emit a drag event for further customization if needed
  487. emit('drag', { deltaX, deltaY });
  488. });
  489. function handleDragEnd() {
  490. // Determine the active axis and direction multiplier
  491. const dragAxis = isVertical.value ? 'y' : 'x';
  492. const directionMultiplier = ['rtl', 'btt'].includes(normalizeDir.value) ? -1 : 1;
  493. // Calculate dragged slides with a tolerance to account for incomplete drags
  494. const tolerance = Math.sign(dragged[dragAxis]) * 0.4; // Smooth out small drags
  495. const draggedSlides = Math.round(dragged[dragAxis] / effectiveSlideSize.value + tolerance) *
  496. directionMultiplier;
  497. // Prevent accidental clicks when there is a slide drag
  498. if (draggedSlides && !isTouch) {
  499. const preventClick = (e) => {
  500. e.preventDefault();
  501. window.removeEventListener('click', preventClick);
  502. };
  503. window.addEventListener('click', preventClick);
  504. }
  505. // Slide to the appropriate slide index
  506. const targetSlideIndex = currentSlideIndex.value - draggedSlides;
  507. slideTo(targetSlideIndex);
  508. // Reset drag state
  509. dragged.x = 0;
  510. dragged.y = 0;
  511. isDragging.value = false;
  512. const moveEvent = isTouch ? 'touchmove' : 'mousemove';
  513. const endEvent = isTouch ? 'touchend' : 'mouseup';
  514. document.removeEventListener(moveEvent, handleDragging);
  515. document.removeEventListener(endEvent, handleDragEnd);
  516. }
  517. /**
  518. * Autoplay
  519. */
  520. function initAutoplay() {
  521. if (!config.autoplay || config.autoplay <= 0) {
  522. return;
  523. }
  524. autoplayTimer = setInterval(() => {
  525. if (config.pauseAutoplayOnHover && isHover.value) {
  526. return;
  527. }
  528. next();
  529. }, config.autoplay);
  530. }
  531. function resetAutoplay() {
  532. if (autoplayTimer) {
  533. clearInterval(autoplayTimer);
  534. autoplayTimer = null;
  535. }
  536. initAutoplay();
  537. }
  538. /**
  539. * Navigation function
  540. */
  541. const isSliding = vue.ref(false);
  542. function slideTo(slideIndex) {
  543. const currentVal = config.wrapAround
  544. ? slideIndex
  545. : getNumberInRange({
  546. val: slideIndex,
  547. max: maxSlideIndex.value,
  548. min: minSlideIndex.value,
  549. });
  550. if (currentSlideIndex.value === currentVal || isSliding.value) {
  551. return;
  552. }
  553. emit('slide-start', {
  554. slidingToIndex: slideIndex,
  555. currentSlideIndex: currentSlideIndex.value,
  556. prevSlideIndex: prevSlideIndex.value,
  557. slidesCount: slidesCount.value,
  558. });
  559. isSliding.value = true;
  560. prevSlideIndex.value = currentSlideIndex.value;
  561. currentSlideIndex.value = currentVal;
  562. transitionTimer = setTimeout(() => {
  563. if (config.wrapAround) {
  564. const mappedNumber = mapNumberToRange({
  565. val: currentVal,
  566. max: maxSlideIndex.value,
  567. min: 0,
  568. });
  569. if (mappedNumber !== currentSlideIndex.value) {
  570. currentSlideIndex.value = mappedNumber;
  571. emit('loop', {
  572. currentSlideIndex: currentSlideIndex.value,
  573. slidingToIndex: slideIndex,
  574. });
  575. }
  576. }
  577. emit('update:modelValue', currentSlideIndex.value);
  578. emit('slide-end', {
  579. currentSlideIndex: currentSlideIndex.value,
  580. prevSlideIndex: prevSlideIndex.value,
  581. slidesCount: slidesCount.value,
  582. });
  583. isSliding.value = false;
  584. resetAutoplay();
  585. }, config.transition);
  586. }
  587. function next() {
  588. slideTo(currentSlideIndex.value + config.itemsToScroll);
  589. }
  590. function prev() {
  591. slideTo(currentSlideIndex.value - config.itemsToScroll);
  592. }
  593. const nav = { slideTo, next, prev };
  594. vue.provide('nav', nav);
  595. vue.provide('isSliding', isSliding);
  596. function restartCarousel() {
  597. updateBreakpointsConfig();
  598. updateSlidesData();
  599. updateSlideSize();
  600. resetAutoplay();
  601. }
  602. // Update the carousel on props change
  603. vue.watch(() => (Object.assign({}, props)), restartCarousel, { deep: true });
  604. // Handle changing v-model value
  605. vue.watch(() => props['modelValue'], (val) => {
  606. if (val === currentSlideIndex.value) {
  607. return;
  608. }
  609. slideTo(Number(val));
  610. });
  611. // Handel when slides added/removed
  612. vue.watch(slidesCount, updateSlidesData);
  613. // Init carousel
  614. emit('before-init');
  615. const data = {
  616. config,
  617. slidesCount,
  618. slideSize,
  619. currentSlide: currentSlideIndex,
  620. maxSlide: maxSlideIndex,
  621. minSlide: minSlideIndex,
  622. middleSlide: middleSlideIndex,
  623. };
  624. expose({
  625. updateBreakpointsConfig,
  626. updateSlidesData,
  627. updateSlideSize,
  628. restartCarousel,
  629. slideTo,
  630. next,
  631. prev,
  632. nav,
  633. data,
  634. });
  635. /**
  636. * Track style
  637. */
  638. const trackTransform = vue.computed(() => {
  639. // Calculate the scrolled index with wrapping offset if applicable
  640. const scrolledIndex = getScrolledIndex({
  641. config,
  642. currentSlide: currentSlideIndex.value,
  643. slidesCount: slidesCount.value,
  644. });
  645. const cloneOffset = config.wrapAround ? slidesCount.value : 0;
  646. // Determine direction multiplier for orientation
  647. const isReverseDirection = ['rtl', 'btt'].includes(normalizeDir.value);
  648. const directionMultiplier = isReverseDirection ? -1 : 1;
  649. // Calculate the total offset for slide transformation
  650. const totalOffset = (scrolledIndex + cloneOffset) * effectiveSlideSize.value * directionMultiplier;
  651. // Include user drag interaction offset
  652. const dragOffset = isVertical.value ? dragged.y : dragged.x;
  653. // Generate the appropriate CSS transformation
  654. const translateAxis = isVertical.value ? 'Y' : 'X';
  655. return `translate${translateAxis}(${dragOffset - totalOffset}px)`;
  656. });
  657. const slotSlides = slots.default || slots.slides;
  658. const slotAddons = slots.addons;
  659. const slotsProps = vue.reactive(data);
  660. return () => {
  661. if (!config.enabled) {
  662. return vue.h('section', {
  663. ref: root,
  664. class: ['carousel', 'is-disabled'],
  665. }, slotSlides === null || slotSlides === void 0 ? void 0 : slotSlides());
  666. }
  667. const slidesElements = getSlidesVNodes(slotSlides === null || slotSlides === void 0 ? void 0 : slotSlides(slotsProps));
  668. const addonsElements = (slotAddons === null || slotAddons === void 0 ? void 0 : slotAddons(slotsProps)) || [];
  669. slidesElements.forEach((el, index) => {
  670. if (el.props) {
  671. el.props.index = index;
  672. }
  673. else {
  674. el.props = { index };
  675. }
  676. });
  677. let output = slidesElements;
  678. if (config.wrapAround) {
  679. const slidesBefore = slidesElements.map((el, index) => vue.cloneVNode(el, {
  680. index: -slidesElements.length + index,
  681. isClone: true,
  682. key: `clone-before-${index}`,
  683. }));
  684. const slidesAfter = slidesElements.map((el, index) => vue.cloneVNode(el, {
  685. index: slidesElements.length + index,
  686. isClone: true,
  687. key: `clone-after-${index}`,
  688. }));
  689. output = [...slidesBefore, ...slidesElements, ...slidesAfter];
  690. }
  691. slides.value = slidesElements;
  692. slidesCount.value = Math.max(slidesElements.length, 1);
  693. const trackEl = vue.h('ol', {
  694. class: 'carousel__track',
  695. style: {
  696. transform: trackTransform.value,
  697. transition: `${isSliding.value ? config.transition : 0}ms`,
  698. gap: `${config.gap}px`,
  699. },
  700. onMousedownCapture: config.mouseDrag ? handleDragStart : null,
  701. onTouchstartPassiveCapture: config.touchDrag ? handleDragStart : null,
  702. }, output);
  703. const viewPortEl = vue.h('div', { class: 'carousel__viewport', ref: viewport }, trackEl);
  704. return vue.h('section', {
  705. ref: root,
  706. class: [
  707. 'carousel',
  708. `is-${normalizeDir.value}`,
  709. {
  710. 'is-vertical': isVertical.value,
  711. 'is-sliding': isSliding.value,
  712. 'is-dragging': isDragging.value,
  713. 'is-hover': isHover.value,
  714. },
  715. ],
  716. style: {
  717. '--vc-trk-height': `${typeof config.height === 'number' ? `${config.height}px` : config.height}`,
  718. },
  719. dir: normalizeDir.value,
  720. 'aria-label': config.i18n['ariaGallery'],
  721. tabindex: '0',
  722. onMouseenter: handleMouseEnter,
  723. onMouseleave: handleMouseLeave,
  724. }, [viewPortEl, addonsElements, vue.h(ARIAComponent)]);
  725. };
  726. },
  727. });
  728. var IconName;
  729. (function (IconName) {
  730. IconName["arrowUp"] = "arrowUp";
  731. IconName["arrowDown"] = "arrowDown";
  732. IconName["arrowRight"] = "arrowRight";
  733. IconName["arrowLeft"] = "arrowLeft";
  734. })(IconName || (IconName = {}));
  735. const icons = {
  736. arrowUp: 'M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z',
  737. arrowDown: 'M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z',
  738. arrowRight: 'M8.59 16.59L13.17 12 8.59 7.41 10 6l6 6-6 6-1.41-1.41z',
  739. arrowLeft: 'M15.41 16.59L10.83 12l4.58-4.59L14 6l-6 6 6 6 1.41-1.41z',
  740. };
  741. function isIconName(candidate) {
  742. return candidate in IconName;
  743. }
  744. const Icon = (props) => {
  745. const config = vue.inject('config', vue.reactive(Object.assign({}, DEFAULT_CONFIG)));
  746. const iconName = String(props.name);
  747. const iconI18n = `icon${iconName.charAt(0).toUpperCase() + iconName.slice(1)}`;
  748. if (!iconName || typeof iconName !== 'string' || !isIconName(iconName)) {
  749. return;
  750. }
  751. const path = icons[iconName];
  752. const pathEl = vue.h('path', { d: path });
  753. const iconTitle = config.i18n[iconI18n] || props.title || iconName;
  754. const titleEl = vue.h('title', iconTitle);
  755. return vue.h('svg', {
  756. class: 'carousel__icon',
  757. viewBox: '0 0 24 24',
  758. role: 'img',
  759. 'aria-label': iconTitle,
  760. }, [titleEl, pathEl]);
  761. };
  762. Icon.props = { name: String, title: String };
  763. const Navigation = (props, { slots, attrs }) => {
  764. const { next: slotNext, prev: slotPrev } = slots || {};
  765. const config = vue.inject('config', vue.reactive(Object.assign({}, DEFAULT_CONFIG)));
  766. const maxSlide = vue.inject('maxSlide', vue.ref(1));
  767. const minSlide = vue.inject('minSlide', vue.ref(1));
  768. const normalizeDir = vue.inject('normalizeDir', vue.ref('ltr'));
  769. const currentSlide = vue.inject('currentSlide', vue.ref(1));
  770. const nav = vue.inject('nav', {});
  771. const { wrapAround, i18n } = config;
  772. const getPrevIcon = () => {
  773. const directionIcons = {
  774. ltr: 'arrowLeft',
  775. rtl: 'arrowRight',
  776. ttb: 'arrowUp',
  777. btt: 'arrowDown',
  778. };
  779. return directionIcons[normalizeDir.value];
  780. };
  781. const getNextIcon = () => {
  782. const directionIcons = {
  783. ltr: 'arrowRight',
  784. rtl: 'arrowLeft',
  785. ttb: 'arrowDown',
  786. btt: 'arrowUp',
  787. };
  788. return directionIcons[normalizeDir.value];
  789. };
  790. const prevButton = vue.h('button', {
  791. type: 'button',
  792. class: [
  793. 'carousel__prev',
  794. !wrapAround && currentSlide.value <= minSlide.value && 'carousel__prev--disabled',
  795. attrs === null || attrs === void 0 ? void 0 : attrs.class,
  796. ],
  797. 'aria-label': i18n['ariaPreviousSlide'],
  798. title: i18n['ariaPreviousSlide'],
  799. onClick: nav.prev,
  800. }, (slotPrev === null || slotPrev === void 0 ? void 0 : slotPrev()) || vue.h(Icon, { name: getPrevIcon() }));
  801. const nextButton = vue.h('button', {
  802. type: 'button',
  803. class: [
  804. 'carousel__next',
  805. !wrapAround && currentSlide.value >= maxSlide.value && 'carousel__next--disabled',
  806. attrs === null || attrs === void 0 ? void 0 : attrs.class,
  807. ],
  808. 'aria-label': i18n['ariaNextSlide'],
  809. title: i18n['ariaNextSlide'],
  810. onClick: nav.next,
  811. }, (slotNext === null || slotNext === void 0 ? void 0 : slotNext()) || vue.h(Icon, { name: getNextIcon() }));
  812. return [prevButton, nextButton];
  813. };
  814. const Pagination = () => {
  815. const config = vue.inject('config', vue.reactive(Object.assign({}, DEFAULT_CONFIG)));
  816. const maxSlide = vue.inject('maxSlide', vue.ref(1));
  817. const minSlide = vue.inject('minSlide', vue.ref(1));
  818. const currentSlide = vue.inject('currentSlide', vue.ref(1));
  819. const nav = vue.inject('nav', {});
  820. const isActive = (slide) => mapNumberToRange({
  821. val: currentSlide.value,
  822. max: maxSlide.value,
  823. min: 0,
  824. }) === slide;
  825. const children = [];
  826. for (let slide = minSlide.value; slide < maxSlide.value + 1; slide++) {
  827. const buttonLabel = i18nFormatter(config.i18n['ariaNavigateToSlide'], {
  828. slideNumber: slide + 1,
  829. });
  830. const button = vue.h('button', {
  831. type: 'button',
  832. class: {
  833. 'carousel__pagination-button': true,
  834. 'carousel__pagination-button--active': isActive(slide),
  835. },
  836. 'aria-label': buttonLabel,
  837. title: buttonLabel,
  838. onClick: () => nav.slideTo(slide),
  839. });
  840. const item = vue.h('li', { class: 'carousel__pagination-item', key: slide }, button);
  841. children.push(item);
  842. }
  843. return vue.h('ol', { class: 'carousel__pagination' }, children);
  844. };
  845. var Slide = vue.defineComponent({
  846. name: 'CarouselSlide',
  847. props: {
  848. index: {
  849. type: Number,
  850. default: 1,
  851. },
  852. isClone: {
  853. type: Boolean,
  854. default: false,
  855. },
  856. },
  857. setup(props, { slots }) {
  858. const config = vue.inject('config', vue.reactive(Object.assign({}, DEFAULT_CONFIG)));
  859. const currentSlide = vue.inject('currentSlide', vue.ref(0));
  860. const slidesToScroll = vue.inject('slidesToScroll', vue.ref(0));
  861. const isSliding = vue.inject('isSliding', vue.ref(false));
  862. const isVertical = vue.inject('isVertical', vue.ref(false));
  863. const slideSize = vue.inject('slideSize', vue.ref(0));
  864. const isActive = vue.computed(() => props.index === currentSlide.value);
  865. const isPrev = vue.computed(() => props.index === currentSlide.value - 1);
  866. const isNext = vue.computed(() => props.index === currentSlide.value + 1);
  867. const isVisible = vue.computed(() => {
  868. const min = Math.floor(slidesToScroll.value);
  869. const max = Math.ceil(slidesToScroll.value + config.itemsToShow - 1);
  870. return props.index >= min && props.index <= max;
  871. });
  872. const slideStyle = vue.computed(() => {
  873. const dimension = config.gap
  874. ? `${slideSize.value}px`
  875. : `${100 / config.itemsToShow}%`;
  876. return isVertical.value
  877. ? { height: dimension, width: '' }
  878. : { width: dimension, height: '' };
  879. });
  880. return () => {
  881. var _a, _b;
  882. if (!config.enabled) {
  883. return (_a = slots.default) === null || _a === void 0 ? void 0 : _a.call(slots);
  884. }
  885. return vue.h('li', {
  886. style: slideStyle.value,
  887. class: {
  888. carousel__slide: true,
  889. 'carousel__slide--clone': props.isClone,
  890. 'carousel__slide--visible': isVisible.value,
  891. 'carousel__slide--active': isActive.value,
  892. 'carousel__slide--prev': isPrev.value,
  893. 'carousel__slide--next': isNext.value,
  894. 'carousel__slide--sliding': isSliding.value,
  895. },
  896. 'aria-hidden': !isVisible.value,
  897. }, (_b = slots.default) === null || _b === void 0 ? void 0 : _b.call(slots, {
  898. isActive: isActive.value,
  899. isClone: props.isClone,
  900. isPrev: isPrev.value,
  901. isNext: isNext.value,
  902. isSliding: isSliding.value,
  903. isVisible: isVisible.value,
  904. }));
  905. };
  906. },
  907. });
  908. exports.Carousel = Carousel;
  909. exports.Icon = Icon;
  910. exports.Navigation = Navigation;
  911. exports.Pagination = Pagination;
  912. exports.Slide = Slide;
  913. }));