class Picker {
DEFAULT_DURATION = 200;
MIN_DISTANCE = 10;
DEMO_DATA = [];
// demo数据
// 惯性滑动思路:
// 在手指离开屏幕时,如果和上一次 move 时的间隔小于 `MOMENTUM_LIMIT_TIME` 且 move
// 距离大于 `MOMENTUM_LIMIT_DISTANCE` 时,执行惯性滑动
MOMENTUM_LIMIT_TIME = 30;
MOMENTUM_LIMIT_DISTANCE = 15;
supportsPassive = false;
constructor(options = {}) {
this.initValue(options);
this._initValue(options);
this.resetTouchStatus();
this.initComputed(options);
this.setEleStyle();
this.onMounted();
}
// 私有变量
_initValue(options) {
this.offset = 0;
this.duration = 0;
this.options = this.initOptions;
this.direction = options.direction || "vertical";
this.deltaX = 0;
this.deltaY = 0;
this.offsetX = 0;
this.offsetY = 0;
this.startX = 0;
this.startY = 0;
this.moving = false;
this.startOffset = 0;
this.transitionEndTrigger = null; // 滚动函数
this.touchStartTime = 0; // 记录开始滑动时间
this.momentumOffset = 0; // 记录开始滑动位置
this.currentIndex = this.defaultIndex;
}
// 初始化--用户变量
initValue(options) {
// 可是区域子元素个数
this.visibleItemCount = Number(options.visibleItemCount || 3) || 3;
// 子元素高度
this.itemPxHeight = Number(this.itemPxHeight) || 105;
// 初始化传入的数据列表(当前案例微用到,可结合框架使用)
this.initOptions = options.initOptions || this.DEMO_DATA;
// 是否只读
this.readonly = options.readonly || false;
// 初始显示元素(当前案例未使用,可结合框架扩展)
this.defaultIndex = Number(options.defaultIndex) || 0;
}
// 根据传入变量--获取计算属性
initComputed(options) {
// 外层容器高度
this.wrapHeight = this.itemPxHeight * this.visibleItemCount;
this.maskStyle = {
backgroundSize: `100% ${(this.wrapHeight - this.itemPxHeight) / 2}px`,
};
this.frameStyle = { height: `${this.itemPxHeight}px` };
// this.count = this.options.length
this.count = document.querySelector(
".bo-wrapper-container"
).children.length;
this.baseOffset = (this.itemPxHeight * (this.visibleItemCount - 1)) / 2;
// 内层元素高度计算
this.wrapperStyle = {
transform: `translate3d(0, ${this.offset + this.baseOffset}px, 0)`,
transitionDuration: `${this.duration}ms`,
transitionProperty: this.duration ? "all" : "none",
};
}
// 设置外部容器的样式及遮罩层
setEleStyle() {
let mask = document.querySelector(".bo-mask");
let coverBorder = document.querySelector(".bo-cover-border");
let columnItem = document.querySelectorAll(".bo-column-item");
mask.style.backgroundSize = this.maskStyle.backgroundSize;
coverBorder.style.height = this.frameStyle.height;
this.setUlStyle();
this.setColumnHeight(columnItem);
}
// 滑动主要逻辑--动态设置容器的垂直方向偏移量
setUlStyle() {
let wrapperContainer = document.querySelector(".bo-wrapper-container");
wrapperContainer.style.transform = this.wrapperStyle.transform;
wrapperContainer.style.transitionDuration =
this.wrapperStyle.transitionDuration;
wrapperContainer.style.transitionProperty =
this.wrapperStyle.transitionProperty;
}
setUlTransform() {
this.initComputed();
this.setUlStyle();
}
// 设置每个行元素的高度及点击事件
setColumnHeight(columnItem) {
columnItem.forEach((item, index) => {
item.style.height = `${this.itemPxHeight}px`;
item.tabindex = index;
item.onclick = () => {
this.onClickItem(index);
this.setUlTransform();
};
});
}
// 点击单个行元素
onClickItem(index) {
if (this.moving || this.readonly) {
return;
}
this.transitionEndTrigger = null;
this.duration = this.DEFAULT_DURATION;
this.setIndex(index, true);
}
// 初始化完成--执行事件绑定
onMounted() {
let el = document.querySelector(".bo-picker-column");
this.bindTouchEvent(el);
this.bindMouseScrollEvent(el); // 添加鼠标滚轮事件
}
bindTouchEvent(el) {
const { onTouchStart, onTouchMove, onTouchEnd, onTransitionEnd } = this;
let wrapper = document.querySelector(".bo-wrapper-container");
this.on(el, "touchstart", onTouchStart);
this.on(el, "touchmove", onTouchMove);
this.on(wrapper, "transitionend", onTransitionEnd);
if (onTouchEnd) {
this.on(el, "touchend", onTouchEnd);
this.on(el, "touchcancel", onTouchEnd);
}
}
on(target, event, handler, passive = false) {
target.addEventListener(
event,
handler,
this.supportsPassive ? { capture: false, passive } : false
);
}
// 动画结束事件
onTransitionEnd = () => {
this.stopMomentum();
};
// 滑动结束后数据获取及优化处理
stopMomentum() {
this.moving = false;
this.duration = 0;
if (this.transitionEndTrigger) {
this.transitionEndTrigger();
this.transitionEndTrigger = null;
}
}
// 开始滑动
onTouchStart = (event) => {
// 控制只读
if (this.readonly) return;
let wrapper = document.querySelector(".bo-wrapper-container");
this.touchStart(event);
if (this.moving) {
const translateY = this.getElementTranslateY(wrapper);
this.offset = Math.min(0, translateY - this.baseOffset);
this.startOffset = this.offset;
} else {
this.startOffset = this.offset;
}
this.duration = 0;
this.transitionEndTrigger = null;
this.touchStartTime = Date.now();
this.momentumOffset = this.startOffset;
// 设置滑动
this.setUlTransform();
};
touchStart(event) {
this.resetTouchStatus();
this.startX = event.touches[0].clientX;
this.startY = event.touches[0].clientY;
}
// 重置滑动数据变量
resetTouchStatus() {
this.direction = "";
this.deltaX = 0;
this.deltaY = 0;
this.offsetX = 0;
this.offsetY = 0;
}
// 动态获取元素滑动距离--关键
getElementTranslateY(element) {
const style = window.getComputedStyle(element);
const transform = style.transform || style.webkitTransform;
const translateY = transform.slice(7, transform.length - 1).split(", ")[5];
return Number(translateY);
}
onTouchMove = (event) => {
if (this.readonly) return;
this.touchMove(event);
if (this.direction === "vertical") {
this.moving = true;
this.preventDefault(event, true);
}
this.offset = this.range(
this.startOffset + this.deltaY,
-(this.count * this.itemPxHeight),
this.itemPxHeight
);
const now = Date.now();
if (now - this.touchStartTime > this.MOMENTUM_LIMIT_TIME) {
this.touchStartTime = now;
this.momentumOffset = this.offset;
}
// 滑动中
this.setUlTransform();
};
onTouchEnd = (event) => {
if (this.readonly) return;
const distance = this.offset - this.momentumOffset;
const duration = Date.now() - this.touchStartTime;
const allowMomentum =
duration < this.MOMENTUM_LIMIT_TIME &&
Math.abs(distance) > this.MOMENTUM_LIMIT_DISTANCE;
if (allowMomentum) {
this.momentum(distance, duration);
return;
}
const index = this.getIndexByOffset(this.offset);
this.duration = this.DEFAULT_DURATION;
this.setIndex(index, true);
// 滑动结束
this.setUlTransform();
// compatible with desktop scenario
// use setTimeout to skip the click event triggered after touchstart
setTimeout(() => {
this.moving = false;
}, 0);
};
// 滑动动画函数--关键
momentum(distance, duration) {
const speed = Math.abs(distance / duration);
distance = this.offset + (speed / 0.003) * (distance < 0 ? -1 : 1);
const index = this.getIndexByOffset(distance);
this.duration = +this.swipeDuration;
this.setIndex(index, true);
}
// 获取当前展示的元素数据信息--关键
setIndex(index, emitChange) {
index = this.adjustIndex(index) || 0;
const offset = -index * this.itemPxHeight;
const trigger = () => {
if (index !== this.currentIndex) {
this.currentIndex = index;
if (emitChange) {
// this.$emit('change', index);
console.log(index);
}
}
};
// trigger the change event after transitionend when moving
if (this.moving && offset !== this.offset) {
this.transitionEndTrigger = trigger;
} else {
trigger();
}
this.offset = offset;
}
getValue() {
return this.options[this.currentIndex];
}
adjustIndex(index) {
index = this.range(index, 0, this.count);
for (let i = index; i < this.count; i++) {
if (!this.isOptionDisabled(this.options[i])) return i;
}
for (let i = index - 1; i >= 0; i--) {
if (!this.isOptionDisabled(this.options[i])) return i;
}
}
isOptionDisabled(option) {
return this.isObject(option) && option.disabled;
}
isObject(val) {
return val !== null && typeof val === "object";
}
// 滑动偏移量
getIndexByOffset(offset) {
return this.range(
Math.round(-offset / this.itemPxHeight),
0,
this.count - 1
);
}
// 阻止默认行为
preventDefault(event, isStopPropagation) {
/* istanbul ignore else */
if (typeof event.cancelable !== "boolean" || event.cancelable) {
event.preventDefault();
}
if (isStopPropagation) {
this.stopPropagation(event);
}
}
stopPropagation(event) {
event.stopPropagation();
}
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);
}
// 确定滑动方向
getDirection(x, y) {
if (x > y && x > this.MIN_DISTANCE) {
return "horizontal";
}
if (y > x && y > this.MIN_DISTANCE) {
return "vertical";
}
return "";
}
// 滑动范围限制--关键代码
range(num, min, max) {
return Math.min(Math.max(num, min), max);
}
bindMouseScrollEvent(el) {
let isFirefox = typeof InstallTrigger !== "undefined"; // Firefox 1.0+
let mouseScrollEvent = isFirefox ? "DOMMouseScroll" : "mousewheel"; // 检测是否是火狐浏览器,如果是则使用"DOMMouseScroll"事件
const handler = (event) => {
event.preventDefault(); // 防止默认滚动行为
let delta = isFirefox ? -event.detail : event.wheelDelta; // Firefox 和 其他浏览器滚动值是相反的,修改以保持一致性。
// 使用滚轮的滚动值更新 currentIndex 的值,并在改变选择项后重新渲染视图
this.currentIndex += delta > 0 ? -1 : 1;
this.onClickItem(this.currentIndex);
this.setUlTransform();
};
this.on(el, mouseScrollEvent, handler);
}
}
new Picker()