mirror of
https://github.com/element-plus/element-plus.git
synced 2026-03-13 07:51:17 +08:00
refactor(components): [carousel] refactor (#6681)
* refactor(components): [carousel] refactor * fix: build error
This commit is contained in:
@@ -12,6 +12,7 @@
|
||||
"packages/components/button/",
|
||||
"packages/components/button-group/",
|
||||
"packages/components/card/",
|
||||
"packages/components/carousel/",
|
||||
"packages/components/check-tag/",
|
||||
"packages/components/col/",
|
||||
"packages/components/collapse/",
|
||||
|
||||
@@ -1,46 +1,41 @@
|
||||
import { nextTick } from 'vue'
|
||||
import { nextTick, reactive } from 'vue'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import Carousel from '../src/main.vue'
|
||||
import CarouselItem from '../src/item.vue'
|
||||
import Carousel from '../src/carousel.vue'
|
||||
import CarouselItem from '../src/carousel-item.vue'
|
||||
import type { CarouselInstance } from '../src/carousel'
|
||||
|
||||
const wait = (ms = 100) =>
|
||||
new Promise((resolve) => setTimeout(() => resolve(0), ms))
|
||||
|
||||
const _mount = (template: string, data?: () => void, methods?: any) =>
|
||||
mount({
|
||||
components: {
|
||||
'el-carousel': Carousel,
|
||||
'el-carousel-item': CarouselItem,
|
||||
},
|
||||
template,
|
||||
data,
|
||||
methods,
|
||||
})
|
||||
const generateCarouselItems = (count = 3) => {
|
||||
const list = Array.from({ length: count }, (_, index) => index + 1)
|
||||
return list.map((i) => <CarouselItem key={i}></CarouselItem>)
|
||||
}
|
||||
|
||||
describe('Carousel', () => {
|
||||
it('create', () => {
|
||||
const wrapper = _mount(
|
||||
`
|
||||
<div>
|
||||
<el-carousel ref="carousel">
|
||||
<el-carousel-item v-for="item in 3" :key="item"></el-carousel-item>
|
||||
</el-carousel>
|
||||
</div>
|
||||
`
|
||||
)
|
||||
const wrapper = mount({
|
||||
setup() {
|
||||
return () => (
|
||||
<div>
|
||||
<Carousel ref="carousel">{generateCarouselItems()}</Carousel>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.$refs.carousel.direction).toBe('horizontal')
|
||||
const carousel = wrapper.findComponent({ ref: 'carousel' })
|
||||
.vm as CarouselInstance
|
||||
expect(carousel.direction).toBe('horizontal')
|
||||
expect(wrapper.findAll('.el-carousel__item').length).toEqual(3)
|
||||
})
|
||||
|
||||
it('auto play', async (done) => {
|
||||
const wrapper = _mount(`
|
||||
<div>
|
||||
<el-carousel :interval="50">
|
||||
<el-carousel-item v-for="item in 3" :key="item"></el-carousel-item>
|
||||
</el-carousel>
|
||||
</div>
|
||||
`)
|
||||
const wrapper = mount(() => (
|
||||
<div>
|
||||
<Carousel interval={50}>{generateCarouselItems()}</Carousel>
|
||||
</div>
|
||||
))
|
||||
|
||||
await nextTick()
|
||||
await wait(10)
|
||||
@@ -52,13 +47,13 @@ describe('Carousel', () => {
|
||||
})
|
||||
|
||||
it('initial index', async (done) => {
|
||||
const wrapper = _mount(`
|
||||
<div>
|
||||
<el-carousel :autoplay="false" :initial-index="1">
|
||||
<el-carousel-item v-for="item in 3" :key="item"></el-carousel-item>
|
||||
</el-carousel>
|
||||
</div>
|
||||
`)
|
||||
const wrapper = mount(() => (
|
||||
<div>
|
||||
<Carousel autoplay={false} initial-index={1}>
|
||||
{generateCarouselItems()}
|
||||
</Carousel>
|
||||
</div>
|
||||
))
|
||||
|
||||
await nextTick()
|
||||
await wait(10)
|
||||
@@ -72,13 +67,11 @@ describe('Carousel', () => {
|
||||
})
|
||||
|
||||
it('reset timer', async (done) => {
|
||||
const wrapper = _mount(`
|
||||
<div>
|
||||
<el-carousel :interval="500">
|
||||
<el-carousel-item v-for="item in 3" :key="item"></el-carousel-item>
|
||||
</el-carousel>
|
||||
</div>
|
||||
`)
|
||||
const wrapper = mount(() => (
|
||||
<div>
|
||||
<Carousel interval={500}>{generateCarouselItems()}</Carousel>
|
||||
</div>
|
||||
))
|
||||
await nextTick()
|
||||
const items = wrapper.vm.$el.querySelectorAll('.el-carousel__item')
|
||||
await wrapper.trigger('mouseenter')
|
||||
@@ -92,43 +85,45 @@ describe('Carousel', () => {
|
||||
})
|
||||
|
||||
it('change', async (done) => {
|
||||
const wrapper = _mount(
|
||||
`
|
||||
<div>
|
||||
<el-carousel :interval="50" @change="handleChange">
|
||||
<el-carousel-item v-for="item in 3" :key="item"></el-carousel-item>
|
||||
</el-carousel>
|
||||
</div>
|
||||
`,
|
||||
() => {
|
||||
return {
|
||||
val: -1,
|
||||
oldVal: -1,
|
||||
const state = reactive({
|
||||
val: -1,
|
||||
oldVal: -1,
|
||||
})
|
||||
|
||||
mount({
|
||||
setup() {
|
||||
const handleChange = (val: number, oldVal: number) => {
|
||||
state.val = val
|
||||
state.oldVal = oldVal
|
||||
}
|
||||
|
||||
return () => (
|
||||
<div>
|
||||
<Carousel interval={50} onChange={handleChange}>
|
||||
{generateCarouselItems()}
|
||||
</Carousel>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
handleChange(val, oldVal) {
|
||||
this.val = val
|
||||
this.oldVal = oldVal
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
await nextTick()
|
||||
await wait(50)
|
||||
expect(wrapper.vm.val).toBe(1)
|
||||
expect(wrapper.vm.oldVal).toBe(0)
|
||||
expect(state.val).toBe(1)
|
||||
expect(state.oldVal).toBe(0)
|
||||
done()
|
||||
})
|
||||
|
||||
it('label', async (done) => {
|
||||
const wrapper = _mount(`
|
||||
<div>
|
||||
<el-carousel>
|
||||
<el-carousel-item v-for="item in 3" :key="item" :label="item"></el-carousel-item>
|
||||
</el-carousel>
|
||||
</div>
|
||||
`)
|
||||
const wrapper = mount(() => (
|
||||
<div>
|
||||
<Carousel>
|
||||
{[1, 2, 3].map((i) => (
|
||||
<CarouselItem key={i} label={i}></CarouselItem>
|
||||
))}
|
||||
</Carousel>
|
||||
</div>
|
||||
))
|
||||
await nextTick()
|
||||
expect(wrapper.find('.el-carousel__button span').text()).toBe('1')
|
||||
done()
|
||||
@@ -136,13 +131,11 @@ describe('Carousel', () => {
|
||||
|
||||
describe('manual control', () => {
|
||||
it('hover', async (done) => {
|
||||
const wrapper = _mount(`
|
||||
const wrapper = mount(() => (
|
||||
<div>
|
||||
<el-carousel :autoplay="false">
|
||||
<el-carousel-item v-for="item in 3" :key="item"></el-carousel-item>
|
||||
</el-carousel>
|
||||
<Carousel autoplay={false}>{generateCarouselItems()}</Carousel>
|
||||
</div>
|
||||
`)
|
||||
))
|
||||
|
||||
await nextTick()
|
||||
await wait()
|
||||
@@ -159,13 +152,13 @@ describe('Carousel', () => {
|
||||
})
|
||||
|
||||
it('card', async (done) => {
|
||||
const wrapper = _mount(`
|
||||
<div>
|
||||
<el-carousel :autoplay="false" type="card">
|
||||
<el-carousel-item v-for="item in 7" :key="item"></el-carousel-item>
|
||||
</el-carousel>
|
||||
</div>
|
||||
`)
|
||||
const wrapper = mount(() => (
|
||||
<div>
|
||||
<Carousel autoplay={false} type="card">
|
||||
{generateCarouselItems(7)}
|
||||
</Carousel>
|
||||
</div>
|
||||
))
|
||||
await nextTick()
|
||||
await wait()
|
||||
const items = wrapper.vm.$el.querySelectorAll('.el-carousel__item')
|
||||
@@ -185,27 +178,37 @@ describe('Carousel', () => {
|
||||
})
|
||||
|
||||
it('vertical direction', () => {
|
||||
const wrapper = _mount(`
|
||||
<div>
|
||||
<el-carousel ref="carousel" :autoplay="false" direction="vertical" height="100px">
|
||||
<el-carousel-item v-for="item in 3" :key="item"></el-carousel-item>
|
||||
</el-carousel>
|
||||
</div>
|
||||
`)
|
||||
const wrapper = mount({
|
||||
setup() {
|
||||
return () => (
|
||||
<div>
|
||||
<Carousel
|
||||
ref="carousel"
|
||||
autoplay={false}
|
||||
direction="vertical"
|
||||
height="100px"
|
||||
>
|
||||
{generateCarouselItems()}
|
||||
</Carousel>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
const items = wrapper.vm.$el.querySelectorAll('.el-carousel__item')
|
||||
|
||||
expect(wrapper.vm.$refs.carousel.direction).toBe('vertical')
|
||||
const carousel = wrapper.findComponent({ ref: 'carousel' })
|
||||
.vm as CarouselInstance
|
||||
expect(carousel.direction).toBe('vertical')
|
||||
expect(items[0].style.transform.includes('translateY')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('pause auto play on hover', async (done) => {
|
||||
const wrapper = _mount(`
|
||||
<div>
|
||||
<el-carousel :interval="50" :pause-on-hover="false">
|
||||
<el-carousel-item v-for="item in 3" :key="item"></el-carousel-item>
|
||||
</el-carousel>
|
||||
</div>
|
||||
`)
|
||||
const wrapper = mount(() => (
|
||||
<div>
|
||||
<Carousel interval={50} pause-on-hover={false}>
|
||||
{generateCarouselItems()}
|
||||
</Carousel>
|
||||
</div>
|
||||
))
|
||||
|
||||
await nextTick()
|
||||
await wrapper.find('.el-carousel').trigger('mouseenter')
|
||||
@@ -1,6 +1,6 @@
|
||||
import { withInstall, withNoopInstall } from '@element-plus/utils'
|
||||
import Carousel from './src/main.vue'
|
||||
import CarouselItem from './src/item.vue'
|
||||
import Carousel from './src/carousel.vue'
|
||||
import CarouselItem from './src/carousel-item.vue'
|
||||
|
||||
export const ElCarousel = withInstall(Carousel, {
|
||||
CarouselItem,
|
||||
@@ -9,3 +9,6 @@ export const ElCarousel = withInstall(Carousel, {
|
||||
export default ElCarousel
|
||||
|
||||
export const ElCarouselItem = withNoopInstall(CarouselItem)
|
||||
|
||||
export * from './src/carousel'
|
||||
export * from './src/carousel-item'
|
||||
|
||||
15
packages/components/carousel/src/carousel-item.ts
Normal file
15
packages/components/carousel/src/carousel-item.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { buildProps } from '@element-plus/utils'
|
||||
import type { ExtractPropTypes } from 'vue'
|
||||
import type CarouselItem from './carousel-item.vue'
|
||||
|
||||
export const carouselItemProps = buildProps({
|
||||
name: { type: String, default: '' },
|
||||
label: {
|
||||
type: [String, Number],
|
||||
default: '',
|
||||
},
|
||||
} as const)
|
||||
|
||||
export type CarouselItemProps = ExtractPropTypes<typeof carouselItemProps>
|
||||
|
||||
export type CarouselItemInstance = InstanceType<typeof CarouselItem>
|
||||
169
packages/components/carousel/src/carousel-item.vue
Normal file
169
packages/components/carousel/src/carousel-item.vue
Normal file
@@ -0,0 +1,169 @@
|
||||
<template>
|
||||
<div
|
||||
v-show="ready"
|
||||
:class="[
|
||||
ns.e('item'),
|
||||
ns.is('active', active),
|
||||
ns.is('in-stage', inStage),
|
||||
ns.is('hover', hover),
|
||||
ns.is('animating', animating),
|
||||
{ [ns.em('item', 'card')]: type === 'card' },
|
||||
]"
|
||||
:style="itemStyle"
|
||||
@click="handleItemClick"
|
||||
>
|
||||
<div v-if="type === 'card'" v-show="!active" :class="ns.e('mask')" />
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
onMounted,
|
||||
inject,
|
||||
computed,
|
||||
getCurrentInstance,
|
||||
onUnmounted,
|
||||
ref,
|
||||
} from 'vue'
|
||||
import { debugWarn } from '@element-plus/utils'
|
||||
import { useNamespace } from '@element-plus/hooks'
|
||||
import { carouselContextKey } from '@element-plus/tokens'
|
||||
import { carouselItemProps } from './carousel-item'
|
||||
import type { CSSProperties } from 'vue'
|
||||
|
||||
defineOptions({
|
||||
name: 'ElCarouselItem',
|
||||
})
|
||||
|
||||
const props = defineProps(carouselItemProps)
|
||||
|
||||
// inject
|
||||
const carouselContext = inject(carouselContextKey)
|
||||
|
||||
const ns = useNamespace('carousel')
|
||||
const CARD_SCALE = 0.83
|
||||
const type = carouselContext?.type
|
||||
|
||||
// instance
|
||||
const instance = getCurrentInstance()
|
||||
|
||||
const hover = ref(false)
|
||||
const translate = ref(0)
|
||||
const scale = ref(1)
|
||||
const active = ref(false)
|
||||
const ready = ref(false)
|
||||
const inStage = ref(false)
|
||||
const animating = ref(false)
|
||||
|
||||
// computed
|
||||
const parentDirection = computed(() => {
|
||||
return carouselContext?.direction
|
||||
})
|
||||
|
||||
const itemStyle = computed(() => {
|
||||
const translateType =
|
||||
parentDirection.value === 'vertical' ? 'translateY' : 'translateX'
|
||||
const value = `${translateType}(${translate.value}px) scale(${scale.value})`
|
||||
const style: CSSProperties = {
|
||||
transform: value,
|
||||
}
|
||||
return style
|
||||
})
|
||||
|
||||
// methods
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
function calcCardTranslate(index, activeIndex) {
|
||||
const parentWidth = carouselContext?.root.value?.offsetWidth || 0
|
||||
if (inStage.value) {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
function calcTranslate(index, activeIndex, isVertical) {
|
||||
const distance =
|
||||
(isVertical
|
||||
? carouselContext?.root.value?.offsetHeight
|
||||
: carouselContext?.root.value?.offsetWidth) || 0
|
||||
return distance * (index - activeIndex)
|
||||
}
|
||||
|
||||
const translateItem = (
|
||||
index: number,
|
||||
activeIndex: number,
|
||||
oldIndex: number
|
||||
) => {
|
||||
const parentType = carouselContext?.type
|
||||
const length = carouselContext?.items.value.length ?? Number.NaN
|
||||
if (parentType !== 'card' && oldIndex !== undefined) {
|
||||
animating.value = index === activeIndex || index === oldIndex
|
||||
}
|
||||
if (index !== activeIndex && length > 2 && carouselContext?.loop) {
|
||||
index = processIndex(index, activeIndex, length)
|
||||
}
|
||||
if (parentType === 'card') {
|
||||
if (parentDirection.value === 'vertical') {
|
||||
debugWarn('Carousel', 'vertical direction is not supported in card mode')
|
||||
}
|
||||
inStage.value = Math.round(Math.abs(index - activeIndex)) <= 1
|
||||
active.value = index === activeIndex
|
||||
translate.value = calcCardTranslate(index, activeIndex)
|
||||
scale.value = active.value ? 1 : CARD_SCALE
|
||||
} else {
|
||||
active.value = index === activeIndex
|
||||
const isVertical = parentDirection.value === 'vertical'
|
||||
translate.value = calcTranslate(index, activeIndex, isVertical)
|
||||
}
|
||||
ready.value = true
|
||||
}
|
||||
|
||||
function handleItemClick() {
|
||||
if (carouselContext && carouselContext?.type === 'card') {
|
||||
const index = carouselContext?.items.value
|
||||
.map((d) => d.uid)
|
||||
.indexOf(instance?.uid)
|
||||
carouselContext?.setActiveItem(index)
|
||||
}
|
||||
}
|
||||
|
||||
// lifecycle
|
||||
onMounted(() => {
|
||||
if (carouselContext?.addItem) {
|
||||
carouselContext?.addItem({
|
||||
uid: instance?.uid,
|
||||
...props,
|
||||
hover,
|
||||
translate,
|
||||
scale,
|
||||
active,
|
||||
ready,
|
||||
inStage,
|
||||
animating,
|
||||
translateItem,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (carouselContext?.removeItem) {
|
||||
carouselContext?.removeItem(instance?.uid)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -1,51 +1,57 @@
|
||||
import type { Ref, ToRefs, UnwrapRef } from 'vue'
|
||||
import { buildProps, isNumber } from '@element-plus/utils'
|
||||
import type { ExtractPropTypes } from 'vue'
|
||||
import type Carousel from './carousel.vue'
|
||||
|
||||
export interface ICarouselItemProps {
|
||||
name: string
|
||||
label: string | number
|
||||
key: string
|
||||
export const carouselProps = buildProps({
|
||||
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,
|
||||
},
|
||||
} as const)
|
||||
|
||||
export const carouselEmits = {
|
||||
change: (current: number, prev: number) => [current, prev].every(isNumber),
|
||||
}
|
||||
|
||||
export interface ICarouselItemData {
|
||||
hover: boolean
|
||||
translate: number
|
||||
scale: number
|
||||
active: boolean
|
||||
ready: boolean
|
||||
inStage: boolean
|
||||
animating: boolean
|
||||
}
|
||||
export type CarouselProps = ExtractPropTypes<typeof carouselProps>
|
||||
export type CarouselEmits = typeof carouselEmits
|
||||
|
||||
export interface ICarouselProps {
|
||||
initialIndex: number
|
||||
height: string
|
||||
trigger: string
|
||||
autoplay: boolean
|
||||
interval: number
|
||||
indicatorPosition: string
|
||||
indicator: boolean
|
||||
arrow: string
|
||||
type: string
|
||||
loop: boolean
|
||||
direction: string
|
||||
pauseOnHover: boolean
|
||||
}
|
||||
|
||||
export type UnionCarouselItemData = ICarouselItemProps &
|
||||
ToRefs<ICarouselItemData>
|
||||
|
||||
export interface CarouselItem extends UnionCarouselItemData {
|
||||
uid: number
|
||||
translateItem: (index: number, activeIndex: number, oldIndex: number) => void
|
||||
}
|
||||
|
||||
export interface InjectCarouselScope {
|
||||
root: Ref<HTMLElement>
|
||||
direction: string
|
||||
type: string
|
||||
items: Ref<UnwrapRef<CarouselItem[]>>
|
||||
loop: boolean
|
||||
addItem: (item: CarouselItem) => void
|
||||
removeItem: (uid: number) => void
|
||||
setActiveItem: (index: number) => void
|
||||
}
|
||||
export type CarouselInstance = InstanceType<typeof Carousel>
|
||||
|
||||
324
packages/components/carousel/src/carousel.vue
Normal file
324
packages/components/carousel/src/carousel.vue
Normal file
@@ -0,0 +1,324 @@
|
||||
<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' || hover) && (props.loop || activeIndex > 0)
|
||||
"
|
||||
type="button"
|
||||
:class="[ns.e('arrow'), ns.em('arrow', 'left')]"
|
||||
@mouseenter="handleButtonEnter('left')"
|
||||
@mouseleave="handleButtonLeave"
|
||||
@click.stop="throttledArrowClick(activeIndex - 1)"
|
||||
>
|
||||
<ElIcon>
|
||||
<ArrowLeft />
|
||||
</ElIcon>
|
||||
</button>
|
||||
</transition>
|
||||
<transition v-if="arrowDisplay" name="carousel-arrow-right">
|
||||
<button
|
||||
v-show="
|
||||
(arrow === 'always' || hover) &&
|
||||
(props.loop || activeIndex < items.length - 1)
|
||||
"
|
||||
type="button"
|
||||
:class="[ns.e('arrow'), ns.em('arrow', 'right')]"
|
||||
@mouseenter="handleButtonEnter('right')"
|
||||
@mouseleave="handleButtonLeave"
|
||||
@click.stop="throttledArrowClick(activeIndex + 1)"
|
||||
>
|
||||
<ElIcon>
|
||||
<ArrowRight />
|
||||
</ElIcon>
|
||||
</button>
|
||||
</transition>
|
||||
<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 === 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" setup>
|
||||
import {
|
||||
ref,
|
||||
computed,
|
||||
provide,
|
||||
onMounted,
|
||||
onBeforeUnmount,
|
||||
watch,
|
||||
nextTick,
|
||||
} from 'vue'
|
||||
import { throttle } from 'lodash-unified'
|
||||
import { useResizeObserver } from '@vueuse/core'
|
||||
import { debugWarn } from '@element-plus/utils'
|
||||
import { ElIcon } from '@element-plus/components/icon'
|
||||
import { ArrowLeft, ArrowRight } from '@element-plus/icons-vue'
|
||||
import { useNamespace } from '@element-plus/hooks'
|
||||
import { carouselContextKey } from '@element-plus/tokens'
|
||||
import { carouselProps, carouselEmits } from './carousel'
|
||||
import type { CarouselItemContext } from '@element-plus/tokens'
|
||||
|
||||
defineOptions({
|
||||
name: 'ElCarousel',
|
||||
})
|
||||
const props = defineProps(carouselProps)
|
||||
const emit = defineEmits(carouselEmits)
|
||||
const ns = useNamespace('carousel')
|
||||
|
||||
// refs
|
||||
const activeIndex = ref(-1)
|
||||
const timer = ref<ReturnType<typeof setInterval> | null>(null)
|
||||
const hover = ref(false)
|
||||
const root = ref<HTMLDivElement>()
|
||||
const items = ref<CarouselItemContext[]>([])
|
||||
|
||||
// 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 (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) {
|
||||
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 (Number.isNaN(index) || index !== Math.floor(index)) {
|
||||
debugWarn('Carousel', 'index must be an integer.')
|
||||
return
|
||||
}
|
||||
const length = items.value.length
|
||||
const oldIndex = activeIndex.value
|
||||
if (index < 0) {
|
||||
activeIndex.value = props.loop ? length - 1 : 0
|
||||
} else if (index >= length) {
|
||||
activeIndex.value = props.loop ? 0 : length - 1
|
||||
} else {
|
||||
activeIndex.value = index
|
||||
}
|
||||
if (oldIndex === activeIndex.value) {
|
||||
resetItemPosition(oldIndex)
|
||||
}
|
||||
}
|
||||
|
||||
function resetItemPosition(oldIndex) {
|
||||
items.value.forEach((item, index) => {
|
||||
item.translateItem(index, activeIndex.value, 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 (activeIndex.value === 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() {
|
||||
hover.value = true
|
||||
if (props.pauseOnHover) {
|
||||
pauseTimer()
|
||||
}
|
||||
}
|
||||
|
||||
function handleMouseLeave() {
|
||||
hover.value = 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) {
|
||||
activeIndex.value = index
|
||||
}
|
||||
|
||||
function handleIndicatorHover(index) {
|
||||
if (props.trigger === 'hover' && index !== activeIndex.value) {
|
||||
activeIndex.value = index
|
||||
}
|
||||
}
|
||||
|
||||
function prev() {
|
||||
setActiveItem(activeIndex.value - 1)
|
||||
}
|
||||
|
||||
function next() {
|
||||
setActiveItem(activeIndex.value + 1)
|
||||
}
|
||||
|
||||
// watch
|
||||
watch(
|
||||
() => activeIndex.value,
|
||||
(current, prev) => {
|
||||
resetItemPosition(prev)
|
||||
if (prev > -1) {
|
||||
emit('change', current, prev)
|
||||
}
|
||||
}
|
||||
)
|
||||
watch(
|
||||
() => props.autoplay,
|
||||
(current) => {
|
||||
current ? startTimer() : pauseTimer()
|
||||
}
|
||||
)
|
||||
watch(
|
||||
() => props.loop,
|
||||
() => {
|
||||
setActiveItem(activeIndex.value)
|
||||
}
|
||||
)
|
||||
|
||||
// lifecycle
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
const resizeObserver = useResizeObserver(root.value, resetItemPosition)
|
||||
if (props.initialIndex < items.value.length && props.initialIndex >= 0) {
|
||||
activeIndex.value = props.initialIndex
|
||||
}
|
||||
startTimer()
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (root.value) resizeObserver.stop()
|
||||
pauseTimer()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// provide
|
||||
provide(carouselContextKey, {
|
||||
root,
|
||||
direction: props.direction,
|
||||
type: props.type,
|
||||
items,
|
||||
loop: props.loop,
|
||||
addItem,
|
||||
removeItem,
|
||||
setActiveItem,
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
/** @description manually switch slide */
|
||||
setActiveItem,
|
||||
/** @description switch to the previous slide */
|
||||
prev,
|
||||
/** @description switch to the next slide */
|
||||
next,
|
||||
})
|
||||
</script>
|
||||
@@ -1,186 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
v-show="data.ready"
|
||||
:class="[
|
||||
ns.e('item'),
|
||||
ns.is('active', data.active),
|
||||
ns.is('in-stage', data.inStage),
|
||||
ns.is('hover', data.hover),
|
||||
ns.is('animating', data.animating),
|
||||
{ [ns.em('item', 'card')]: type === 'card' },
|
||||
]"
|
||||
:style="itemStyle"
|
||||
@click="handleItemClick"
|
||||
>
|
||||
<div v-if="type === 'card'" v-show="!data.active" :class="ns.e('mask')" />
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import {
|
||||
defineComponent,
|
||||
reactive,
|
||||
onMounted,
|
||||
inject,
|
||||
computed,
|
||||
toRefs,
|
||||
getCurrentInstance,
|
||||
onUnmounted,
|
||||
} from 'vue'
|
||||
import { debugWarn } from '@element-plus/utils'
|
||||
import { useNamespace } from '@element-plus/hooks'
|
||||
import type { CSSProperties } from 'vue'
|
||||
import type { InjectCarouselScope, ICarouselItemProps } from './carousel'
|
||||
|
||||
const CARD_SCALE = 0.83
|
||||
export default defineComponent({
|
||||
name: 'ElCarouselItem',
|
||||
props: {
|
||||
name: { type: String, default: '' },
|
||||
label: {
|
||||
type: [String, Number],
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
setup(props: ICarouselItemProps) {
|
||||
const ns = useNamespace('carousel')
|
||||
// instance
|
||||
const instance = getCurrentInstance()
|
||||
|
||||
// data
|
||||
const data = reactive({
|
||||
hover: false,
|
||||
translate: 0,
|
||||
scale: 1,
|
||||
active: false,
|
||||
ready: false,
|
||||
inStage: false,
|
||||
animating: false,
|
||||
})
|
||||
|
||||
// inject
|
||||
const injectCarouselScope: InjectCarouselScope = inject(
|
||||
'injectCarouselScope'
|
||||
)
|
||||
|
||||
// computed
|
||||
const parentDirection = computed(() => {
|
||||
return injectCarouselScope.direction
|
||||
})
|
||||
|
||||
const itemStyle = computed(() => {
|
||||
const translateType =
|
||||
parentDirection.value === 'vertical' ? 'translateY' : 'translateX'
|
||||
const value = `${translateType}(${data.translate}px) scale(${data.scale})`
|
||||
const style: CSSProperties = {
|
||||
transform: value,
|
||||
}
|
||||
return style
|
||||
})
|
||||
|
||||
// methods
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
function calcCardTranslate(index, activeIndex) {
|
||||
const parentWidth = injectCarouselScope.root.value?.offsetWidth || 0
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
function calcTranslate(index, activeIndex, isVertical) {
|
||||
const distance =
|
||||
(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
|
||||
if (parentType !== 'card' && oldIndex !== undefined) {
|
||||
data.animating = index === activeIndex || index === oldIndex
|
||||
}
|
||||
if (index !== activeIndex && length > 2 && injectCarouselScope.loop) {
|
||||
index = processIndex(index, activeIndex, length)
|
||||
}
|
||||
if (parentType === 'card') {
|
||||
if (parentDirection.value === 'vertical') {
|
||||
debugWarn(
|
||||
'Carousel',
|
||||
'vertical direction is not supported in card mode'
|
||||
)
|
||||
}
|
||||
data.inStage = Math.round(Math.abs(index - activeIndex)) <= 1
|
||||
data.active = index === activeIndex
|
||||
data.translate = calcCardTranslate(index, activeIndex)
|
||||
data.scale = data.active ? 1 : CARD_SCALE
|
||||
} else {
|
||||
data.active = index === activeIndex
|
||||
const isVertical = parentDirection.value === 'vertical'
|
||||
data.translate = calcTranslate(index, activeIndex, isVertical)
|
||||
}
|
||||
data.ready = true
|
||||
}
|
||||
|
||||
function handleItemClick() {
|
||||
if (injectCarouselScope && injectCarouselScope.type === 'card') {
|
||||
const index = injectCarouselScope.items.value
|
||||
.map((d) => d.uid)
|
||||
.indexOf(instance.uid)
|
||||
injectCarouselScope.setActiveItem(index)
|
||||
}
|
||||
}
|
||||
|
||||
// lifecycle
|
||||
onMounted(() => {
|
||||
if (injectCarouselScope.addItem) {
|
||||
injectCarouselScope.addItem({
|
||||
uid: instance.uid,
|
||||
...props,
|
||||
...toRefs(data),
|
||||
translateItem,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (injectCarouselScope.removeItem) {
|
||||
injectCarouselScope.removeItem(instance.uid)
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
data,
|
||||
itemStyle,
|
||||
translateItem,
|
||||
type: injectCarouselScope.type,
|
||||
handleItemClick,
|
||||
ns,
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
@@ -1,416 +0,0 @@
|
||||
<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 />
|
||||
</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,
|
||||
debugWarn,
|
||||
} from '@element-plus/utils'
|
||||
import { ElIcon } from '@element-plus/components/icon'
|
||||
import { ArrowLeft, ArrowRight } from '@element-plus/icons-vue'
|
||||
import { useNamespace } from '@element-plus/hooks'
|
||||
|
||||
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 (Number.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>
|
||||
35
packages/tokens/carousel.ts
Normal file
35
packages/tokens/carousel.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import type { InjectionKey, Ref, ToRefs, UnwrapRef } from 'vue'
|
||||
|
||||
import type { CarouselItemProps } from '@element-plus/components/carousel'
|
||||
|
||||
export type CarouselItemContext = CarouselItemProps &
|
||||
ToRefs<{
|
||||
hover: boolean
|
||||
translate: number
|
||||
scale: number
|
||||
active: boolean
|
||||
ready: boolean
|
||||
inStage: boolean
|
||||
animating: boolean
|
||||
}> & {
|
||||
uid: number | undefined
|
||||
translateItem: (
|
||||
index: number,
|
||||
activeIndex: number,
|
||||
oldIndex: number
|
||||
) => void
|
||||
}
|
||||
|
||||
export type CarouselContext = {
|
||||
root: Ref<HTMLElement | undefined>
|
||||
direction: string
|
||||
type: string
|
||||
items: Ref<UnwrapRef<CarouselItemContext[]>>
|
||||
loop: boolean
|
||||
addItem: (item: CarouselItemContext) => void
|
||||
removeItem: (uid: number | undefined) => void
|
||||
setActiveItem: (index: number) => void
|
||||
}
|
||||
|
||||
export const carouselContextKey: InjectionKey<CarouselContext> =
|
||||
Symbol('carouselContextKey')
|
||||
@@ -1,5 +1,6 @@
|
||||
export * from './breadcrumb'
|
||||
export * from './button'
|
||||
export * from './carousel'
|
||||
export * from './collapse'
|
||||
export * from './config-provider'
|
||||
export * from './dialog'
|
||||
|
||||
Reference in New Issue
Block a user