element carousel源码
src/main.vue
<template> <div :class="carouselClasses" @mouseenter.stop="handleMouseEnter" @mouseleave.stop="handleMouseLeave"> <div class="el-carousel__container" :style="{ height: height }"> <transition v-if="arrowDisplay" name="carousel-arrow-left"> <button type="button" v-show="(arrow === 'always' || hover) && (loop || activeIndex > 0)" @mouseenter="handleButtonEnter('left')" @mouseleave="handleButtonLeave" @click.stop="throttledArrowClick(activeIndex - 1)" class="el-carousel__arrow el-carousel__arrow--left"> <i class="el-icon-arrow-left"></i> </button> </transition> <transition v-if="arrowDisplay" name="carousel-arrow-right"> <button type="button" v-show="(arrow === 'always' || hover) && (loop || activeIndex < items.length - 1)" @mouseenter="handleButtonEnter('right')" @mouseleave="handleButtonLeave" @click.stop="throttledArrowClick(activeIndex + 1)" class="el-carousel__arrow el-carousel__arrow--right"> <i class="el-icon-arrow-right"></i> </button> </transition> <slot></slot> </div> <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 === 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> <script> import throttle from 'throttle-debounce/throttle'; import { addResizeListener, removeResizeListener } from 'element-ui/src/utils/resize-event'; export default { name: 'ElCarousel', props: { // 初始状态激活的幻灯片的索引,从 0 开始 initialIndex: { type: Number, default: 0 }, // 走马灯的高度 height: String, // 指示器的触发方式 trigger: { type: String, default: 'hover' }, // 是否自动切换 autoplay: { type: Boolean, default: true }, // 自动切换的时间间隔,单位为毫秒 interval: { type: Number, default: 3000 }, // 指示器的位置 indicatorPosition: String, // 是否显示指示器 indicator: { type: Boolean, default: true }, // 切换箭头的显示时机 arrow: { type: String, default: 'hover' }, // 走马灯的类型 type: String, // 是否循环显示 loop: { type: Boolean, default: true }, // 走马灯展示的方向 direction: { type: String, default: 'horizontal', validator(val) { return ['horizontal', 'vertical'].indexOf(val) !== -1; } } }, data() { return { items: [], activeIndex: -1, containerWidth: 0, timer: null, hover: false }; }, computed: { arrowDisplay() { return this.arrow !== 'never' && this.direction !== 'vertical'; }, hasLabel() { return this.items.some(item => item.label.toString().length > 0); }, carouselClasses() { const classes = ['el-carousel', 'el-carousel--' + this.direction]; if (this.type === 'card') { classes.push('el-carousel--card'); } return classes; }, indicatorsClasses() { const classes = ['el-carousel__indicators', 'el-carousel__indicators--' + this.direction]; if (this.hasLabel) { classes.push('el-carousel__indicators--labels'); } if (this.indicatorPosition === 'outside' || this.type === 'card') { classes.push('el-carousel__indicators--outside'); } return classes; } }, watch: { items(val) { if (val.length > 0) this.setActiveItem(this.initialIndex); }, // 当前活动的index activeIndex(val, oldVal) { // 重置子项的位置 this.resetItemPosition(oldVal); // 向外抛出change事件,传递两个参数,当前索引页和前一页索引 this.$emit('change', val, oldVal); }, autoplay(val) { val ? this.startTimer() : this.pauseTimer(); }, loop() { this.setActiveItem(this.activeIndex); } }, methods: { handleMouseEnter() { this.hover = true; this.pauseTimer(); }, handleMouseLeave() { this.hover = false; this.startTimer(); }, itemInStage(item, index) { const length = this.items.length; // 满足当前为最后一个幻灯片;当前幻灯片在场景内;第一个幻灯片激活状态; // 或者 满足 当前幻灯片在场景内;当前幻灯片后面有至少一个项目;当前幻灯片后面一个项目处于激活状态 if (index === length - 1 && item.inStage && this.items[0].active || (item.inStage && this.items[index + 1] && this.items[index + 1].active)) { return 'left'; } else if (index === 0 && item.inStage && this.items[length - 1].active || (item.inStage && this.items[index - 1] && this.items[index - 1].active)) { return 'right'; } return false; }, // 鼠标放到左右按钮 handleButtonEnter(arrow) { if (this.direction === 'vertical') return; this.items.forEach((item, index) => { if (arrow === this.itemInStage(item, index)) { item.hover = true; } }); }, // 鼠标离开左右按钮 handleButtonLeave() { if (this.direction === 'vertical') return; this.items.forEach(item => { item.hover = false; }); }, // 初始化获取所有的字slider得到items updateItems() { this.items = this.$children.filter(child => child.$options.name === 'ElCarouselItem'); }, // 重置item的位置 resetItemPosition(oldIndex) { this.items.forEach((item, index) => { item.translateItem(index, this.activeIndex, oldIndex); }); }, // 自动滑动slide playSlides() { if (this.activeIndex < this.items.length - 1) { this.activeIndex++; } else if (this.loop) { this.activeIndex = 0; } }, // 清除定时器 pauseTimer() { if (this.timer) { clearInterval(this.timer); this.timer = null; } }, // 开启定时器 startTimer() { if (this.interval <= 0 || !this.autoplay || this.timer) return; this.timer = setInterval(this.playSlides, this.interval); }, /** // 假设为 el-carousel 设置了 ref="car" // setActiveItem 手动切换幻灯片 需要切换的幻灯片的索引,从 0 开始;或相应 el-carousel-item 的 name 属性值 // prev 切换至上一张幻灯片 // next 切换至下一张幻灯片 pre() { this.$refs.car.prev() }, next() { this.$refs.car.next() }, first() { this.$refs.car.setActiveItem(0) } *// setActiveItem(index) { // 如果索引是字符串,说明是指定名字的 if (typeof index === 'string') { const filteredItems = this.items.filter(item => item.name === index); if (filteredItems.length > 0) { // 如果找到的items长度大于0,取第一个的索引作为我们要使用的索引 index = this.items.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 = this.items.length; const oldIndex = this.activeIndex; if (index < 0) { this.activeIndex = this.loop ? length - 1 : 0; } else if (index >= length) { this.activeIndex = this.loop ? 0 : length - 1; } else { this.activeIndex = index; } if (oldIndex === this.activeIndex) { this.resetItemPosition(oldIndex); } }, prev() { this.setActiveItem(this.activeIndex - 1); }, next() { this.setActiveItem(this.activeIndex + 1); }, // 点击指示器 handleIndicatorClick(index) { this.activeIndex = index; }, // 如果触发方式是鼠标悬浮并且index不是当前索引 handleIndicatorHover(index) { if (this.trigger === 'hover' && index !== this.activeIndex) { this.activeIndex = index; } } }, created() { // 监听左右按钮点击事件 this.throttledArrowClick = throttle(300, true, index => { this.setActiveItem(index); }); // 监听指示器hover事件,节流触发 this.throttledIndicatorHover = throttle(300, index => { this.handleIndicatorHover(index); }); }, mounted() { // 初始化items this.updateItems(); this.$nextTick(() => { // 增加resize事件的回调为resetItemPosition addResizeListener(this.$el, this.resetItemPosition); if (this.initialIndex < this.items.length && this.initialIndex >= 0) { // 如果初始化的索引有效,则将当前页设置为初始的索引 this.activeIndex = this.initialIndex; } this.startTimer(); }); }, beforeDestroy() { if (this.$el) removeResizeListener(this.$el, this.resetItemPosition); this.pauseTimer(); } }; </script>
src/item.vue
<template> <div v-show="ready" class="el-carousel__item" :class="{ 'is-active': active, 'el-carousel__item--card': $parent.type === 'card', 'is-in-stage': inStage, 'is-hover': hover, 'is-animating': animating }" @click="handleItemClick" :style="itemStyle"> <div v-if="$parent.type === 'card'" v-show="!active" class="el-carousel__mask"> </div> <slot></slot> </div> </template> <script> import { autoprefixer } from 'element-ui/src/utils/util'; const CARD_SCALE = 0.83; export default { name: 'ElCarouselItem', props: { name: String, label: { type: [String, Number], default: '' } }, data() { return { hover: false, translate: 0, scale: 1, active: false, ready: false, inStage: false, animating: false }; }, methods: { processIndex(index, activeIndex, length) { if (activeIndex === 0 && index === length - 1) { return -1;// 活跃页是第一页,当前页是最后一页,返回-1,这样相差为1,表示二者相邻且在左侧 } else if (activeIndex === length - 1 && index === 0) { return length;// 活跃页最后一页,当前页是第一页,返回总页数,这样相差也在1以内 } else if (index < activeIndex - 1 && activeIndex - index >= length / 2) { return length + 1;// 如果,当前页在活跃页前一页的前面,并且之间的间隔在一半页数即以上,则返回页数长度+1,这样它们会被置于最右侧 } else if (index > activeIndex + 1 && index - activeIndex >= length / 2) { return -2; // 如果,当前页在活跃页后一页的后面,并且之间的间隔在一般页数即以上,则返回-2,这样它们会被置于最左侧 } return index;// 其他的返回原值 }, // 计算三张卡片位置的方法 calcCardTranslate(index, activeIndex) { const parentWidth = this.$parent.$el.offsetWidth; if (this.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; } }, calcTranslate(index, activeIndex, isVertical) { const distance = this.$parent.$el[isVertical ? 'offsetHeight' : 'offsetWidth']; return distance * (index - activeIndex); }, translateItem(index, activeIndex, oldIndex) { const parentType = this.$parent.type; const parentDirection = this.parentDirection; const length = this.$parent.items.length; // 判断是否需要过渡动画 class if (parentType !== 'card' && oldIndex !== undefined) { this.animating = index === activeIndex || index === oldIndex; } // 处理 index if (index !== activeIndex && length > 2 && this.$parent.loop) { index = this.processIndex(index, activeIndex, length); } if (parentType === 'card') { if (parentDirection === 'vertical') { console.warn('[Element Warn][Carousel]vertical directionis not supported in card mode'); } this.inStage = Math.round(Math.abs(index - activeIndex)) <= 1; this.active = index === activeIndex; // 计算卡片化偏移量 this.translate = this.calcCardTranslate(index, activeIndex); // 激活卡片不缩放,其他卡片缩放为 0.83 this.scale = this.active ? 1 : CARD_SCALE; } else { // 非卡片化 this.active = index === activeIndex; // 激活class const isVertical = parentDirection === 'vertical'; this.translate = this.calcTranslate(index, activeIndex, isVertical); // 计算位移 根据父组件宽度计算 } // 计算完后显示 this.ready = true; }, handleItemClick() { const parent = this.$parent; if (parent && parent.type === 'card') { const index = parent.items.indexOf(this); parent.setActiveItem(index); } } }, computed: { parentDirection() { return this.$parent.direction; }, itemStyle() { const translateType = this.parentDirection === 'vertical' ? 'translateY' : 'translateX'; const value = `${translateType}(${ this.translate }px) scale(${ this.scale })`; const style = { transform: value }; return autoprefixer(style); } }, created() { this.$parent && this.$parent.updateItems(); }, destroyed() { this.$parent && this.$parent.updateItems(); } }; </script>