|  | 
|  | 1 | +<template> | 
|  | 2 | +  <component | 
|  | 3 | +    :is="tag" | 
|  | 4 | +    :class="className" | 
|  | 5 | +    v-bind="$attrs" | 
|  | 6 | +    @mouseenter="handleMouseenter" | 
|  | 7 | +    @mouseleave="handleMouseleave" | 
|  | 8 | +    @keydown.right="handleRight" | 
|  | 9 | +    @keydown.left="handleLeft" | 
|  | 10 | +    @touchstart="handleTouchstart" | 
|  | 11 | +    @touchmove="handleTouchmove" | 
|  | 12 | +    @touchend="handleTouchend" | 
|  | 13 | +  > | 
|  | 14 | +    <div v-if="indicators" class="carousel-indicators"> | 
|  | 15 | +      <button | 
|  | 16 | +        v-for="(item, key) in items" | 
|  | 17 | +        :key="key" | 
|  | 18 | +        type="button" | 
|  | 19 | +        :class="activeItemKey === key && 'active'" | 
|  | 20 | +        :aria-current="activeItemKey === key && 'true'" | 
|  | 21 | +        :aria-label="`Slide ${key + 1}`" | 
|  | 22 | +        @click="slideTo(key)" | 
|  | 23 | +      ></button> | 
|  | 24 | +    </div> | 
|  | 25 | + | 
|  | 26 | +    <div class="carousel-inner" ref="carouselInnerRef"> | 
|  | 27 | +      <div v-for="(item, key) in items" class="carousel-item" :key="key"> | 
|  | 28 | +        <img :src="item.src" :alt="item.alt" :class="itemsClass" /> | 
|  | 29 | +        <div v-if="item.label || item.caption" :class="captionsClass"> | 
|  | 30 | +          <h5 v-if="item.label">{{ item.label }}</h5> | 
|  | 31 | +          <p v-if="item.caption">{{ item.caption }}</p> | 
|  | 32 | +        </div> | 
|  | 33 | +      </div> | 
|  | 34 | +    </div> | 
|  | 35 | + | 
|  | 36 | +    <button | 
|  | 37 | +      v-if="controls" | 
|  | 38 | +      @click="prev" | 
|  | 39 | +      class="carousel-control-prev" | 
|  | 40 | +      type="button" | 
|  | 41 | +    > | 
|  | 42 | +      <span class="carousel-control-prev-icon" aria-hidden="true"></span> | 
|  | 43 | +      <span class="visually-hidden">Previous</span> | 
|  | 44 | +    </button> | 
|  | 45 | +    <button | 
|  | 46 | +      v-if="controls" | 
|  | 47 | +      @click="next" | 
|  | 48 | +      class="carousel-control-next" | 
|  | 49 | +      type="button" | 
|  | 50 | +    > | 
|  | 51 | +      <span class="carousel-control-next-icon" aria-hidden="true"></span> | 
|  | 52 | +      <span class="visually-hidden">Next</span> | 
|  | 53 | +    </button> | 
|  | 54 | +  </component> | 
|  | 55 | +</template> | 
|  | 56 | + | 
|  | 57 | +<script> | 
|  | 58 | +import { computed, ref, onMounted, onUnmounted, watch } from "vue"; | 
|  | 59 | +
 | 
|  | 60 | +export default { | 
|  | 61 | +  name: "MDBCarousel", | 
|  | 62 | +  props: { | 
|  | 63 | +    captionsClass: { | 
|  | 64 | +      type: String, | 
|  | 65 | +      default: "carousel-caption d-none d-md-block" | 
|  | 66 | +    }, | 
|  | 67 | +    controls: { | 
|  | 68 | +      type: Boolean, | 
|  | 69 | +      default: true | 
|  | 70 | +    }, | 
|  | 71 | +    dark: Boolean, | 
|  | 72 | +    fade: Boolean, | 
|  | 73 | +    indicators: { | 
|  | 74 | +      type: Boolean, | 
|  | 75 | +      default: true | 
|  | 76 | +    }, | 
|  | 77 | +    interval: { | 
|  | 78 | +      type: [Number, Boolean], | 
|  | 79 | +      default: 5000 | 
|  | 80 | +    }, | 
|  | 81 | +    items: { | 
|  | 82 | +      type: Array, | 
|  | 83 | +      reguired: true | 
|  | 84 | +    }, | 
|  | 85 | +    itemsClass: { | 
|  | 86 | +      type: String, | 
|  | 87 | +      default: "d-block w-100" | 
|  | 88 | +    }, | 
|  | 89 | +    keyboard: { | 
|  | 90 | +      type: Boolean, | 
|  | 91 | +      default: true | 
|  | 92 | +    }, | 
|  | 93 | +    modelValue: { | 
|  | 94 | +      type: Number, | 
|  | 95 | +      default: 0 | 
|  | 96 | +    }, | 
|  | 97 | +    pause: { | 
|  | 98 | +      type: [String, Boolean], | 
|  | 99 | +      default: "hover" | 
|  | 100 | +    }, | 
|  | 101 | +    tag: { | 
|  | 102 | +      type: String, | 
|  | 103 | +      default: "div" | 
|  | 104 | +    }, | 
|  | 105 | +    touch: { | 
|  | 106 | +      type: Boolean, | 
|  | 107 | +      default: true | 
|  | 108 | +    } | 
|  | 109 | +  }, | 
|  | 110 | +  emits: ["update:modelValue"], | 
|  | 111 | +  setup(props, { emit }) { | 
|  | 112 | +    const className = computed(() => { | 
|  | 113 | +      return [ | 
|  | 114 | +        "carousel", | 
|  | 115 | +        "slide", | 
|  | 116 | +        props.fade && "carousel-fade", | 
|  | 117 | +        props.dark && "carousel-dark" | 
|  | 118 | +      ]; | 
|  | 119 | +    }); | 
|  | 120 | +
 | 
|  | 121 | +    const activeItemKey = ref(props.modelValue); | 
|  | 122 | +    const carouselInnerRef = ref(null); | 
|  | 123 | +    const isSliding = ref(false); | 
|  | 124 | +
 | 
|  | 125 | +    let slidingInterval = null; | 
|  | 126 | +    let isPaused = false; | 
|  | 127 | +
 | 
|  | 128 | +    const prev = () => { | 
|  | 129 | +      slideTo("prev"); | 
|  | 130 | +    }; | 
|  | 131 | +    const next = () => { | 
|  | 132 | +      slideTo("next"); | 
|  | 133 | +    }; | 
|  | 134 | +    const slideTo = target => { | 
|  | 135 | +      if (isSliding.value) { | 
|  | 136 | +        return; | 
|  | 137 | +      } | 
|  | 138 | +
 | 
|  | 139 | +      const isPausedState = isPaused; | 
|  | 140 | +      isPaused = false; | 
|  | 141 | +
 | 
|  | 142 | +      slide(target); | 
|  | 143 | +
 | 
|  | 144 | +      isPaused = isPausedState; | 
|  | 145 | +    }; | 
|  | 146 | +
 | 
|  | 147 | +    const slide = target => { | 
|  | 148 | +      if (isPaused || !carouselInnerRef.value) { | 
|  | 149 | +        return; | 
|  | 150 | +      } | 
|  | 151 | +
 | 
|  | 152 | +      isSliding.value = true; | 
|  | 153 | +      const targetItemKey = getTargetKey(target); | 
|  | 154 | +      const isNext = getTargetSlideOrder(target); | 
|  | 155 | +      const directionalClassName = getDirectionalClassName(isNext); | 
|  | 156 | +      const orderClassName = getOrderClassName(isNext); | 
|  | 157 | +      const currentItem = getItem(activeItemKey.value); | 
|  | 158 | +      const targetItem = getItem(targetItemKey); | 
|  | 159 | +
 | 
|  | 160 | +      activeItemKey.value = targetItemKey; | 
|  | 161 | +      targetItem.classList.add(orderClassName); | 
|  | 162 | +      emit("update:modelValue", activeItemKey.value); | 
|  | 163 | +
 | 
|  | 164 | +      if (props.interval) { | 
|  | 165 | +        reloadInterval(); | 
|  | 166 | +      } | 
|  | 167 | +
 | 
|  | 168 | +      setTimeout(() => { | 
|  | 169 | +        currentItem.classList.add(directionalClassName); | 
|  | 170 | +        targetItem.classList.add(directionalClassName); | 
|  | 171 | +      }, 20); | 
|  | 172 | +
 | 
|  | 173 | +      setTimeout(() => { | 
|  | 174 | +        currentItem.classList.remove("active"); | 
|  | 175 | +        currentItem.classList.remove(directionalClassName); | 
|  | 176 | +        targetItem.classList.remove(directionalClassName); | 
|  | 177 | +        targetItem.classList.remove(orderClassName); | 
|  | 178 | +        targetItem.classList.add("active"); | 
|  | 179 | +        isSliding.value = false; | 
|  | 180 | +      }, 600); | 
|  | 181 | +    }; | 
|  | 182 | +
 | 
|  | 183 | +    const getTargetKey = target => { | 
|  | 184 | +      if (target === "prev" && activeItemKey.value <= 0) { | 
|  | 185 | +        return props.items.length - 1; | 
|  | 186 | +      } else if (target === "prev") { | 
|  | 187 | +        return activeItemKey.value - 1; | 
|  | 188 | +      } else if ( | 
|  | 189 | +        target === "next" && | 
|  | 190 | +        activeItemKey.value >= props.items.length - 1 | 
|  | 191 | +      ) { | 
|  | 192 | +        return 0; | 
|  | 193 | +      } else if (target === "next") { | 
|  | 194 | +        return activeItemKey.value + 1; | 
|  | 195 | +      } else { | 
|  | 196 | +        return target; | 
|  | 197 | +      } | 
|  | 198 | +    }; | 
|  | 199 | +    const getTargetSlideOrder = target => { | 
|  | 200 | +      if (target === "next" || target > activeItemKey.value) { | 
|  | 201 | +        return true; | 
|  | 202 | +      } else { | 
|  | 203 | +        return false; | 
|  | 204 | +      } | 
|  | 205 | +    }; | 
|  | 206 | +    const getDirectionalClassName = isNext => | 
|  | 207 | +      isNext ? "carousel-item-start" : "carousel-item-end"; | 
|  | 208 | +    const getOrderClassName = isNext => | 
|  | 209 | +      isNext ? "carousel-item-next" : "carousel-item-prev"; | 
|  | 210 | +    const getItem = key => | 
|  | 211 | +      carouselInnerRef.value.querySelectorAll(".carousel-item")[key]; | 
|  | 212 | +
 | 
|  | 213 | +    const reloadInterval = () => { | 
|  | 214 | +      clearInterval(slidingInterval); | 
|  | 215 | +      slidingInterval = null; | 
|  | 216 | +
 | 
|  | 217 | +      const itemInterval = | 
|  | 218 | +        props.items[activeItemKey.value].interval || props.interval; | 
|  | 219 | +      slidingInterval = setInterval(() => { | 
|  | 220 | +        slide("next"); | 
|  | 221 | +      }, itemInterval); | 
|  | 222 | +    }; | 
|  | 223 | +
 | 
|  | 224 | +    // keyboard accessibility | 
|  | 225 | +    const handleMouseenter = () => { | 
|  | 226 | +      if (props.pause === "hover" && props.interval) { | 
|  | 227 | +        clearInterval(slidingInterval); | 
|  | 228 | +        slidingInterval = null; | 
|  | 229 | +        isPaused = true; | 
|  | 230 | +      } | 
|  | 231 | +    }; | 
|  | 232 | +    const handleMouseleave = () => { | 
|  | 233 | +      if (props.pause === "hover" && props.interval) { | 
|  | 234 | +        reloadInterval(); | 
|  | 235 | +        isPaused = false; | 
|  | 236 | +      } | 
|  | 237 | +    }; | 
|  | 238 | +    const handleRight = () => { | 
|  | 239 | +      if (props.keyboard) { | 
|  | 240 | +        next(); | 
|  | 241 | +      } | 
|  | 242 | +    }; | 
|  | 243 | +    const handleLeft = () => { | 
|  | 244 | +      if (props.keyboard) { | 
|  | 245 | +        prev(); | 
|  | 246 | +      } | 
|  | 247 | +    }; | 
|  | 248 | +
 | 
|  | 249 | +    // touch events | 
|  | 250 | +    const pointerEvent = Boolean(window.PointerEvent); | 
|  | 251 | +    const touchStartX = ref(0); | 
|  | 252 | +    const touchDeltaX = ref(0); | 
|  | 253 | +    const handleTouchstart = event => { | 
|  | 254 | +      if (!props.touch) { | 
|  | 255 | +        return; | 
|  | 256 | +      } | 
|  | 257 | +
 | 
|  | 258 | +      if ( | 
|  | 259 | +        pointerEvent && | 
|  | 260 | +        (event.pointerType === "pen" || event.pointerType === "touch") | 
|  | 261 | +      ) { | 
|  | 262 | +        touchStartX.value = event.clientX; | 
|  | 263 | +      } else { | 
|  | 264 | +        touchStartX.value = event.touches[0].clientX; | 
|  | 265 | +      } | 
|  | 266 | +    }; | 
|  | 267 | +    const handleTouchmove = event => { | 
|  | 268 | +      if (!props.touch) { | 
|  | 269 | +        return; | 
|  | 270 | +      } | 
|  | 271 | +
 | 
|  | 272 | +      touchDeltaX.value = | 
|  | 273 | +        event.touches && event.touches.length > 1 | 
|  | 274 | +          ? 0 | 
|  | 275 | +          : event.touches[0].clientX - touchStartX.value; | 
|  | 276 | +    }; | 
|  | 277 | +    const handleTouchend = event => { | 
|  | 278 | +      if (!props.touch) { | 
|  | 279 | +        return; | 
|  | 280 | +      } | 
|  | 281 | +
 | 
|  | 282 | +      if ( | 
|  | 283 | +        pointerEvent && | 
|  | 284 | +        (event.pointerType === "pen" || event.pointerType === "touch") | 
|  | 285 | +      ) { | 
|  | 286 | +        touchDeltaX.value = event.clientX - touchStartX.value; | 
|  | 287 | +      } | 
|  | 288 | +
 | 
|  | 289 | +      handleSwipe(); | 
|  | 290 | +    }; | 
|  | 291 | +    const handleSwipe = () => { | 
|  | 292 | +      const absDeltax = Math.abs(touchDeltaX.value); | 
|  | 293 | +
 | 
|  | 294 | +      if (absDeltax <= 40) { | 
|  | 295 | +        return; | 
|  | 296 | +      } | 
|  | 297 | +
 | 
|  | 298 | +      const direction = absDeltax / touchDeltaX.value; | 
|  | 299 | +      touchDeltaX.value = 0; | 
|  | 300 | +
 | 
|  | 301 | +      if (!direction) { | 
|  | 302 | +        return; | 
|  | 303 | +      } | 
|  | 304 | +
 | 
|  | 305 | +      if (direction > 0) { | 
|  | 306 | +        prev(); | 
|  | 307 | +      } else { | 
|  | 308 | +        next(); | 
|  | 309 | +      } | 
|  | 310 | +    }; | 
|  | 311 | +
 | 
|  | 312 | +    onMounted(() => { | 
|  | 313 | +      const currentActiveItem = carouselInnerRef.value.querySelectorAll( | 
|  | 314 | +        ".carousel-item" | 
|  | 315 | +      )[activeItemKey.value]; | 
|  | 316 | +      currentActiveItem.classList.add("active"); | 
|  | 317 | +
 | 
|  | 318 | +      if (props.interval) { | 
|  | 319 | +        reloadInterval(); | 
|  | 320 | +      } | 
|  | 321 | +    }); | 
|  | 322 | +
 | 
|  | 323 | +    onUnmounted(() => { | 
|  | 324 | +      if (props.interval) { | 
|  | 325 | +        clearInterval(slidingInterval); | 
|  | 326 | +        slidingInterval = null; | 
|  | 327 | +      } | 
|  | 328 | +    }); | 
|  | 329 | +
 | 
|  | 330 | +    watch( | 
|  | 331 | +      () => props.modelValue, | 
|  | 332 | +      targetItemKey => slideTo(targetItemKey) | 
|  | 333 | +    ); | 
|  | 334 | +
 | 
|  | 335 | +    return { | 
|  | 336 | +      className, | 
|  | 337 | +      carouselInnerRef, | 
|  | 338 | +      activeItemKey, | 
|  | 339 | +      handleMouseenter, | 
|  | 340 | +      handleMouseleave, | 
|  | 341 | +      handleRight, | 
|  | 342 | +      handleLeft, | 
|  | 343 | +      handleTouchstart, | 
|  | 344 | +      handleTouchmove, | 
|  | 345 | +      handleTouchend, | 
|  | 346 | +      slideTo, | 
|  | 347 | +      next, | 
|  | 348 | +      prev | 
|  | 349 | +    }; | 
|  | 350 | +  } | 
|  | 351 | +}; | 
|  | 352 | +</script> | 
0 commit comments