使用原生JS实现一个滑块切换的效果,根据常用组件库的实现,拆分UI库源码,实现一个简单的滑块切换组件
实现效果如下:
滑动实现原理
- 通过移动端touch事件,获取滑动距离,利用滑动距离转换为css的translateX变换
- 对于滑动中的状态,通过阻止点击,并且监听document的touch事件,用来完成,点击外部关闭滑块
- 关闭事件使用touchstart,touchstart早于click,可避免很多浏览器上的bug(例如 UC),具体原因网上很多答案
关键代码
// 设置滑块轨道距离
setWrapperStyle() {
const wrapperStyle = {
transform: `translate3d(${this.offset}px, 0, 0)`,
transitionDuration: this.dragging ? "0s" : ".6s",
}
this.$wrapper.style.transform = wrapperStyle.transform
this.$wrapper.style.transitionDuration = wrapperStyle.transitionDuration
}
具体代码实现如下
一、html,css 代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>swiper-cell</title>
<style>
.swiper-cell {
width: 100%;
height: 100%;
position: relative;
overflow: hidden;
cursor: grab;
}
::-webkit-scrollbar {
width: 0;
background: transparent;
}
.swiper-wrapper-left {
position: absolute;
left: 0;
height: 100%;
transform: translate3d(-100%, 0, 0);
background: yellowgreen;
}
.swiper-cell-right {
position: absolute;
top: 0;
right: 0;
height: 100%;
transform: translate3d(100%, 0, 0);
background: yellowgreen;
}
.swiper-wrapper-content {
position: relative;
display: flex;
box-sizing: border-box;
width: 100%;
padding: 10px 16px;
overflow: hidden;
color: #323233;
font-size: 14px;
line-height: 24px;
background-color: #aaa;
}
.test-content {
width: 60px;
height: 100%;
line-height: 44px;
text-align: center;
}
</style>
</head>
<body>
<div class="swiper-cell">
<div class="swiper-wrapper">
<div class="swiper-wrapper-left">
<div class="test-content">选择</div>
</div>
<div class="swiper-wrapper-content">
中间内容区域
</div>
<div class="swiper-cell-right">
<div class="test-content">删除</div>
</div>
</div>
</div>
</body>
<script src="./index.js"></script>
</html>
二、JS实现: 将公共方法封装到方法类中,通过继承调用父类方法
// 父类--工具类
class TouchTools {
MIN_DISTANCE = 10 // 滑动距离
THRESHOLD = 0.15 // 滑动比例
supportsPassive = false
// 滑动范围限制
range(num, min, max) {
return Math.min(Math.max(num, min), max);
}
// 公共滑动方法
touchStart(event) {
this.resetTouchStatus()
this.startX = event.touches[0].clientX
this.startY = event.touches[0].clientY
}
touchMove(event) {
const touch = event.touches[0]
this.deltaX = touch.clientX - this.startX
this.deltaY = touch.clientY - this.startY
this.offsetX = Math.abs(this.deltaX)
this.offsetY = Math.abs(this.deltaY)
this.direction = this.direction || this.getDirection(this.offsetX, this.offsetY)
}
resetTouchStatus() {
this.direction = ''
this.deltaX = 0
this.deltaY = 0
this.offsetX = 0
this.offsetY = 0
}
getDirection(x, y) {
if (x > y && x > this.MIN_DISTANCE) {
return 'horizontal';
}
if (y > x && y > this.MIN_DISTANCE) {
return 'vertical';
}
return '';
}
preventDefault = (event, isStopPropagation) => {
if (typeof event.cancelable !== 'boolean' || event.cancelable) {
event.preventDefault();
}
if (isStopPropagation) {
this.stopPropagation(event);
}
}
// 阻止事件冒泡
stopPropagation(event) {
event.stopPropagation();
}
on = (target, event, handler, passive = false) => {
target.addEventListener(
event,
handler,
this.supportsPassive ? { capture: false, passive } : false
)
}
}
// 滑块类
class SwiperCell extends TouchTools {
constructor(option = {}) {
super()
this.userVariable(option)
this._variable()
this.computedStyle()
this.onMounted()
}
// 用户变量
userVariable(option) {
this.disabled = option.disabled || false
this.leftWidth = option.leftWidth || 0
this.rightWidth = option.rightWidth || 0
// 关闭函数回调
this.beforeClose = option.beforeClose || false
this.onClose = option.onClose || false
}
// 私有变量
_variable() {
this.startOffset = 0 // 私有变量
this.offset = 0 // 滑动距离
this.dragging = false // 滑动状态默认false
this.opened = false // 私有变量 打开状态--false
this.lockClick = false // 私有变量 滑动中--false 不允许点击
this.stopPropagationBool = true // 阻止事件冒泡
this.$el = document.querySelector('.swiper-cell') // 容器
this.$wrapper = document.querySelector('.swiper-wrapper')
this.$left = document.querySelector('.swiper-wrapper-left') // 左侧滑块
this.$right = document.querySelector('.swiper-cell-right') // 右侧滑块
}
// 动态计算属性--主要是滑动样式、距离
computedStyle() {
this.setWrapperStyle()
// 左侧滑块宽
this.computedLeftWidth = this.leftWidth || this.getWidthByRef(this.$left)
// 右侧滑块宽
this.computedRightWidth = this.rightWidth || this.getWidthByRef(this.$right)
}
// 设置滑块轨道距离
setWrapperStyle() {
const wrapperStyle = {
transform: `translate3d(${this.offset}px, 0, 0)`,
transitionDuration: this.dragging ? "0s" : ".6s",
}
this.$wrapper.style.transform = wrapperStyle.transform
this.$wrapper.style.transitionDuration = wrapperStyle.transitionDuration
}
// 计算各个滑块距离
getWidthByRef(ref) {
if (ref) {
const rect = ref.getBoundingClientRect()
return rect.width
}
return 0
}
onMounted() {
this.bindTouchEvent(this.$el)
this.bindClick(this.$el, 'cell')
this.bindClick(this.$left, 'left', true)
this.bindClick(this.$right, 'right', true)
this.bindClick(document, 'outside', true)
}
bindClick(el, position = 'outside', bool) {
const { onClick } = this
this.on(el, 'touchstart', (event) => {
this.stopPropagation(event)
onClick(position, bool)
})
}
bindTouchEvent(el) {
const { onTouchStart, onTouchMove, onTouchEnd } = this
this.on(el, 'touchstart', onTouchStart)
this.on(el, 'touchmove', onTouchMove)
if (onTouchEnd) {
this.on(el, 'touchend', onTouchEnd)
this.on(el, 'touchcancel', onTouchEnd)
}
}
onTouchStart = (event) => {
if (this.disabled) {
return
}
this.startOffset = this.offset
this.touchStart(event)
}
onTouchMove = (event) => {
if (this.disabled) {
return
}
this.touchMove(event)
if (this.direction === "horizontal") {
this.dragging = true
this.lockClick = true
const isPrevent = !this.opened || this.deltaX * this.startOffset < 0
if (isPrevent) {
this.preventDefault(event, this.stopPropagationBool)
}
this.offset = this.range(
this.deltaX + this.startOffset,
-this.computedRightWidth,
this.computedLeftWidth
)
this.setWrapperStyle()
}
}
onTouchEnd = () => {
if (this.disabled) {
return
}
if (this.dragging) {
this.toggle(this.offset > 0 ? "left" : "right")
this.dragging = false
this.setWrapperStyle()
setTimeout(() => {
this.lockClick = false
}, 0)
}
}
toggle(direction) {
const offset = Math.abs(this.offset)
const threshold = this.opened ? 1 - this.THRESHOLD : this.THRESHOLD
const { computedLeftWidth, computedRightWidth } = this
if (
computedRightWidth &&
direction === "right" &&
offset > computedRightWidth * threshold
) {
this.open("right")
} else if (
computedLeftWidth &&
direction === "left" &&
offset > computedLeftWidth * threshold
) {
this.open("left")
} else {
this.close()
}
}
close(position) {
this.offset = 0
if (this.opened) {
this.opened = false
console.log('点击关闭位置', position)
}
}
open(position) {
const offset =
position === "left" ? this.computedLeftWidth : -this.computedRightWidth
this.opened = true
this.offset = offset
console.log('点击打开位置', position)
}
// 点击关闭
onClick = (position = 'outside') => {
console.log('点击位置', position)
if (this.opened && !this.lockClick) {
if (this.beforeClose) {
this.beforeClose({ position, instance: this })
} else if (this.onClose) {
console.log(this.onClose, '自定义关闭函数')
this.onClose({ position, instance: this })
} else {
this.close(position)
}
this.setWrapperStyle()
}
}
}
new SwiperCell({
onClose: function ({ position, instance }) {
// 关闭回调
console.log(position, instance)
instance.close()
},
beforeClose: function ({ position, instance }) {
// 关闭前回调
console.log(position, instance)
instance.close()
}
})