I. Component introduction

Website links: Carousel components | Element (gitee. IO).

The Carousel component, also known as a Carousel diagram, is used to display banners in a Carousel on the home page of a website.

Carousel needs to work with the Carousel-item component.

1.1 attributes

1.1.1 according to class

  • Height: String, set the height of the lantern;
  • Type: String type, a merry-go-round type, can be set to card;
  • Indicator position: String, setting the position of the indicator. Outside or None;
  • Direction: The value is a string. The optional value ishorizontal/verticalHorizontal by default;

1.1.2 control class

  • Initial-index: indicates the number type. The value is the index of the image displayed during the initial state.
  • Trigger: String type; Set the trigger mode of the indicator, default is hover, can be set to click;
  • Autoplay: Boolean indicates whether to switch automatically. The default value is True.
  • Interval: indicates the number type. The unit is ms. The default value is 3000 (3s).
  • Arrow: The arrow type is string. The arrow display time can be set toalways/hover/neverThe default is hover;
  • Loop: Boolean type, which sets the loop display. Default is true.
  • Pause-on-hover: Boolean type, pause switching when hover, default is true;

1.2 event

  • “Change” : triggered when the picture is switchedCurrent active Index, previously active Index

1.3 Callable APIS

  • SetActiveItem: Toggle slide; Parameter: slide index orcarousel-itemThe name attribute value of;
  • Prev: Switch to the previous slide;
  • Next: Switch to the next slide;

Second, source code analysis

2.1 Carousel components

2.1.1 the template

<template>
  <div
    ref="root"
    :class="carouselClasses"// Mouse in/out event, used to pause/restart the rowhead timer @mouseenter.stop="handleMouseEnter"
    @mouseleave.stop="handleMouseLeave"
  >
    <div class="el-carousel__container" :style="{ height: height }">// Switch arrows left and right<transition v-if="arrowDisplay" name="carousel-arrow-left">
        <button
          v-show=" (arrow === 'always' || data.hover) && (props.loop || data.activeIndex > 0) "
          type="button"
          class="el-carousel__arrow el-carousel__arrow--left"
          @mouseenter="handleButtonEnter('left')"
          @mouseleave="handleButtonLeave"// Throttling @click.stop="throttledArrowClick(data.activeIndex - 1)"
        >
          <i class="el-icon-arrow-left"></i>
        </button>
      </transition>
      <transition v-if="arrowDisplay" name="carousel-arrow-right">
        <button
          v-show=" (arrow === 'always' || data.hover) && (props.loop || data.activeIndex < items.length - 1) "
          type="button"
          class="el-carousel__arrow el-carousel__arrow--right"
          @mouseenter="handleButtonEnter('right')"
          @mouseleave="handleButtonLeave"
          @click.stop="throttledArrowClick(data.activeIndex + 1)"
        >
          <i class="el-icon-arrow-right"></i>
        </button>
      </transition>
      <slot></slot>
    </div>/ / indicator<ul v-if="indicatorPosition ! == 'none'" :class="indicatorsClasses">
      <li
        v-for="(item, index) in items"
        :key="index"
        :class="[ 'el-carousel__indicator', 'el-carousel__indicator--' + direction, { 'is-active': index === data.activeIndex }, ]"
        @mouseenter="throttledIndicatorHover(index)"
        @click.stop="handleIndicatorClick(index)"
      >
        <button class="el-carousel__button">
          <span v-if="hasLabel">{{ item.label }}</span>
        </button>
      </li>
    </ul>
  </div>
</template>
Copy the code

2.1.2 script

// Part of the core source

setup(props: ICarouselProps, { emit }) {
    // data
    const data = reactive<{
      activeIndex: number
      containerWidth: number
      timer: null | ReturnType<typeof setInterval>
      hover: boolean({} >activeIndex: -1.containerWidth: 0.timer: null.hover: false,})// refs
    const root = ref(null)
    // Store CarouselItem subcomponent data
    const items = ref<CarouselItem[]>([])

    // Calculate the attribute that controls whether the left and right arrow arrow is displayed or not: arrow is set to never and diretcion is vertical
    const arrowDisplay = computed(
      () = >props.arrow ! = ='never'&& props.direction ! = ='vertical'.)// Calculate whether the carousel-item has the lable attribute
    const hasLabel = computed(() = > {
      return items.value.some(item= > item.label.toString().length > 0)})// Calculate the class of the attribute, carousel
    const carouselClasses = computed(() = > {
      const classes = ['el-carousel'.'el-carousel--' + props.direction]
      if (props.type === 'card') {
        classes.push('el-carousel--card')}return classes
    })
    // Calculate the class of the attribute indicator
    const indicatorsClasses = computed(() = > {
      const classes = [
        'el-carousel__indicators'.'el-carousel__indicators--' + props.direction,
      ]
      if (hasLabel.value) {
        classes.push('el-carousel__indicators--labels')}if (props.indicatorPosition === 'outside' || props.type === 'card') {
        classes.push('el-carousel__indicators--outside')}return classes
    })

    // methods
    // The carousel-item registration method is executed in the Carousel-item component using the provide/inject mode
    function addItem(item) {
      items.value.push(item)
    }
    // Carousel-item unload method
    function removeItem(uid) {
      const index = items.value.findIndex(item= > item.uid === uid)
      if(index ! = = -1) {
        items.value.splice(index, 1)
        if(data.activeIndex === index) next()
      }
    }
    // Throttling processing, to avoid the slide in the rotation, while the user click left and right arrow switch caused by flashing
    const throttledArrowClick = throttle(
      index= > {
        setActiveItem(index)
      },
      300,
      { trailing: true},)// Throttling processing, indicator hover event toggle
    const throttledIndicatorHover = throttle(index= > {
      handleIndicatorHover(index)
    }, 300)
    
    // Clear the timer
    function pauseTimer() {
      if (data.timer) {
        clearInterval(data.timer)
        data.timer = null}}// Start timer
    function startTimer() {
      if (props.interval <= 0| |! props.autoplay || data.timer)return
      // Execute playSides in the timer callback
      data.timer = setInterval(() = > playSlides(), props.interval)
    }
    
    / / set activeIndex
    const playSlides = () = > {
      if (data.activeIndex < items.value.length - 1) {
        data.activeIndex = data.activeIndex + 1
      } else if (props.loop) {
        // Loop
        data.activeIndex = 0}}// Sets the activation object, which can be called internally by the component or by the consumer through the REF
    function setActiveItem(index) {
      // The string parameter is the name attribute of the carousel-item that the user can pass in when calling the API
      if (typeof index === 'string') {
        const filteredItems = items.value.filter(item= > item.name === index)
        if (filteredItems.length > 0) {
          index = items.value.indexOf(filteredItems[0])
        }
      }
      index = Number(index)
      if (isNaN(index) || index ! = =Math.floor(index)) {
        console.warn('[Element Warn][Carousel]index must be an integer.')
        return
      }
      let length = items.value.length
      const oldIndex = data.activeIndex
      if (index < 0) {
        data.activeIndex = props.loop ? length - 1 : 0
      } else if (index >= length) {
        data.activeIndex = props.loop ? 0 : length - 1
      } else {
        data.activeIndex = index
      }
      if (oldIndex === data.activeIndex) {
        // Reset the slide position
        resetItemPosition(oldIndex)
      }
    }
    
    // Set the location of the carouse item
    function resetItemPosition(oldIndex) {
      items.value.forEach((item, index) = > {
        // translateItem is the method of the Carousel-item subcomponent to set the offset position of the slide
        item.translateItem(index, data.activeIndex, oldIndex)
      })
    }
    
    // Instage indicates whether the slide is currently playing in card mode
    function itemInStage(item, index) {
      const length = items.value.length
      if (
        (index === length - 1 && item.inStage && items.value[0].active) ||
        (item.inStage &&
          items.value[index + 1] &&
          items.value[index + 1].active)
      ) {
        return 'left'
      } else if (
        (index === 0 && item.inStage && items.value[length - 1].active) ||
        (item.inStage &&
          items.value[index - 1] &&
          items.value[index - 1].active)
      ) {
        return 'right'
      }
      return false
    }
    
    // Move the mouse to pause the timer
    function handleMouseEnter() {
      data.hover = true
      if (props.pauseOnHover) {
        pauseTimer()
      }
    }
    
    // Move the mouse away to start the timer
    function handleMouseLeave() {
      data.hover = false
      startTimer()
    }
    
    // The mouse enters the left-right toggle arrow
    function handleButtonEnter(arrow) {
      if (props.direction === 'vertical') return
      items.value.forEach((item, index) = > {
        if (arrow === itemInStage(item, index)) {
          item.hover = true}})}// Mouse away left and right toggle arrow
    function handleButtonLeave() {
      if (props.direction === 'vertical') return
      items.value.forEach(item= > {
        item.hover = false})}// set activeIndex to the index of the click
    function handleIndicatorClick(index) {
      data.activeIndex = index
    }
    
    // indicator Hover event. If trigger is set to Hover, activeIndex is set to the index of the click
    function handleIndicatorHover(index) {
      if (props.trigger === 'hover'&& index ! == data.activeIndex) { data.activeIndex = index } }// Switch to the previous method
    function prev() {
      setActiveItem(data.activeIndex - 1)}// Switch to the next method
    function next() {
      setActiveItem(data.activeIndex + 1)}// watch
    // monitor the change of activeIndex,
    watch(
      () = > data.activeIndex,
      (current, prev) = > {
        resetItemPosition(prev)
        if (prev > -1) {
          // Emit the change event
          emit('change', current, prev)
        }
      },
    )
    // Monitor autoPlay changes and start/pause the timer
    watch(
      () = > props.autoplay,
      current= > {
        current ? startTimer() : pauseTimer()
      },
    )
    // Monitor loop changes,
    watch(
      () = > props.loop,
      () = > {
        setActiveItem(data.activeIndex)
      },
    )

    // lifecycle
    onMounted(() = > {
      nextTick(() = > {
        // Listen for the resize event
        addResizeListener(root.value, resetItemPosition)
        // initialIndex Set activeIndex to initialIndex when the range is reasonable
        if (
          props.initialIndex < items.value.length &&
          props.initialIndex >= 0
        ) {
          data.activeIndex = props.initialIndex
        }
        // Start timer
        startTimer()
      })
    })

    onBeforeUnmount(() = > {
      // Clear event listener during uninstallation
      if (root.value) removeResizeListener(root.value, resetItemPosition)
      pauseTimer()
    })

    // provide, provide data to the child component
    provide<InjectCarouselScope>('injectCarouselScope', {
      root,
      direction: props.direction,
      type: props.type,
      items,
      loop: props.loop,
      addItem,
      removeItem,
      setActiveItem,
    })
  },
Copy the code

2.2 Carousel – item component

2.2.1 the template

<template>
  <div
    v-show="data.ready"
    class="el-carousel__item"
    :class="{ 'is-active': data.active, 'el-carousel__item--card': type === 'card', 'is-in-stage': data.inStage, 'is-hover': data.hover, 'is-animating': data.animating, }"
    :style="itemStyle"
    @click="handleItemClick"
  >
    <div
      v-if="type === 'card'"
      v-show=! "" data.active"
      class="el-carousel__mask"
    ></div>
    <slot></slot>
  </div>
</template>
Copy the code

2.2.2 script

setup(props: ICarouselItemProps) {
    // Get the component instance
    const instance = getCurrentInstance()

    // data
    const data = reactive({
      hover: false.translate: 0.scale: 1.active: false.ready: false.inStage: false.animating: false,})// inject data passed by the parent component
    const injectCarouselScope: InjectCarouselScope = inject(
      'injectCarouselScope'.)// computed
    // Direction of the parent component
    const parentDirection = computed(() = > {
      return injectCarouselScope.direction
    })
    // Dynamic inline style,
    const itemStyle = computed(() = > {
      const translateType =
        parentDirection.value === 'vertical' ? 'translateY' : 'translateX'
      const value = `${translateType}(${data.translate}px) scale(${data.scale}) `
      // eg:style="transform: translateX(673px) scale(1);"
      const style: PartialCSSStyleDeclaration = {
        transform: value,
      }
      // Autoprefixer is a tool method for adding browser compatible prefixes such as MS -,webkit-, etc
      return autoprefixer(style)
    })

    // methods
    // In the case of looping, calculate a reasonable index based on the index passed in, the length of the currently active index and items
    function processIndex(index, activeIndex, length) {
      if (activeIndex === 0 && index === length - 1) {
        return -1
      } else if (activeIndex === length - 1 && index === 0) {
        return length
      } else if (index < activeIndex - 1 && activeIndex - index >= length / 2) {
        return length + 1
      } else if (index > activeIndex + 1 && index - activeIndex >= length / 2) {
        return -2
      }
      return index
    }
    
    // Calculate the displacement distance in card mode
    function calcCardTranslate(index, activeIndex) {
      constparentWidth = injectCarouselScope.root.value? .offsetWidth ||0
      // inStage refers to the current slide in card mode
      if (data.inStage) {
        return (
          (parentWidth * ((2 - CARD_SCALE) * (index - activeIndex) + 1)) / 4)}else if (index < activeIndex) {
        return(- (1 + CARD_SCALE) * parentWidth) / 4
      } else {
        return ((3 + CARD_SCALE) * parentWidth) / 4}}// calculate displacement distance: displacement distance = width/height of parent container * (index and activeIndex difference)
    function calcTranslate(index, activeIndex, isVertical) {
      constdistance = (isVertical ? injectCarouselScope.root.value? .offsetHeight : injectCarouselScope.root.value? .offsetWidth) ||0
      return distance * (index - activeIndex)
    }

    const translateItem = (
      index: number,
      activeIndex: number,
      oldIndex: number.) = > {
      const parentType = injectCarouselScope.type
      const length = injectCarouselScope.items.value.length
      // Add animating properties to the current slide and the previous slide
      if(parentType ! = ='card'&& oldIndex ! = =undefined) {
        data.animating = index === activeIndex || index === oldIndex
      }
      // In the loop case, processIndex is called to calculate the actual Index
      if(index ! == activeIndex && length >2 && injectCarouselScope.loop) {
        index = processIndex(index, activeIndex, length)
      }
      / / card mode
      if (parentType === 'card') {
         // The card mode does not support vertical form
        if (parentDirection.value === 'vertical') {
          console.warn(
            '[Element Warn][Carousel]vertical direction is not supported in card mode',
          )
        }
        data.inStage = Math.round(Math.abs(index - activeIndex)) <= 1
        data.active = index === activeIndex
        // Call calcCardTranslate to calculate the offset distance
        data.translate = calcCardTranslate(index, activeIndex)
        data.scale = data.active ? 1 : CARD_SCALE
      } else {
      // Normal mode
        data.active = index === activeIndex
        const isVertical = parentDirection.value === 'vertical'
        // Calculate the offset distance
        data.translate = calcTranslate(index, activeIndex, isVertical)
      }
      data.ready = true
    }
    
    // Click the slide
    function handleItemClick() {
      if (injectCarouselScope && injectCarouselScope.type === 'card') {
        const index = injectCarouselScope.items.value
          .map(d= > d.uid)
          .indexOf(instance.uid)
         // Call the setActiveItem method of the parent component
        injectCarouselScope.setActiveItem(index)
      }
    }

    // lifecycle
    onMounted(() = > {
      // Register by calling the parent's addItem method
      if (injectCarouselScope.addItem) {
        injectCarouselScope.addItem({
          uid: instance.uid, ... props, ... toRefs(data), translateItem, }) } }) onUnmounted(() = > {
       // Cancel registration
      if (injectCarouselScope.removeItem) {
        injectCarouselScope.removeItem(instance.uid)
      }
    })

    return {
      data,
      itemStyle,
      translateItem,
      type: injectCarouselScope.type,
      handleItemClick,
    }
  },
}
Copy the code

2.3 summarize

  1. Provide/Inject Provides data interaction between parent and child components.
  2. The parent component is responsible for maintaining the activeIndex, providing the setActiveItem method to set the active slide;
  3. The child component provides the translateItem method to calculate the offset based on its own index and activeIndex, including the width/height of the parent component;
  4. The principle of the lantern style is: all slides are tiled/vertically displayed, and the offset distance of each slide is adjusted by translateX/Y