carousel.es.js 34 KB

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