Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(components): [carousel] #10188

Merged
merged 2 commits into from Oct 24, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/components/carousel/__tests__/carousel.test.tsx
Expand Up @@ -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))
Expand Down
2 changes: 2 additions & 0 deletions packages/components/carousel/index.ts
Expand Up @@ -12,3 +12,5 @@ export const ElCarouselItem = withNoopInstall(CarouselItem)

export * from './src/carousel'
export * from './src/carousel-item'

export type { CarouselInstance } from './src/instance'
3 changes: 0 additions & 3 deletions packages/components/carousel/src/carousel.ts
@@ -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: {
Expand Down Expand Up @@ -64,5 +63,3 @@ export const carouselEmits = {

export type CarouselProps = ExtractPropTypes<typeof carouselProps>
export type CarouselEmits = typeof carouselEmits

export type CarouselInstance = InstanceType<typeof Carousel>
274 changes: 24 additions & 250 deletions packages/components/carousel/src/carousel.vue
Expand Up @@ -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)]
Expand All @@ -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)) {
Expand All @@ -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,
Expand Down
3 changes: 3 additions & 0 deletions packages/components/carousel/src/instance.ts
@@ -0,0 +1,3 @@
import type Carousel from './carousel.vue'

export type CarouselInstance = InstanceType<typeof Carousel>