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

Feat/swiper navigation #17

Merged
merged 5 commits into from
Mar 15, 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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions src/swiper/demos/base.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<template>
<div class="tdesign-mobile-demo">
<div style="padding: 0 16px">
<t-swiper>
<t-swiper-item class="swiper-item--demo">
<img src="https://tdesign.gtimg.com/site/swiper/01.png" class="img" />
</t-swiper-item>
<t-swiper-item class="swiper-item--demo">
<img src="https://tdesign.gtimg.com/site/swiper/02.png" class="img" />
</t-swiper-item>
<t-swiper-item class="swiper-item--demo">
<img src="https://tdesign.gtimg.com/site/swiper/03.png" class="img" />
</t-swiper-item>
</t-swiper>
</div>
</div>
</template>
<script lang="ts" setup></script>
<style lang="less">
.t-swiper {
margin-bottom: 24px;
}
.swiper-item--demo {
img {
height: 100%;
}
}
</style>
19 changes: 18 additions & 1 deletion src/swiper/demos/mobile.vue
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
</t-swiper-item>
</t-swiper>

<t-swiper :autoplay="false" :height="180" :navigation="{ type: 'fraction' }">
<t-swiper :autoplay="false" :height="180" direction="vertical" :navigation="{ type: 'fraction' }">
<t-swiper-item class="swiper-item--demo1">
<img src="https://tdesign.gtimg.com/site/swiper/01.png" class="img" />
</t-swiper-item>
Expand Down Expand Up @@ -61,6 +61,23 @@
<img src="https://tdesign.gtimg.com/site/swiper/03.png" class="img" />
</t-swiper-item>
</t-swiper>
<!-- <t-swiper :interval="3000" :autoplay="true">
<t-swiper-item class="swiper-item--demo">
<img src="https://tdesign.gtimg.com/site/swiper/01.png" class="img" />
</t-swiper-item>
<t-swiper-item class="swiper-item--demo">
<img src="https://tdesign.gtimg.com/site/swiper/02.png" class="img" />
</t-swiper-item>
<t-swiper-item class="swiper-item--demo">
<img src="https://tdesign.gtimg.com/site/swiper/03.png" class="img" />
</t-swiper-item>
<template #navigation>
<span></span>
<span></span>
<span></span>
<span></span>
</template>
</t-swiper> -->
</div>
</tdesign-demo-block>
</div>
Expand Down
5 changes: 3 additions & 2 deletions src/swiper/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ export default {
type: String as PropType<TdSwiperProps['animation']>,
default: 'slide' as TdSwiperProps['animation'],
validator(val: TdSwiperProps['animation']): boolean {
return ['slide'].includes(val!);
if (!val) return true;
return ['slide'].includes(`${val}`);
},
},
/** 是否自动播放 */
Expand All @@ -40,7 +41,7 @@ export default {
type: String as PropType<TdSwiperProps['direction']>,
default: 'horizontal' as TdSwiperProps['direction'],
validator(val: TdSwiperProps['direction']): boolean {
return ['horizontal', 'vertical'].includes(val!);
return ['horizontal', 'vertical'].includes(`${val}`);
},
},
/** 滑动动画时长 */
Expand Down
112 changes: 45 additions & 67 deletions src/swiper/swiper.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,10 @@
flexDirection: direction === 'horizontal' ? 'row' : 'column',
}"
@transitionend="handleAnimationEnd"
@touchstart="onTouchStart"
@touchmove.prevent="onTouchMove"
@touchend="onTouchEnd"
@touchcancel="onTouchEnd"
>
<slot></slot>
</div>
<template v-if="navigation && 'type' in navigation">
<!-- 左右侧的按钮 -->
<template v-if="navigation">
<span v-if="direction === 'horizontal' && navigation.showSlideBtn">
<span :class="`${name}__btn btn-prev`" @click="prev(1)">
<chevron-left-icon size="20px" />
Expand All @@ -25,28 +20,31 @@
<chevron-right-icon size="20px" />
</span>
</span>
<!-- 分页器 -->
<span v-if="navigation.type" :class="`${name}__pagination ${name}__pagination-${navigation.type}`">
<span v-if="'type' in navigation" :class="`${name}__pagination ${name}__pagination-${navigation.type || ''}`">
<template v-if="['dots', 'dots-bar'].includes(navigation.type)">
<span
v-for="(item, index) in paginationList"
:key="'page' + index"
:class="{ [`${name}-dot`]: true, [`${name}-dot--active`]: index === state.activeIndex }"
></span>
</template>
<span v-if="navigation.type === 'fraction'">
<span v-if="navigation.type && navigation.type === 'fraction'">
{{ showPageNum + '/' + state.itemLength }}
</span>
</span>
</template>
<template v-if="computedNavigation !== undefined">
<t-node :content="computedNavigation" :style="{}"></t-node>
</template>
</div>
</template>
<script lang="ts">
import { defineComponent, reactive, getCurrentInstance, onMounted, computed, watch, ref, SetupContext } from 'vue';
import { ChevronLeftIcon, ChevronRightIcon } from 'tdesign-icons-vue-next';
import { SwipeDirection, useSwipe } from '@vueuse/core';
import SwiperProps from './props';
import config from '../config';
import { useDefault, useEmitEvent } from '../shared';
import { renderTNode, useDefault, TNode } from '../shared';
import { TdSwiperProps } from './type';

const { prefix } = config;
Expand All @@ -57,14 +55,15 @@ const setOffset = (element: HTMLDivElement, offset: number, direction = 'X'): vo
};
export default defineComponent({
name,
components: { ChevronLeftIcon, ChevronRightIcon },
components: { ChevronLeftIcon, ChevronRightIcon, TNode },
props: SwiperProps,
emits: ['change', 'update:current', 'update:modelValue'],
setup(props, context: SetupContext) {
const emitEvent = useEmitEvent(props, context.emit);
// const emitEvent = useEmitEvent(props, context.emit);
const [swiperValue, setSwiperValue] = useDefault<Number, TdSwiperProps>(props, context.emit, 'current', 'change');
const self = getCurrentInstance();
const swiperContainer = ref(null);
const swiperContainer = ref<HTMLElement | null>(null);
const computedNavigation = computed(() => renderTNode(self, 'navigation'));
// const { height = 180, current = null } = props;
const height = props.height || 180;
const state: {
Expand All @@ -80,33 +79,24 @@ export default defineComponent({
isControl: false,
btnDisabled: false,
});
// 分页数组--任意数组,用于循环分页点
const paginationList = computed(() => new Array(state.itemLength).fill(1));
// 限制的分页值(hike循环播放添加的节点数量)
const showPageNum = computed(() => {
const { activeIndex, itemLength } = state;
if (activeIndex > itemLength - 1) return itemLength;
if (activeIndex < 0) return 1;
return activeIndex + 1;
});
// 获取容器节点(实时获取,才能获取到最新的节点)
const getContainer = (): HTMLDivElement => self?.proxy?.$el.querySelector('.t-swiper__container');
// const getContainer = (): HTMLDivElement => swiperContainer.value as any;
// 初始化轮播图元素
const initSwiper = () => {
const _swiperContainer = getContainer();
const items = _swiperContainer.querySelectorAll('.t-swiper-item');
const first = items[0].cloneNode(true);
const last = items[items.length - 1].cloneNode(true);
// 把第一个元素复制到最后面,以供循环轮播使用
_swiperContainer.appendChild(first);
// 把最后一个元素复制到最前面
_swiperContainer.insertBefore(last, items[0]);
// 默认前移一格(因为前面增加了最后一个元素)
move(0);
};

// 勾子函数初始化部分数据
onMounted(() => {
const _swiperContainer = getContainer();
state.itemLength = _swiperContainer.children?.length || 0;
Expand All @@ -123,70 +113,61 @@ export default defineComponent({
let autoplayTimer: number | NodeJS.Timeout | null = null;
let actionIsTrust = true;
/**
* 移动节点
* move item
*/
const move = (targetIndex: number, isTrust = true) => {
const _swiperContainer = getContainer();
const moveDirection = props.direction === 'horizontal' ? 'X' : 'Y';
const moveLength: number = props.direction === 'vertical' ? height : state.itemWidth;
const moveDirection = props?.direction === 'horizontal' ? 'X' : 'Y';
const moveLength: number = props?.direction === 'vertical' ? height : state.itemWidth;
actionIsTrust = isTrust;
_swiperContainer.dataset.isTrust = `${isTrust}`;
_swiperContainer.style.transform = `translate${moveDirection}(-${moveLength * (targetIndex + 1)}px)`;
};
// 添加动画
const addAnimation = () => {
const _swiperContainer = getContainer();
_swiperContainer.style.transition = `transform ${props.duration}ms`;
_swiperContainer.style.transition = `transform ${props?.duration}ms`;
};
// 移除动画(轮播时用到)
const removeAnimation = () => {
const _swiperContainer = getContainer();
_swiperContainer.style.transition = 'none';
};
// 确认是否已经移动到最后一个元素,每次transitionend事件后即检查
const handleAnimationEnd = () => {
state.btnDisabled = false;
removeAnimation();
if (state.activeIndex >= state.itemLength) {
// console.log('到了最后一个元素', state.activeIndex, state.itemLength);
state.activeIndex = 0;
move(0);
}
if (state.activeIndex <= -1) {
// console.log('到了第一个元素', state.activeIndex, state.itemLength);
state.activeIndex = state.itemLength - 1;
move(state.itemLength - 1);
}
setTimeout(() => {
actionIsTrust && emitCurrentChange(state.activeIndex);
}, 0);
};
// 停止自动播放
const stopAutoplay = () => {
if (!autoplayTimer) return;
clearInterval(autoplayTimer as number);
autoplayTimer = null;
};
// 自动播放
const startAutoplay = () => {
// 如果是受控组件,永远不自动播放
if (typeof props.current === 'number') return false;
if (!props.autoplay || autoplayTimer !== null) return false; // 防止多次创建定时器
if (!props?.autoplay || autoplayTimer !== null) return false; // stop repeat autoplay
autoplayTimer = setInterval(() => {
state.activeIndex += 1;
addAnimation();
move(state.activeIndex);
}, props.interval);
}, props?.interval);
};
// 通知父组件更新页数(受控模式)
const emitCurrentChange = (index: number) => {
if (!state.isControl) return false;
let resultIndex = index;
if (index >= state.itemLength) resultIndex = 0;
if (index < 0) resultIndex = state.itemLength - 1;
emitEvent('change', resultIndex);
// emitEvent('change', resultIndex);
setSwiperValue(resultIndex);
};
// 移动到上一个
const prev = (step = 1) => {
if (state.btnDisabled) return false;
stopAutoplay();
Expand All @@ -196,7 +177,6 @@ export default defineComponent({
startAutoplay();
state.btnDisabled = true;
};
// 移动到下一个
const next = (step = 1) => {
if (state.btnDisabled) return false;
stopAutoplay();
Expand All @@ -206,46 +186,44 @@ export default defineComponent({
startAutoplay();
state.btnDisabled = true;
};
let touchStartX = 0;
let touchStartY = 0;
// 按下鼠标或屏幕开始滑动
const onTouchStart = (event: TouchEvent) => {
stopAutoplay();
touchStartY = event.touches[0].clientY;
touchStartX = event.touches[0].clientX;
};
// 滑动过程中位移容器
const { lengthX, lengthY } = useSwipe(swiperContainer, {
passive: false,
onSwipeStart(e: TouchEvent) {
stopAutoplay();
},
onSwipe(e: TouchEvent) {
onTouchMove(e);
},
onSwipeEnd() {
onTouchEnd();
},
});
const onTouchMove = (event: TouchEvent) => {
event.preventDefault();
const { activeIndex, itemWidth } = state;
const endY = event.changedTouches[0].clientY;
const endX = event.changedTouches[0].clientX;
const distanceX = endX - touchStartX;
const distanceY = endY - touchStartY;
const distanceX = lengthX.value;
const distanceY = lengthY.value;
const _container = getContainer();
removeAnimation();
if (props.direction === 'horizontal') {
setOffset(_container, -((activeIndex + 1) * itemWidth - distanceX));
if (props?.direction === 'horizontal') {
setOffset(_container, -((activeIndex + 1) * itemWidth + distanceX));
} else {
const { height = 180 } = props;
setOffset(_container, -((activeIndex + 1) * height - distanceY), 'Y');
setOffset(_container, -((activeIndex + 1) * height + distanceY), 'Y');
}
};
// 放开手指或者鼠标,停止滑动,判断滑动量,如果不够回到原来的位置,否则按方向移动一个节点。
const onTouchEnd = (event: TouchEvent) => {
const endY = event.changedTouches[0].clientY;
const endX = event.changedTouches[0].clientX;
const distanceX = endX - touchStartX;
const distanceY = endY - touchStartY;
const onTouchEnd = () => {
const distanceX = lengthX.value;
const distanceY = lengthY.value;
addAnimation();
if (
(props.direction === 'horizontal' && distanceX > 100) ||
(props.direction === 'vertical' && distanceY > 100)
(props?.direction === 'horizontal' && distanceX < -100) ||
(props?.direction === 'vertical' && distanceY < -100)
) {
prev(1);
} else if (
(props.direction === 'horizontal' && distanceX < -100) ||
(props.direction === 'vertical' && distanceY < -100)
(props?.direction === 'horizontal' && distanceX > 100) ||
(props?.direction === 'vertical' && distanceY > 100)
) {
next(1);
} else {
Expand All @@ -266,7 +244,7 @@ export default defineComponent({
return {
swiperContainer,
name,
onTouchStart,
computedNavigation,
onTouchMove,
onTouchEnd,
handleAnimationEnd,
Expand Down
Loading