mirror of
https://github.com/element-plus/element-plus.git
synced 2024-11-27 02:01:15 +08:00
refactor(components): [carousel] (#10188)
Co-authored-by: JeremyWuuuuu <15975785+JeremyWuuuuu@users.noreply.github.com>
This commit is contained in:
parent
9285109a0c
commit
904605587f
@ -5,7 +5,7 @@ import Carousel from '../src/carousel.vue'
|
||||
import CarouselItem from '../src/carousel-item.vue'
|
||||
|
||||
import type { VueWrapper } from '@vue/test-utils'
|
||||
import type { CarouselInstance } from '../src/carousel'
|
||||
import type { CarouselInstance } from '../src/instance'
|
||||
|
||||
const wait = (ms = 100) =>
|
||||
new Promise((resolve) => setTimeout(() => resolve(0), ms))
|
||||
|
@ -12,3 +12,5 @@ export const ElCarouselItem = withNoopInstall(CarouselItem)
|
||||
|
||||
export * from './src/carousel'
|
||||
export * from './src/carousel-item'
|
||||
|
||||
export type { CarouselInstance } from './src/instance'
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { buildProps, isNumber } from '@element-plus/utils'
|
||||
import type { ExtractPropTypes } from 'vue'
|
||||
import type Carousel from './carousel.vue'
|
||||
|
||||
export const carouselProps = buildProps({
|
||||
initialIndex: {
|
||||
@ -64,5 +63,3 @@ export const carouselEmits = {
|
||||
|
||||
export type CarouselProps = ExtractPropTypes<typeof carouselProps>
|
||||
export type CarouselEmits = typeof carouselEmits
|
||||
|
||||
export type CarouselInstance = InstanceType<typeof Carousel>
|
||||
|
@ -62,60 +62,39 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
computed,
|
||||
getCurrentInstance,
|
||||
onBeforeUnmount,
|
||||
onMounted,
|
||||
provide,
|
||||
ref,
|
||||
shallowRef,
|
||||
unref,
|
||||
watch,
|
||||
} from 'vue'
|
||||
import { throttle } from 'lodash-unified'
|
||||
import { useResizeObserver } from '@vueuse/core'
|
||||
import { debugWarn, isString } from '@element-plus/utils'
|
||||
import { computed, unref } from 'vue'
|
||||
import { ElIcon } from '@element-plus/components/icon'
|
||||
import { ArrowLeft, ArrowRight } from '@element-plus/icons-vue'
|
||||
import { useNamespace, useOrderedChildren } from '@element-plus/hooks'
|
||||
import { carouselContextKey } from '@element-plus/tokens'
|
||||
import { useNamespace } from '@element-plus/hooks'
|
||||
import { carouselEmits, carouselProps } from './carousel'
|
||||
import type { CarouselItemContext } from '@element-plus/tokens'
|
||||
import { useCarousel } from './use-carousel'
|
||||
|
||||
const COMPONENT_NAME = 'ElCarousel'
|
||||
defineOptions({
|
||||
name: 'ElCarousel',
|
||||
name: COMPONENT_NAME,
|
||||
})
|
||||
|
||||
const props = defineProps(carouselProps)
|
||||
const emit = defineEmits(carouselEmits)
|
||||
const ns = useNamespace('carousel')
|
||||
const COMPONENT_NAME = 'ElCarousel'
|
||||
const THROTTLE_TIME = 300
|
||||
|
||||
const {
|
||||
children: items,
|
||||
addChild: addItem,
|
||||
removeChild: removeItem,
|
||||
} = useOrderedChildren<CarouselItemContext>(
|
||||
getCurrentInstance()!,
|
||||
'ElCarouselItem'
|
||||
)
|
||||
|
||||
// refs
|
||||
const activeIndex = ref(-1)
|
||||
const timer = ref<ReturnType<typeof setInterval> | null>(null)
|
||||
const hover = ref(false)
|
||||
const root = ref<HTMLDivElement>()
|
||||
|
||||
// computed
|
||||
const arrowDisplay = computed(
|
||||
() => props.arrow !== 'never' && !unref(isVertical)
|
||||
)
|
||||
|
||||
const hasLabel = computed(() => {
|
||||
return items.value.some((item) => item.props.label.toString().length > 0)
|
||||
})
|
||||
activeIndex,
|
||||
arrowDisplay,
|
||||
hasLabel,
|
||||
hover,
|
||||
isCardType,
|
||||
items,
|
||||
handleButtonEnter,
|
||||
handleButtonLeave,
|
||||
handleIndicatorClick,
|
||||
handleMouseEnter,
|
||||
handleMouseLeave,
|
||||
setActiveItem,
|
||||
prev,
|
||||
next,
|
||||
throttledArrowClick,
|
||||
throttledIndicatorHover,
|
||||
} = useCarousel(props, emit, COMPONENT_NAME)
|
||||
const ns = useNamespace('carousel')
|
||||
|
||||
const carouselClasses = computed(() => {
|
||||
const classes = [ns.b(), ns.m(props.direction)]
|
||||
@ -127,7 +106,7 @@ const carouselClasses = computed(() => {
|
||||
|
||||
const indicatorsClasses = computed(() => {
|
||||
const classes = [ns.e('indicators'), ns.em('indicators', props.direction)]
|
||||
if (hasLabel.value) {
|
||||
if (unref(hasLabel)) {
|
||||
classes.push(ns.em('indicators', 'labels'))
|
||||
}
|
||||
if (props.indicatorPosition === 'outside' || unref(isCardType)) {
|
||||
@ -136,211 +115,6 @@ const indicatorsClasses = computed(() => {
|
||||
return classes
|
||||
})
|
||||
|
||||
const isCardType = computed(() => props.type === 'card')
|
||||
const isVertical = computed(() => props.direction === 'vertical')
|
||||
|
||||
// methods
|
||||
const throttledArrowClick = throttle(
|
||||
(index: number) => {
|
||||
setActiveItem(index)
|
||||
},
|
||||
THROTTLE_TIME,
|
||||
{ trailing: true }
|
||||
)
|
||||
|
||||
const throttledIndicatorHover = throttle((index: number) => {
|
||||
handleIndicatorHover(index)
|
||||
}, THROTTLE_TIME)
|
||||
|
||||
function pauseTimer() {
|
||||
if (timer.value) {
|
||||
clearInterval(timer.value)
|
||||
timer.value = null
|
||||
}
|
||||
}
|
||||
|
||||
function startTimer() {
|
||||
if (props.interval <= 0 || !props.autoplay || timer.value) return
|
||||
timer.value = setInterval(() => playSlides(), props.interval)
|
||||
}
|
||||
|
||||
const playSlides = () => {
|
||||
if (activeIndex.value < items.value.length - 1) {
|
||||
activeIndex.value = activeIndex.value + 1
|
||||
} else if (props.loop) {
|
||||
activeIndex.value = 0
|
||||
}
|
||||
}
|
||||
|
||||
function setActiveItem(index: number | string) {
|
||||
if (isString(index)) {
|
||||
const filteredItems = items.value.filter(
|
||||
(item) => item.props.name === index
|
||||
)
|
||||
if (filteredItems.length > 0) {
|
||||
index = items.value.indexOf(filteredItems[0])
|
||||
}
|
||||
}
|
||||
index = Number(index)
|
||||
if (Number.isNaN(index) || index !== Math.floor(index)) {
|
||||
debugWarn(COMPONENT_NAME, 'index must be integer.')
|
||||
return
|
||||
}
|
||||
const itemCount = items.value.length
|
||||
const oldIndex = activeIndex.value
|
||||
if (index < 0) {
|
||||
activeIndex.value = props.loop ? itemCount - 1 : 0
|
||||
} else if (index >= itemCount) {
|
||||
activeIndex.value = props.loop ? 0 : itemCount - 1
|
||||
} else {
|
||||
activeIndex.value = index
|
||||
}
|
||||
if (oldIndex === activeIndex.value) {
|
||||
resetItemPosition(oldIndex)
|
||||
}
|
||||
resetTimer()
|
||||
}
|
||||
|
||||
function resetItemPosition(oldIndex?: number) {
|
||||
items.value.forEach((item, index) => {
|
||||
item.translateItem(index, activeIndex.value, oldIndex)
|
||||
})
|
||||
}
|
||||
|
||||
function itemInStage(item: CarouselItemContext, index: number) {
|
||||
const _items = unref(items)
|
||||
const itemCount = _items.length
|
||||
if (itemCount === 0 || !item.states.inStage) return false
|
||||
const nextItemIndex = index + 1
|
||||
const prevItemIndex = index - 1
|
||||
const lastItemIndex = itemCount - 1
|
||||
const isLastItemActive = _items[lastItemIndex].states.active
|
||||
const isFirstItemActive = _items[0].states.active
|
||||
const isNextItemActive = _items[nextItemIndex]?.states?.active
|
||||
const isPrevItemActive = _items[prevItemIndex]?.states?.active
|
||||
|
||||
if ((index === lastItemIndex && isFirstItemActive) || isNextItemActive) {
|
||||
return 'left'
|
||||
} else if ((index === 0 && isLastItemActive) || isPrevItemActive) {
|
||||
return 'right'
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function handleMouseEnter() {
|
||||
hover.value = true
|
||||
if (props.pauseOnHover) {
|
||||
pauseTimer()
|
||||
}
|
||||
}
|
||||
|
||||
function handleMouseLeave() {
|
||||
hover.value = false
|
||||
startTimer()
|
||||
}
|
||||
|
||||
function handleButtonEnter(arrow: 'left' | 'right') {
|
||||
if (unref(isVertical)) return
|
||||
items.value.forEach((item, index) => {
|
||||
if (arrow === itemInStage(item, index)) {
|
||||
item.states.hover = true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function handleButtonLeave() {
|
||||
if (unref(isVertical)) return
|
||||
items.value.forEach((item) => {
|
||||
item.states.hover = false
|
||||
})
|
||||
}
|
||||
|
||||
function handleIndicatorClick(index: number) {
|
||||
activeIndex.value = index
|
||||
}
|
||||
|
||||
function handleIndicatorHover(index: number) {
|
||||
if (props.trigger === 'hover' && index !== activeIndex.value) {
|
||||
activeIndex.value = index
|
||||
}
|
||||
}
|
||||
|
||||
function prev() {
|
||||
setActiveItem(activeIndex.value - 1)
|
||||
}
|
||||
|
||||
function next() {
|
||||
setActiveItem(activeIndex.value + 1)
|
||||
}
|
||||
|
||||
function resetTimer() {
|
||||
pauseTimer()
|
||||
startTimer()
|
||||
}
|
||||
|
||||
// watch
|
||||
watch(
|
||||
() => activeIndex.value,
|
||||
(current, prev) => {
|
||||
resetItemPosition(prev)
|
||||
if (prev > -1) {
|
||||
emit('change', current, prev)
|
||||
}
|
||||
}
|
||||
)
|
||||
watch(
|
||||
() => props.autoplay,
|
||||
(autoplay) => {
|
||||
autoplay ? startTimer() : pauseTimer()
|
||||
}
|
||||
)
|
||||
watch(
|
||||
() => props.loop,
|
||||
() => {
|
||||
setActiveItem(activeIndex.value)
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.interval,
|
||||
() => {
|
||||
resetTimer()
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => items.value,
|
||||
() => {
|
||||
if (items.value.length > 0) setActiveItem(props.initialIndex)
|
||||
}
|
||||
)
|
||||
|
||||
const resizeObserver = shallowRef<ReturnType<typeof useResizeObserver>>()
|
||||
// lifecycle
|
||||
onMounted(() => {
|
||||
resizeObserver.value = useResizeObserver(root.value, () => {
|
||||
resetItemPosition()
|
||||
})
|
||||
startTimer()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
pauseTimer()
|
||||
if (root.value && resizeObserver.value) resizeObserver.value.stop()
|
||||
})
|
||||
|
||||
// provide
|
||||
provide(carouselContextKey, {
|
||||
root,
|
||||
isCardType,
|
||||
isVertical,
|
||||
items,
|
||||
loop: props.loop,
|
||||
addItem,
|
||||
removeItem,
|
||||
setActiveItem,
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
/** @description manually switch slide */
|
||||
setActiveItem,
|
||||
|
3
packages/components/carousel/src/instance.ts
Normal file
3
packages/components/carousel/src/instance.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import type Carousel from './carousel.vue'
|
||||
|
||||
export type CarouselInstance = InstanceType<typeof Carousel>
|
276
packages/components/carousel/src/use-carousel.ts
Normal file
276
packages/components/carousel/src/use-carousel.ts
Normal file
@ -0,0 +1,276 @@
|
||||
import {
|
||||
computed,
|
||||
getCurrentInstance,
|
||||
onBeforeUnmount,
|
||||
onMounted,
|
||||
provide,
|
||||
ref,
|
||||
shallowRef,
|
||||
unref,
|
||||
watch,
|
||||
} from 'vue'
|
||||
import { throttle } from 'lodash-unified'
|
||||
import { useResizeObserver } from '@vueuse/core'
|
||||
import { debugWarn, isString } from '@element-plus/utils'
|
||||
import { carouselContextKey } from '@element-plus/tokens'
|
||||
import { useOrderedChildren } from '@element-plus/hooks'
|
||||
|
||||
import type { SetupContext } from 'vue'
|
||||
import type { CarouselItemContext } from '@element-plus/tokens'
|
||||
import type { CarouselEmits, CarouselProps } from './carousel'
|
||||
|
||||
const THROTTLE_TIME = 300
|
||||
|
||||
export const useCarousel = (
|
||||
props: CarouselProps,
|
||||
emit: SetupContext<CarouselEmits>['emit'],
|
||||
componentName: string
|
||||
) => {
|
||||
const {
|
||||
children: items,
|
||||
addChild: addItem,
|
||||
removeChild: removeItem,
|
||||
} = useOrderedChildren<CarouselItemContext>(
|
||||
getCurrentInstance()!,
|
||||
'ElCarouselItem'
|
||||
)
|
||||
|
||||
// refs
|
||||
const activeIndex = ref(-1)
|
||||
const timer = ref<ReturnType<typeof setInterval> | null>(null)
|
||||
const hover = ref(false)
|
||||
const root = ref<HTMLDivElement>()
|
||||
|
||||
// computed
|
||||
const arrowDisplay = computed(
|
||||
() => props.arrow !== 'never' && !unref(isVertical)
|
||||
)
|
||||
|
||||
const hasLabel = computed(() => {
|
||||
return items.value.some((item) => item.props.label.toString().length > 0)
|
||||
})
|
||||
|
||||
const isCardType = computed(() => props.type === 'card')
|
||||
const isVertical = computed(() => props.direction === 'vertical')
|
||||
|
||||
// methods
|
||||
const throttledArrowClick = throttle(
|
||||
(index: number) => {
|
||||
setActiveItem(index)
|
||||
},
|
||||
THROTTLE_TIME,
|
||||
{ trailing: true }
|
||||
)
|
||||
|
||||
const throttledIndicatorHover = throttle((index: number) => {
|
||||
handleIndicatorHover(index)
|
||||
}, THROTTLE_TIME)
|
||||
|
||||
function pauseTimer() {
|
||||
if (timer.value) {
|
||||
clearInterval(timer.value)
|
||||
timer.value = null
|
||||
}
|
||||
}
|
||||
|
||||
function startTimer() {
|
||||
if (props.interval <= 0 || !props.autoplay || timer.value) return
|
||||
timer.value = setInterval(() => playSlides(), props.interval)
|
||||
}
|
||||
|
||||
const playSlides = () => {
|
||||
if (activeIndex.value < items.value.length - 1) {
|
||||
activeIndex.value = activeIndex.value + 1
|
||||
} else if (props.loop) {
|
||||
activeIndex.value = 0
|
||||
}
|
||||
}
|
||||
|
||||
function setActiveItem(index: number | string) {
|
||||
if (isString(index)) {
|
||||
const filteredItems = items.value.filter(
|
||||
(item) => item.props.name === index
|
||||
)
|
||||
if (filteredItems.length > 0) {
|
||||
index = items.value.indexOf(filteredItems[0])
|
||||
}
|
||||
}
|
||||
index = Number(index)
|
||||
if (Number.isNaN(index) || index !== Math.floor(index)) {
|
||||
debugWarn(componentName, 'index must be integer.')
|
||||
return
|
||||
}
|
||||
const itemCount = items.value.length
|
||||
const oldIndex = activeIndex.value
|
||||
if (index < 0) {
|
||||
activeIndex.value = props.loop ? itemCount - 1 : 0
|
||||
} else if (index >= itemCount) {
|
||||
activeIndex.value = props.loop ? 0 : itemCount - 1
|
||||
} else {
|
||||
activeIndex.value = index
|
||||
}
|
||||
if (oldIndex === activeIndex.value) {
|
||||
resetItemPosition(oldIndex)
|
||||
}
|
||||
resetTimer()
|
||||
}
|
||||
|
||||
function resetItemPosition(oldIndex?: number) {
|
||||
items.value.forEach((item, index) => {
|
||||
item.translateItem(index, activeIndex.value, oldIndex)
|
||||
})
|
||||
}
|
||||
|
||||
function itemInStage(item: CarouselItemContext, index: number) {
|
||||
const _items = unref(items)
|
||||
const itemCount = _items.length
|
||||
if (itemCount === 0 || !item.states.inStage) return false
|
||||
const nextItemIndex = index + 1
|
||||
const prevItemIndex = index - 1
|
||||
const lastItemIndex = itemCount - 1
|
||||
const isLastItemActive = _items[lastItemIndex].states.active
|
||||
const isFirstItemActive = _items[0].states.active
|
||||
const isNextItemActive = _items[nextItemIndex]?.states?.active
|
||||
const isPrevItemActive = _items[prevItemIndex]?.states?.active
|
||||
|
||||
if ((index === lastItemIndex && isFirstItemActive) || isNextItemActive) {
|
||||
return 'left'
|
||||
} else if ((index === 0 && isLastItemActive) || isPrevItemActive) {
|
||||
return 'right'
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function handleMouseEnter() {
|
||||
hover.value = true
|
||||
if (props.pauseOnHover) {
|
||||
pauseTimer()
|
||||
}
|
||||
}
|
||||
|
||||
function handleMouseLeave() {
|
||||
hover.value = false
|
||||
startTimer()
|
||||
}
|
||||
|
||||
function handleButtonEnter(arrow: 'left' | 'right') {
|
||||
if (unref(isVertical)) return
|
||||
items.value.forEach((item, index) => {
|
||||
if (arrow === itemInStage(item, index)) {
|
||||
item.states.hover = true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function handleButtonLeave() {
|
||||
if (unref(isVertical)) return
|
||||
items.value.forEach((item) => {
|
||||
item.states.hover = false
|
||||
})
|
||||
}
|
||||
|
||||
function handleIndicatorClick(index: number) {
|
||||
activeIndex.value = index
|
||||
}
|
||||
|
||||
function handleIndicatorHover(index: number) {
|
||||
if (props.trigger === 'hover' && index !== activeIndex.value) {
|
||||
activeIndex.value = index
|
||||
}
|
||||
}
|
||||
|
||||
function prev() {
|
||||
setActiveItem(activeIndex.value - 1)
|
||||
}
|
||||
|
||||
function next() {
|
||||
setActiveItem(activeIndex.value + 1)
|
||||
}
|
||||
|
||||
function resetTimer() {
|
||||
pauseTimer()
|
||||
startTimer()
|
||||
}
|
||||
|
||||
// watch
|
||||
watch(
|
||||
() => activeIndex.value,
|
||||
(current, prev) => {
|
||||
resetItemPosition(prev)
|
||||
if (prev > -1) {
|
||||
emit('change', current, prev)
|
||||
}
|
||||
}
|
||||
)
|
||||
watch(
|
||||
() => props.autoplay,
|
||||
(autoplay) => {
|
||||
autoplay ? startTimer() : pauseTimer()
|
||||
}
|
||||
)
|
||||
watch(
|
||||
() => props.loop,
|
||||
() => {
|
||||
setActiveItem(activeIndex.value)
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.interval,
|
||||
() => {
|
||||
resetTimer()
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => items.value,
|
||||
() => {
|
||||
if (items.value.length > 0) setActiveItem(props.initialIndex)
|
||||
}
|
||||
)
|
||||
|
||||
const resizeObserver = shallowRef<ReturnType<typeof useResizeObserver>>()
|
||||
// lifecycle
|
||||
onMounted(() => {
|
||||
resizeObserver.value = useResizeObserver(root.value, () => {
|
||||
resetItemPosition()
|
||||
})
|
||||
startTimer()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
pauseTimer()
|
||||
if (root.value && resizeObserver.value) resizeObserver.value.stop()
|
||||
})
|
||||
|
||||
// provide
|
||||
provide(carouselContextKey, {
|
||||
root,
|
||||
isCardType,
|
||||
isVertical,
|
||||
items,
|
||||
loop: props.loop,
|
||||
addItem,
|
||||
removeItem,
|
||||
setActiveItem,
|
||||
})
|
||||
|
||||
return {
|
||||
activeIndex,
|
||||
arrowDisplay,
|
||||
hasLabel,
|
||||
hover,
|
||||
isCardType,
|
||||
items,
|
||||
handleButtonEnter,
|
||||
handleButtonLeave,
|
||||
handleIndicatorClick,
|
||||
handleMouseEnter,
|
||||
handleMouseLeave,
|
||||
setActiveItem,
|
||||
prev,
|
||||
next,
|
||||
throttledArrowClick,
|
||||
throttledIndicatorHover,
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user