|
|
<template>
|
|
|
<!--注意阻止横向滑动的穿透:横向移动时阻止冒泡-->
|
|
|
<view
|
|
|
:class="`wd-swipe-action ${customClass}`"
|
|
|
@click.stop="onClick()"
|
|
|
@touchmove="stopPropagation ? nothing : ''"
|
|
|
@touchstart="startDrag"
|
|
|
@touchmove.prevent="onDrag"
|
|
|
@touchend="endDrag"
|
|
|
@touchcancel="endDrag"
|
|
|
>
|
|
|
<!--容器-->
|
|
|
<view class="wd-swipe-action__wrapper" :style="wrapperStyle">
|
|
|
<!--左侧操作-->
|
|
|
<view class="wd-swipe-action__left" @click="onClick('left')">
|
|
|
<slot name="left" />
|
|
|
</view>
|
|
|
<!--内容-->
|
|
|
<slot />
|
|
|
<!--右侧操作-->
|
|
|
<view class="wd-swipe-action__right" @click="onClick('right')">
|
|
|
<slot name="right" />
|
|
|
</view>
|
|
|
</view>
|
|
|
</view>
|
|
|
</template>
|
|
|
<script lang="ts">
|
|
|
export default {
|
|
|
name: 'wd-swipe-action',
|
|
|
options: {
|
|
|
addGlobalClass: true,
|
|
|
virtualHost: true,
|
|
|
styleIsolation: 'shared'
|
|
|
}
|
|
|
}
|
|
|
</script>
|
|
|
<script lang="ts" setup>
|
|
|
import { getCurrentInstance, inject, onBeforeMount, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
|
|
import { closeOther, pushToQueue, removeFromQueue } from '../common/clickoutside'
|
|
|
import { type Queue, queueKey } from '../composables/useQueue'
|
|
|
import { useTouch } from '../composables/useTouch'
|
|
|
import { getRect } from '../common/util'
|
|
|
|
|
|
interface Props {
|
|
|
customClass?: string
|
|
|
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
|
beforeClose?: Function
|
|
|
disabled?: boolean
|
|
|
modelValue?: string
|
|
|
}
|
|
|
|
|
|
const props = withDefaults(defineProps<Props>(), {
|
|
|
customStyle: '',
|
|
|
modelValue: 'close',
|
|
|
disabled: false
|
|
|
})
|
|
|
|
|
|
const queue = inject<Queue | null>(queueKey, null)
|
|
|
|
|
|
const wrapperStyle = ref<string>('')
|
|
|
const stopPropagation = ref<boolean>(false)
|
|
|
|
|
|
// 滑动开始时,wrapper的偏移量
|
|
|
const originOffset = ref<number>(0)
|
|
|
// wrapper现在的偏移量
|
|
|
const wrapperOffset = ref<number>(0)
|
|
|
// 是否处于滑动状态
|
|
|
const touching = ref<boolean>(false)
|
|
|
|
|
|
const touch = useTouch()
|
|
|
|
|
|
const { proxy } = getCurrentInstance() as any
|
|
|
|
|
|
watch(
|
|
|
() => props.modelValue,
|
|
|
(value, old) => {
|
|
|
changeState(value, old)
|
|
|
},
|
|
|
{
|
|
|
deep: true
|
|
|
}
|
|
|
)
|
|
|
|
|
|
onBeforeMount(() => {
|
|
|
if (queue && queue.pushToQueue) {
|
|
|
queue.pushToQueue(proxy)
|
|
|
} else {
|
|
|
pushToQueue(proxy)
|
|
|
}
|
|
|
// 滑动开始时,wrapper的偏移量
|
|
|
originOffset.value = 0
|
|
|
// wrapper现在的偏移量
|
|
|
wrapperOffset.value = 0
|
|
|
// 是否处于滑动状态
|
|
|
touching.value = false
|
|
|
})
|
|
|
|
|
|
onMounted(() => {
|
|
|
touching.value = true
|
|
|
changeState(props.modelValue)
|
|
|
touching.value = false
|
|
|
})
|
|
|
|
|
|
onBeforeUnmount(() => {
|
|
|
if (queue && queue.removeFromQueue) {
|
|
|
queue.removeFromQueue(proxy)
|
|
|
} else {
|
|
|
removeFromQueue(proxy)
|
|
|
}
|
|
|
})
|
|
|
|
|
|
const emit = defineEmits(['click', 'update:modelValue'])
|
|
|
|
|
|
function changeState(value: string, old?: string) {
|
|
|
if (props.disabled) {
|
|
|
return
|
|
|
}
|
|
|
getWidths().then(([leftWidth, rightWidth]) => {
|
|
|
switch (value) {
|
|
|
case 'close':
|
|
|
// 调用此函数时,偏移量本就是0
|
|
|
if (wrapperOffset.value === 0) return
|
|
|
close('value', old)
|
|
|
break
|
|
|
case 'left':
|
|
|
swipeMove(leftWidth)
|
|
|
break
|
|
|
case 'right':
|
|
|
swipeMove(-rightWidth)
|
|
|
break
|
|
|
}
|
|
|
})
|
|
|
}
|
|
|
/** 防穿透函数的占位符 **/
|
|
|
function nothing() {}
|
|
|
/**
|
|
|
* @description 获取左/右操作按钮的宽度
|
|
|
* @return {Promise<[Number, Number]>} 左宽度、右宽度
|
|
|
*/
|
|
|
function getWidths() {
|
|
|
return Promise.all([
|
|
|
getRect('.wd-swipe-action__left', false, proxy).then((rects: any) => {
|
|
|
return rects.width ? rects.width : 0
|
|
|
}),
|
|
|
getRect('.wd-swipe-action__right', false, proxy).then((rects: any) => {
|
|
|
return rects.width ? rects.width : 0
|
|
|
})
|
|
|
])
|
|
|
}
|
|
|
/**
|
|
|
* @description wrapper滑动函数
|
|
|
* @param {Number} offset 滑动漂移量
|
|
|
*/
|
|
|
function swipeMove(offset = 0) {
|
|
|
// this.offset = offset
|
|
|
const transform = `translate3d(${offset}px, 0, 0)`
|
|
|
// 跟随手指滑动,不需要动画
|
|
|
const transition = touching.value ? 'none' : '.6s cubic-bezier(0.18, 0.89, 0.32, 1)'
|
|
|
wrapperStyle.value = `
|
|
|
-webkit-transform: ${transform};
|
|
|
-webkit-transition: ${transition};
|
|
|
transform: ${transform};
|
|
|
transition: ${transition};
|
|
|
`
|
|
|
// 记录容器当前偏移的量
|
|
|
wrapperOffset.value = offset
|
|
|
}
|
|
|
/**
|
|
|
* @description click的handler
|
|
|
* @param event
|
|
|
*/
|
|
|
function onClick(position?: string) {
|
|
|
if (props.disabled || wrapperOffset.value === 0) {
|
|
|
return
|
|
|
}
|
|
|
|
|
|
position = position || 'inside'
|
|
|
close('click', position)
|
|
|
emit('click', {
|
|
|
value: position
|
|
|
})
|
|
|
}
|
|
|
/**
|
|
|
* @description 开始滑动
|
|
|
*/
|
|
|
function startDrag(event) {
|
|
|
if (props.disabled) return
|
|
|
|
|
|
originOffset.value = wrapperOffset.value
|
|
|
touch.touchStart(event)
|
|
|
if (queue && queue.closeOther) {
|
|
|
queue.closeOther(proxy)
|
|
|
} else {
|
|
|
closeOther(proxy)
|
|
|
}
|
|
|
}
|
|
|
/**
|
|
|
* @description 滑动时,逐渐展示按钮
|
|
|
* @param event
|
|
|
*/
|
|
|
function onDrag(event) {
|
|
|
if (props.disabled) return
|
|
|
|
|
|
touch.touchMove(event)
|
|
|
|
|
|
if (touch.direction.value === 'vertical') {
|
|
|
stopPropagation.value = false
|
|
|
return
|
|
|
} else {
|
|
|
stopPropagation.value = true
|
|
|
}
|
|
|
|
|
|
touching.value = true
|
|
|
|
|
|
// 本次滑动,wrapper应该设置的偏移量
|
|
|
const offset = originOffset.value + touch.deltaX.value
|
|
|
getWidths().then(([leftWidth, rightWidth]) => {
|
|
|
// 如果需要想滑出来的按钮不存在,对应的按钮肯定滑不出来,容器处于初始状态。此时需要模拟一下位于此处的start事件。
|
|
|
if ((leftWidth === 0 && offset > 0) || (rightWidth === 0 && offset < 0)) {
|
|
|
swipeMove(0)
|
|
|
return startDrag(event)
|
|
|
}
|
|
|
// 按钮已经展示完了,再滑动没有任何意义,相当于滑动结束。此时需要模拟一下位于此处的start事件。
|
|
|
if (leftWidth !== 0 && offset >= leftWidth) {
|
|
|
swipeMove(leftWidth)
|
|
|
return startDrag(event)
|
|
|
} else if (rightWidth !== 0 && -offset >= rightWidth) {
|
|
|
swipeMove(-rightWidth)
|
|
|
return startDrag(event)
|
|
|
}
|
|
|
swipeMove(offset)
|
|
|
})
|
|
|
}
|
|
|
/**
|
|
|
* @description 滑动结束,自动修正位置
|
|
|
*/
|
|
|
function endDrag() {
|
|
|
if (props.disabled) return
|
|
|
// 滑出"操作按钮"的阈值
|
|
|
const THRESHOLD = 0.3
|
|
|
stopPropagation.value = false
|
|
|
touching.value = false
|
|
|
|
|
|
getWidths().then(([leftWidth, rightWidth]) => {
|
|
|
if (
|
|
|
originOffset.value < 0 && // 之前展示的是右按钮
|
|
|
wrapperOffset.value < 0 && // 目前仍然是右按钮
|
|
|
wrapperOffset.value - originOffset.value < rightWidth * THRESHOLD // 并且滑动的范围不超过右边框阀值
|
|
|
) {
|
|
|
swipeMove(-rightWidth) // 回归右按钮
|
|
|
emit('update:modelValue', 'right')
|
|
|
} else if (
|
|
|
originOffset.value > 0 && // 之前展示的是左按钮
|
|
|
wrapperOffset.value > 0 && // 现在仍然是左按钮
|
|
|
originOffset.value - wrapperOffset.value < leftWidth * THRESHOLD // 并且滑动的范围不超过左按钮阀值
|
|
|
) {
|
|
|
swipeMove(leftWidth) // 回归左按钮
|
|
|
emit('update:modelValue', 'left')
|
|
|
} else if (
|
|
|
rightWidth > 0 &&
|
|
|
originOffset.value >= 0 && // 之前是初始状态或者展示左按钮显
|
|
|
wrapperOffset.value < 0 && // 现在展示右按钮
|
|
|
Math.abs(wrapperOffset.value) > rightWidth * THRESHOLD // 视图中已经展示的右按钮长度超过阀值
|
|
|
) {
|
|
|
swipeMove(-rightWidth)
|
|
|
emit('update:modelValue', 'right')
|
|
|
} else if (
|
|
|
leftWidth > 0 &&
|
|
|
originOffset.value <= 0 && // 之前初始状态或者右按钮显示
|
|
|
wrapperOffset.value > 0 && // 现在左按钮
|
|
|
Math.abs(wrapperOffset.value) > leftWidth * THRESHOLD // 视图中已经展示的左按钮长度超过阀值
|
|
|
) {
|
|
|
swipeMove(leftWidth)
|
|
|
emit('update:modelValue', 'left')
|
|
|
} else {
|
|
|
// 回归初始状态
|
|
|
close('swipe')
|
|
|
}
|
|
|
})
|
|
|
}
|
|
|
/**
|
|
|
* @description 关闭操过按钮,并在合适的时候调用 beforeClose
|
|
|
*/
|
|
|
function close(reason, position?: string) {
|
|
|
if (reason === 'swipe' && originOffset.value === 0) {
|
|
|
// offset:0 ——> offset:0
|
|
|
return swipeMove(0)
|
|
|
} else if (reason === 'swipe' && originOffset.value > 0) {
|
|
|
// offset > 0 ——> offset:0
|
|
|
position = 'left'
|
|
|
} else if (reason === 'swipe' && originOffset.value < 0) {
|
|
|
// offset < 0 ——> offset:0
|
|
|
position = 'right'
|
|
|
}
|
|
|
|
|
|
if (reason && position) {
|
|
|
props.beforeClose && props.beforeClose(reason, position)
|
|
|
}
|
|
|
|
|
|
swipeMove(0)
|
|
|
if (props.modelValue !== 'close') {
|
|
|
emit('update:modelValue', 'close')
|
|
|
}
|
|
|
}
|
|
|
|
|
|
defineExpose({ close })
|
|
|
</script>
|
|
|
<style lang="scss" scoped>
|
|
|
@import './index.scss';
|
|
|
</style>
|