mirror of
https://github.com/element-plus/element-plus.git
synced 2026-03-13 07:51:17 +08:00
417 lines
9.8 KiB
Vue
417 lines
9.8 KiB
Vue
<template>
|
|
<div
|
|
ref="root"
|
|
:class="carouselClasses"
|
|
@mouseenter.stop="handleMouseEnter"
|
|
@mouseleave.stop="handleMouseLeave"
|
|
>
|
|
<div :class="ns.e('container')" :style="{ height: height }">
|
|
<transition v-if="arrowDisplay" name="carousel-arrow-left">
|
|
<button
|
|
v-show="
|
|
(arrow === 'always' || data.hover) &&
|
|
(props.loop || data.activeIndex > 0)
|
|
"
|
|
type="button"
|
|
:class="[ns.e('arrow'), ns.em('arrow', 'left')]"
|
|
@mouseenter="handleButtonEnter('left')"
|
|
@mouseleave="handleButtonLeave"
|
|
@click.stop="throttledArrowClick(data.activeIndex - 1)"
|
|
>
|
|
<el-icon>
|
|
<arrow-left />
|
|
</el-icon>
|
|
</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="[ns.e('arrow'), ns.em('arrow', 'right')]"
|
|
@mouseenter="handleButtonEnter('right')"
|
|
@mouseleave="handleButtonLeave"
|
|
@click.stop="throttledArrowClick(data.activeIndex + 1)"
|
|
>
|
|
<el-icon>
|
|
<arrow-right />
|
|
</el-icon>
|
|
</button>
|
|
</transition>
|
|
<slot></slot>
|
|
</div>
|
|
<ul v-if="indicatorPosition !== 'none'" :class="indicatorsClasses">
|
|
<li
|
|
v-for="(item, index) in items"
|
|
:key="index"
|
|
:class="[
|
|
ns.e('indicator'),
|
|
ns.em('indicator', direction),
|
|
ns.is('active', index === data.activeIndex),
|
|
]"
|
|
@mouseenter="throttledIndicatorHover(index)"
|
|
@click.stop="handleIndicatorClick(index)"
|
|
>
|
|
<button :class="ns.e('button')">
|
|
<span v-if="hasLabel">{{ item.label }}</span>
|
|
</button>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</template>
|
|
|
|
<script lang="ts">
|
|
import {
|
|
defineComponent,
|
|
reactive,
|
|
computed,
|
|
ref,
|
|
provide,
|
|
onMounted,
|
|
onBeforeUnmount,
|
|
watch,
|
|
nextTick,
|
|
} from 'vue'
|
|
import { throttle } from 'lodash-unified'
|
|
import {
|
|
addResizeListener,
|
|
removeResizeListener,
|
|
} from '@element-plus/utils/resize-event'
|
|
import { ElIcon } from '@element-plus/components/icon'
|
|
import { ArrowLeft, ArrowRight } from '@element-plus/icons-vue'
|
|
import { useNamespace } from '@element-plus/hooks'
|
|
|
|
import { debugWarn } from '@element-plus/utils-v2'
|
|
import type {
|
|
ICarouselProps,
|
|
CarouselItem,
|
|
InjectCarouselScope,
|
|
} from './carousel'
|
|
|
|
export default defineComponent({
|
|
name: 'ElCarousel',
|
|
components: {
|
|
ElIcon,
|
|
ArrowLeft,
|
|
ArrowRight,
|
|
},
|
|
props: {
|
|
initialIndex: {
|
|
type: Number,
|
|
default: 0,
|
|
},
|
|
height: { type: String, default: '' },
|
|
trigger: {
|
|
type: String,
|
|
default: 'hover',
|
|
},
|
|
autoplay: {
|
|
type: Boolean,
|
|
default: true,
|
|
},
|
|
interval: {
|
|
type: Number,
|
|
default: 3000,
|
|
},
|
|
indicatorPosition: { type: String, default: '' },
|
|
indicator: {
|
|
type: Boolean,
|
|
default: true,
|
|
},
|
|
arrow: {
|
|
type: String,
|
|
default: 'hover',
|
|
},
|
|
type: { type: String, default: '' },
|
|
loop: {
|
|
type: Boolean,
|
|
default: true,
|
|
},
|
|
direction: {
|
|
type: String,
|
|
default: 'horizontal',
|
|
validator(val: string) {
|
|
return ['horizontal', 'vertical'].includes(val)
|
|
},
|
|
},
|
|
pauseOnHover: {
|
|
type: Boolean,
|
|
default: true,
|
|
},
|
|
},
|
|
emits: ['change'],
|
|
setup(props: ICarouselProps, { emit }) {
|
|
const ns = useNamespace('carousel')
|
|
// 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)
|
|
const items = ref<CarouselItem[]>([])
|
|
|
|
// computed
|
|
const arrowDisplay = computed(
|
|
() => props.arrow !== 'never' && props.direction !== 'vertical'
|
|
)
|
|
|
|
const hasLabel = computed(() => {
|
|
return items.value.some((item) => item.label.toString().length > 0)
|
|
})
|
|
|
|
const carouselClasses = computed(() => {
|
|
const classes = [ns.b(), ns.m(props.direction)]
|
|
if (props.type === 'card') {
|
|
classes.push(ns.m('card'))
|
|
}
|
|
return classes
|
|
})
|
|
|
|
const indicatorsClasses = computed(() => {
|
|
const classes = [ns.e('indicators'), ns.em('indicators', props.direction)]
|
|
if (hasLabel.value) {
|
|
classes.push(ns.em('indicators', 'labels'))
|
|
}
|
|
if (props.indicatorPosition === 'outside' || props.type === 'card') {
|
|
classes.push(ns.em('indicators', 'outside'))
|
|
}
|
|
return classes
|
|
})
|
|
|
|
// methods
|
|
const throttledArrowClick = throttle(
|
|
(index) => {
|
|
setActiveItem(index)
|
|
},
|
|
300,
|
|
{ trailing: true }
|
|
)
|
|
|
|
const throttledIndicatorHover = throttle((index) => {
|
|
handleIndicatorHover(index)
|
|
}, 300)
|
|
|
|
function pauseTimer() {
|
|
if (data.timer) {
|
|
clearInterval(data.timer)
|
|
data.timer = null
|
|
}
|
|
}
|
|
|
|
function startTimer() {
|
|
if (props.interval <= 0 || !props.autoplay || data.timer) return
|
|
data.timer = setInterval(() => playSlides(), props.interval)
|
|
}
|
|
|
|
const playSlides = () => {
|
|
if (data.activeIndex < items.value.length - 1) {
|
|
data.activeIndex = data.activeIndex + 1
|
|
} else if (props.loop) {
|
|
data.activeIndex = 0
|
|
}
|
|
}
|
|
|
|
function setActiveItem(index) {
|
|
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)) {
|
|
debugWarn('Carousel', 'index must be an integer.')
|
|
return
|
|
}
|
|
const 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) {
|
|
resetItemPosition(oldIndex)
|
|
}
|
|
}
|
|
|
|
function resetItemPosition(oldIndex) {
|
|
items.value.forEach((item, index) => {
|
|
item.translateItem(index, data.activeIndex, oldIndex)
|
|
})
|
|
}
|
|
|
|
function addItem(item) {
|
|
items.value.push(item)
|
|
}
|
|
|
|
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()
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
function handleMouseEnter() {
|
|
data.hover = true
|
|
if (props.pauseOnHover) {
|
|
pauseTimer()
|
|
}
|
|
}
|
|
|
|
function handleMouseLeave() {
|
|
data.hover = false
|
|
startTimer()
|
|
}
|
|
|
|
function handleButtonEnter(arrow) {
|
|
if (props.direction === 'vertical') return
|
|
items.value.forEach((item, index) => {
|
|
if (arrow === itemInStage(item, index)) {
|
|
item.hover = true
|
|
}
|
|
})
|
|
}
|
|
|
|
function handleButtonLeave() {
|
|
if (props.direction === 'vertical') return
|
|
items.value.forEach((item) => {
|
|
item.hover = false
|
|
})
|
|
}
|
|
|
|
function handleIndicatorClick(index) {
|
|
data.activeIndex = index
|
|
}
|
|
|
|
function handleIndicatorHover(index) {
|
|
if (props.trigger === 'hover' && index !== data.activeIndex) {
|
|
data.activeIndex = index
|
|
}
|
|
}
|
|
|
|
function prev() {
|
|
setActiveItem(data.activeIndex - 1)
|
|
}
|
|
|
|
function next() {
|
|
setActiveItem(data.activeIndex + 1)
|
|
}
|
|
|
|
// watch
|
|
watch(
|
|
() => data.activeIndex,
|
|
(current, prev) => {
|
|
resetItemPosition(prev)
|
|
if (prev > -1) {
|
|
emit('change', current, prev)
|
|
}
|
|
}
|
|
)
|
|
watch(
|
|
() => props.autoplay,
|
|
(current) => {
|
|
current ? startTimer() : pauseTimer()
|
|
}
|
|
)
|
|
watch(
|
|
() => props.loop,
|
|
() => {
|
|
setActiveItem(data.activeIndex)
|
|
}
|
|
)
|
|
|
|
// lifecycle
|
|
onMounted(() => {
|
|
nextTick(() => {
|
|
addResizeListener(root.value, resetItemPosition)
|
|
if (
|
|
props.initialIndex < items.value.length &&
|
|
props.initialIndex >= 0
|
|
) {
|
|
data.activeIndex = props.initialIndex
|
|
}
|
|
startTimer()
|
|
})
|
|
})
|
|
|
|
onBeforeUnmount(() => {
|
|
if (root.value) removeResizeListener(root.value, resetItemPosition)
|
|
pauseTimer()
|
|
})
|
|
|
|
// provide
|
|
provide<InjectCarouselScope>('injectCarouselScope', {
|
|
root,
|
|
direction: props.direction,
|
|
type: props.type,
|
|
items,
|
|
loop: props.loop,
|
|
addItem,
|
|
removeItem,
|
|
setActiveItem,
|
|
})
|
|
|
|
return {
|
|
data,
|
|
props,
|
|
items,
|
|
|
|
arrowDisplay,
|
|
carouselClasses,
|
|
indicatorsClasses,
|
|
hasLabel,
|
|
|
|
handleMouseEnter,
|
|
handleMouseLeave,
|
|
handleIndicatorClick,
|
|
throttledArrowClick,
|
|
throttledIndicatorHover,
|
|
handleButtonEnter,
|
|
handleButtonLeave,
|
|
|
|
prev,
|
|
next,
|
|
setActiveItem,
|
|
|
|
root,
|
|
ns,
|
|
}
|
|
},
|
|
})
|
|
</script>
|