mirror of
https://gitee.com/element-plus/element-plus.git
synced 2024-11-30 02:08:12 +08:00
Feat/carousel (#151)
* feat: init carousel component * feat(carousel): init carousel component * chore: add global dependencies * chore: use prettier formate code * feat(carousel): transfer logic - progress 40% * feat(carousel): migrate logic - progress 50% * feat(carousel): migrate logic - progress 55% * feat(carousel): replenish doc * feat(carousel): add utils * feat(carousel): finish component logi * feat(carousel): fix type error * feat(carousel): test case 80% * feat(carousel): migrate finish * feat(carousel): more test cases * feat(carousel): test case passed * feat(carousel): fix CI dependencies collides * feat(carousel): update yarn.lock * feat(carousel): merge sub component * feat(carousel): fix lose ctx attribute in buid env * feat(carousel): finish spec * feat(carousel): optimize code * chore: update yarn lock * feat(carousel): fall back lock file * chore(carousel): fallback dep * feat(carousel): update vue dep * feat(carousel): update spec file * feat(carousel): use async test * feat(carousel): revert eslint modify Co-authored-by: liik <385211478@qq.com> Co-authored-by: liik <linyunqianpp@126.com>
This commit is contained in:
parent
34c248fe70
commit
1f8fc62d73
8
.prettierrc.js
Normal file
8
.prettierrc.js
Normal file
@ -0,0 +1,8 @@
|
||||
module.exports = {
|
||||
semi: false,
|
||||
trailingComma: 'all',
|
||||
singleQuote: true,
|
||||
printWidth: 80,
|
||||
tabWidth: 2,
|
||||
endOfLine: 'auto',
|
||||
}
|
200
packages/carousel/__tests__/carousel.spec.ts
Normal file
200
packages/carousel/__tests__/carousel.spec.ts
Normal file
@ -0,0 +1,200 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { nextTick } from 'vue'
|
||||
import Carousel from '../src/main.vue'
|
||||
import CarouselItem from '../src/item.vue'
|
||||
|
||||
const wait = (ms = 100) =>
|
||||
new Promise(resolve => setTimeout(() => resolve(), ms))
|
||||
|
||||
const _mount = (template: string, data?: () => void, methods?: any) =>
|
||||
mount({
|
||||
components: {
|
||||
'el-carousel': Carousel,
|
||||
'el-carousel-item': CarouselItem,
|
||||
},
|
||||
template,
|
||||
data,
|
||||
methods,
|
||||
})
|
||||
|
||||
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>
|
||||
`,
|
||||
)
|
||||
|
||||
expect(wrapper.vm.$refs.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>
|
||||
`)
|
||||
|
||||
await nextTick()
|
||||
await wait(10)
|
||||
const items = wrapper.vm.$el.querySelectorAll('.el-carousel__item')
|
||||
expect(items[0].classList.contains('is-active')).toBeTruthy()
|
||||
await wait(60)
|
||||
expect(items[1].classList.contains('is-active')).toBeTruthy()
|
||||
done()
|
||||
})
|
||||
|
||||
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>
|
||||
`)
|
||||
|
||||
await nextTick()
|
||||
await wait(10)
|
||||
|
||||
expect(
|
||||
wrapper.vm.$el
|
||||
.querySelectorAll('.el-carousel__item')[1]
|
||||
.classList.contains('is-active'),
|
||||
).toBeTruthy()
|
||||
done()
|
||||
})
|
||||
|
||||
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>
|
||||
`)
|
||||
await nextTick()
|
||||
const items = wrapper.vm.$el.querySelectorAll('.el-carousel__item')
|
||||
await wrapper.trigger('mouseenter')
|
||||
await nextTick()
|
||||
expect(items[0].classList.contains('is-active')).toBeTruthy()
|
||||
await wrapper.trigger('mouseleave')
|
||||
await nextTick()
|
||||
await wait(700)
|
||||
expect(items[1].classList.contains('is-active')).toBeTruthy()
|
||||
done()
|
||||
})
|
||||
|
||||
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,
|
||||
}
|
||||
},
|
||||
{
|
||||
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)
|
||||
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>
|
||||
`)
|
||||
await nextTick()
|
||||
expect(wrapper.find('.el-carousel__button span').text()).toBe('1')
|
||||
done()
|
||||
})
|
||||
|
||||
describe('manual control', () => {
|
||||
it('hover', async done => {
|
||||
const wrapper = _mount(`
|
||||
<div>
|
||||
<el-carousel :autoplay="false">
|
||||
<el-carousel-item v-for="item in 3" :key="item"></el-carousel-item>
|
||||
</el-carousel>
|
||||
</div>
|
||||
`)
|
||||
|
||||
await nextTick()
|
||||
await wait()
|
||||
await wrapper.findAll('.el-carousel__indicator')[1].trigger('mouseenter')
|
||||
await nextTick()
|
||||
await wait()
|
||||
expect(
|
||||
wrapper.vm.$el
|
||||
.querySelectorAll('.el-carousel__item')[1]
|
||||
.classList.contains('is-active'),
|
||||
).toBeTruthy()
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
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>
|
||||
`)
|
||||
await nextTick()
|
||||
await wait()
|
||||
const items = wrapper.vm.$el.querySelectorAll('.el-carousel__item')
|
||||
expect(items[0].classList.contains('is-active')).toBeTruthy()
|
||||
expect(items[1].classList.contains('is-in-stage')).toBeTruthy()
|
||||
expect(items[6].classList.contains('is-in-stage')).toBeTruthy()
|
||||
await items[1].click()
|
||||
await wait()
|
||||
expect(items[1].classList.contains('is-active')).toBeTruthy()
|
||||
await wrapper.vm.$el.querySelector('.el-carousel__arrow--left').click()
|
||||
await wait()
|
||||
expect(items[0].classList.contains('is-active')).toBeTruthy()
|
||||
await items[6].click()
|
||||
await wait()
|
||||
expect(items[6].classList.contains('is-active')).toBeTruthy()
|
||||
done()
|
||||
})
|
||||
|
||||
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 items = wrapper.vm.$el.querySelectorAll('.el-carousel__item')
|
||||
|
||||
expect(wrapper.vm.$refs.carousel.direction).toBe('vertical')
|
||||
expect(items[0].style.transform.indexOf('translateY') !== -1).toBeTruthy()
|
||||
})
|
||||
})
|
25
packages/carousel/doc/carousel-card.vue
Normal file
25
packages/carousel/doc/carousel-card.vue
Normal file
@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<el-carousel :interval="4000" type="card" height="200px">
|
||||
<el-carousel-item v-for="item in 6" :key="item">
|
||||
<h3 class="medium">{{ item }}</h3>
|
||||
</el-carousel-item>
|
||||
</el-carousel>
|
||||
</template>
|
||||
<script lang="ts"></script>
|
||||
<style>
|
||||
.el-carousel__item h3 {
|
||||
color: #475669;
|
||||
font-size: 14px;
|
||||
opacity: 0.75;
|
||||
line-height: 200px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.el-carousel__item:nth-child(2n) {
|
||||
background-color: #99a9bf;
|
||||
}
|
||||
|
||||
.el-carousel__item:nth-child(2n + 1) {
|
||||
background-color: #d3dce6;
|
||||
}
|
||||
</style>
|
25
packages/carousel/doc/carousel-vertical.vue
Normal file
25
packages/carousel/doc/carousel-vertical.vue
Normal file
@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<el-carousel height="200px" direction="vertical" :autoplay="false">
|
||||
<el-carousel-item v-for="item in 3" :key="item">
|
||||
<h3 class="medium">{{ item }}</h3>
|
||||
</el-carousel-item>
|
||||
</el-carousel>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.el-carousel__item h3 {
|
||||
color: #475669;
|
||||
font-size: 14px;
|
||||
opacity: 0.75;
|
||||
line-height: 200px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.el-carousel__item:nth-child(2n) {
|
||||
background-color: #99a9bf;
|
||||
}
|
||||
|
||||
.el-carousel__item:nth-child(2n + 1) {
|
||||
background-color: #d3dce6;
|
||||
}
|
||||
</style>
|
40
packages/carousel/doc/carousel.vue
Normal file
40
packages/carousel/doc/carousel.vue
Normal file
@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<div class="carousel">
|
||||
<div class="block">
|
||||
<span class="demonstration">默认 Hover 指示器触发</span>
|
||||
<el-carousel height="150px">
|
||||
<el-carousel-item v-for="item in 4" :key="item">
|
||||
<h3 class="small">{{ item }}</h3>
|
||||
</el-carousel-item>
|
||||
</el-carousel>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
</script>
|
||||
<style>
|
||||
* {
|
||||
text-align: center;
|
||||
}
|
||||
.demonstration {
|
||||
display: block;
|
||||
color: #8492a6;
|
||||
font-size: 14px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.el-carousel__item h3 {
|
||||
color: #475669;
|
||||
font-size: 14px;
|
||||
opacity: 0.75;
|
||||
line-height: 150px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.el-carousel__item:nth-child(2n) {
|
||||
background-color: #99a9bf;
|
||||
}
|
||||
|
||||
.el-carousel__item:nth-child(2n + 1) {
|
||||
background-color: #d3dce6;
|
||||
}
|
||||
</style>
|
7
packages/carousel/doc/index.stories.ts
Normal file
7
packages/carousel/doc/index.stories.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export { default as BasicUsage } from './carousel.vue'
|
||||
export { default as CardUsage } from './carousel-card.vue'
|
||||
export { default as VerticalUsage } from './carousel-vertical.vue'
|
||||
|
||||
export default {
|
||||
title: 'Carousel',
|
||||
}
|
7
packages/carousel/index.ts
Normal file
7
packages/carousel/index.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { App } from 'vue'
|
||||
import Carousel from './src/main.vue'
|
||||
import CarouselItem from './src/item.vue'
|
||||
export default (app: App): void => {
|
||||
app.component(Carousel.name, Carousel)
|
||||
app.component(CarouselItem.name, CarouselItem)
|
||||
}
|
12
packages/carousel/package.json
Normal file
12
packages/carousel/package.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "@element-plus/carousel",
|
||||
"version": "0.0.0",
|
||||
"main": "dist/index.js",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"vue": "^3.0.0-rc.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/test-utils": "^2.0.0-beta.0"
|
||||
}
|
||||
}
|
199
packages/carousel/src/item.vue
Normal file
199
packages/carousel/src/item.vue
Normal file
@ -0,0 +1,199 @@
|
||||
<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>
|
||||
<script lang="ts">
|
||||
import {
|
||||
defineComponent,
|
||||
reactive,
|
||||
onMounted,
|
||||
inject,
|
||||
computed,
|
||||
toRefs,
|
||||
getCurrentInstance,
|
||||
} from 'vue'
|
||||
import {
|
||||
autoprefixer,
|
||||
PartialCSSStyleDeclaration,
|
||||
} from '@element-plus/utils/util'
|
||||
import { InjectCarouselScope } from './main.vue'
|
||||
|
||||
export interface ICarouselItemProps {
|
||||
name: string
|
||||
label: string | number
|
||||
key: string
|
||||
}
|
||||
|
||||
export interface ICarouselItemData {
|
||||
hover: boolean
|
||||
translate: number
|
||||
scale: number
|
||||
active: boolean
|
||||
ready: boolean
|
||||
inStage: boolean
|
||||
animating: boolean
|
||||
}
|
||||
|
||||
const CARD_SCALE = 0.83
|
||||
export default defineComponent({
|
||||
name: 'ElCarouselItem',
|
||||
props: {
|
||||
name: { type: String, default: '' },
|
||||
label: {
|
||||
type: [String, Number],
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
setup(props: ICarouselItemProps) {
|
||||
// instance
|
||||
const instance = getCurrentInstance()
|
||||
instance.uid
|
||||
|
||||
// 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: PartialCSSStyleDeclaration = {
|
||||
transform: value,
|
||||
}
|
||||
return autoprefixer(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.offsetWidth.value
|
||||
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 =
|
||||
injectCarouselScope[isVertical ? 'offsetHeight' : 'offsetWidth'].value
|
||||
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') {
|
||||
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
|
||||
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.updateItems) {
|
||||
injectCarouselScope.updateItems({
|
||||
uid: instance.uid,
|
||||
...props,
|
||||
...toRefs(data),
|
||||
translateItem,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
data,
|
||||
|
||||
itemStyle,
|
||||
translateItem,
|
||||
type: injectCarouselScope.type,
|
||||
|
||||
handleItemClick,
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
<style scoped></style>
|
424
packages/carousel/src/main.vue
Normal file
424
packages/carousel/src/main.vue
Normal file
@ -0,0 +1,424 @@
|
||||
<template>
|
||||
<div
|
||||
ref="root"
|
||||
: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
|
||||
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"
|
||||
@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.value.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>
|
||||
<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>
|
||||
|
||||
<script lang="ts">
|
||||
import {
|
||||
reactive,
|
||||
computed,
|
||||
ref,
|
||||
Ref,
|
||||
provide,
|
||||
onMounted,
|
||||
ToRefs,
|
||||
UnwrapRef,
|
||||
onBeforeUnmount,
|
||||
watch,
|
||||
nextTick,
|
||||
} from 'vue'
|
||||
import throttle from 'lodash/throttle'
|
||||
import {
|
||||
addResizeListener,
|
||||
removeResizeListener,
|
||||
} from '@element-plus/utils/resize-event'
|
||||
import { ICarouselItemProps, ICarouselItemData } from './item.vue'
|
||||
|
||||
interface ICarouselProps {
|
||||
initialIndex: number
|
||||
height: string
|
||||
trigger: string
|
||||
autoplay: boolean
|
||||
interval: number
|
||||
indicatorPosition: string
|
||||
indicator: boolean
|
||||
arrow: string
|
||||
type: string
|
||||
loop: boolean
|
||||
direction: string
|
||||
}
|
||||
|
||||
type UnionCarouselItemData = ICarouselItemProps & ToRefs<ICarouselItemData>
|
||||
interface CarouselItem extends UnionCarouselItemData {
|
||||
uid: number
|
||||
translateItem: (index: number, activeIndex: number, oldIndex: number) => void
|
||||
}
|
||||
|
||||
export interface InjectCarouselScope {
|
||||
direction: string
|
||||
offsetWidth: Ref<number>
|
||||
offsetHeight: Ref<number>
|
||||
type: string
|
||||
items: Ref<UnwrapRef<CarouselItem[]>>
|
||||
loop: boolean
|
||||
updateItems: (item: CarouselItem) => void
|
||||
setActiveItem: (index: number) => void
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'ElCarousel',
|
||||
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)
|
||||
},
|
||||
},
|
||||
},
|
||||
emits: ['change'],
|
||||
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)
|
||||
const items = ref<CarouselItem[]>([])
|
||||
const offsetWidth = ref(0)
|
||||
const offsetHeight = ref(0)
|
||||
|
||||
// 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 = ['el-carousel', 'el-carousel--' + props.direction]
|
||||
if (props.type === 'card') {
|
||||
classes.push('el-carousel--card')
|
||||
}
|
||||
return classes
|
||||
})
|
||||
|
||||
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
|
||||
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)) {
|
||||
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) {
|
||||
resetItemPosition(oldIndex)
|
||||
}
|
||||
}
|
||||
|
||||
function resetItemPosition(oldIndex) {
|
||||
items.value.forEach((item, index) => {
|
||||
item.translateItem(index, data.activeIndex, oldIndex)
|
||||
})
|
||||
}
|
||||
|
||||
function updateItems(item) {
|
||||
items.value.push(item)
|
||||
}
|
||||
|
||||
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
|
||||
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 (root.value) {
|
||||
offsetWidth.value = root.value.offsetWidth
|
||||
offsetHeight.value = root.value.offsetHeight
|
||||
}
|
||||
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', {
|
||||
direction: props.direction,
|
||||
offsetWidth,
|
||||
offsetHeight,
|
||||
type: props.type,
|
||||
items,
|
||||
loop: props.loop,
|
||||
updateItems,
|
||||
setActiveItem,
|
||||
})
|
||||
|
||||
return {
|
||||
data,
|
||||
props,
|
||||
items,
|
||||
|
||||
arrowDisplay,
|
||||
carouselClasses,
|
||||
indicatorsClasses,
|
||||
hasLabel,
|
||||
|
||||
handleMouseEnter,
|
||||
handleMouseLeave,
|
||||
handleIndicatorClick,
|
||||
throttledArrowClick,
|
||||
throttledIndicatorHover,
|
||||
handleButtonEnter,
|
||||
handleButtonLeave,
|
||||
|
||||
prev,
|
||||
next,
|
||||
setActiveItem,
|
||||
|
||||
root,
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
@ -10,6 +10,7 @@ import ElDropdown from '@element-plus/dropdown'
|
||||
import ElTag from '@element-plus/tag'
|
||||
import ElLayout from '@element-plus/layout'
|
||||
import ElDivider from '@element-plus/divider'
|
||||
import ElCarousel from '@element-plus/carousel'
|
||||
import ElTimeline from '@element-plus/timeline'
|
||||
import ElProgress from '@element-plus/progress'
|
||||
import ElBreadcrumb from '@element-plus/breadcrumb'
|
||||
@ -46,6 +47,7 @@ export {
|
||||
ElDivider,
|
||||
ElDropdown,
|
||||
ElTag,
|
||||
ElCarousel,
|
||||
ElTimeline,
|
||||
ElProgress,
|
||||
ElBreadcrumb,
|
||||
@ -82,6 +84,7 @@ export default function install(app: App): void {
|
||||
ElTag(app)
|
||||
ElLayout(app)
|
||||
ElDivider(app)
|
||||
ElCarousel(app)
|
||||
ElTimeline(app)
|
||||
ElProgress(app)
|
||||
ElBreadcrumb(app)
|
||||
|
@ -6,6 +6,9 @@ import { isEmpty, castArray, isEqual } from 'lodash'
|
||||
import type { AnyFunction } from './types'
|
||||
import type { Ref } from 'vue'
|
||||
|
||||
export type PartialCSSStyleDeclaration = Partial<
|
||||
Pick<CSSStyleDeclaration, 'transform' | 'transition' | 'animation'>
|
||||
>
|
||||
const { hasOwnProperty } = Object.prototype
|
||||
|
||||
export function hasOwn(obj: any, key: string): boolean {
|
||||
@ -82,13 +85,12 @@ export const isEdge = function(): boolean {
|
||||
}
|
||||
|
||||
export const isFirefox = function(): boolean {
|
||||
return (
|
||||
!isServer && !!window.navigator.userAgent.match(/firefox/i)
|
||||
)
|
||||
return !isServer && !!window.navigator.userAgent.match(/firefox/i)
|
||||
}
|
||||
|
||||
export const autoprefixer = function(style: CSSStyleDeclaration): CSSStyleDeclaration {
|
||||
if (typeof style !== 'object') return style
|
||||
export const autoprefixer = function(
|
||||
style: PartialCSSStyleDeclaration,
|
||||
): PartialCSSStyleDeclaration {
|
||||
const rules = ['transform', 'transition', 'animation']
|
||||
const prefixes = ['ms-', 'webkit-']
|
||||
rules.forEach(rule => {
|
||||
|
37
src/utils/resize-event.ts
Normal file
37
src/utils/resize-event.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import ResizeObserver from 'resize-observer-polyfill'
|
||||
|
||||
const isServer = typeof window === 'undefined'
|
||||
|
||||
// TODO: add hack prototype __resizeListeners__
|
||||
|
||||
/* istanbul ignore next */
|
||||
const resizeHandler = function(entries: ResizeObserverEntry[]) {
|
||||
for (const entry of entries) {
|
||||
const listeners = entry.target.__resizeListeners__ || []
|
||||
if (listeners.length) {
|
||||
listeners.forEach(fn => {
|
||||
fn()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* istanbul ignore next */
|
||||
export const addResizeListener = function(element, fn) {
|
||||
if (isServer) return
|
||||
if (!element.__resizeListeners__) {
|
||||
element.__resizeListeners__ = []
|
||||
element.__ro__ = new ResizeObserver(() => {})
|
||||
element.__ro__.observe(element)
|
||||
}
|
||||
element.__resizeListeners__.push(fn)
|
||||
}
|
||||
|
||||
/* istanbul ignore next */
|
||||
export const removeResizeListener = function(element, fn) {
|
||||
if (!element || !element.__resizeListeners__) return
|
||||
element.__resizeListeners__.splice(element.__resizeListeners__.indexOf(fn), 1)
|
||||
if (!element.__resizeListeners__.length) {
|
||||
element.__ro__.disconnect()
|
||||
}
|
||||
}
|
14
src/utils/util.ts
Normal file
14
src/utils/util.ts
Normal file
@ -0,0 +1,14 @@
|
||||
export const autoprefixer = function(style) {
|
||||
if (typeof style !== 'object') return style
|
||||
const rules = ['transform', 'transition', 'animation']
|
||||
const prefixes = ['ms-', 'webkit-']
|
||||
rules.forEach(rule => {
|
||||
const value = style[rule]
|
||||
if (rule && value) {
|
||||
prefixes.forEach(prefix => {
|
||||
style[prefix + rule] = value
|
||||
})
|
||||
}
|
||||
})
|
||||
return style
|
||||
}
|
Loading…
Reference in New Issue
Block a user