wheel-picker
Version:
仿 iOS UIPickerView 的滚动选择器
343 lines (262 loc) • 10.7 kB
JavaScript
"use strict";
var utils = require("./utils.js");
function Wheel(el, options) {
this.container = typeof el === "string" ? document.querySelector(el) : el;
this.data = [];
this.items = [];
this.y = 0;
this.selectedIndex = 0;
this.isTransition = false;
this.isTouching = false;
this.easings = {
scroll: "cubic-bezier(0.23, 1, 0.32, 1)", // easeOutQuint
scrollBounce: "cubic-bezier(0.25, 0.46, 0.45, 0.94)", // easeOutQuard
bounce: "cubic-bezier(0.165, 0.84, 0.44, 1)" // easeOutQuart
};
this.options = utils.extend({
data: [],
rows: 5,
rowHeight: 34,
adjustTime: 400,
bounceTime: 600,
momentumThresholdTime: 300,
momentumThresholdDistance: 10
}, options);
if (this.options.rows % 2 === 0) this.options.rows++;
this.pointerEvents = "ontouchstart" in window ? {
start: "touchstart",
move: "touchmove",
end: "touchend",
cancel: "touchcancel"
} : {
start: "mousedown",
move: "mousemove",
end: "mouseup",
cancel: "mouseleave"
};
this.transformName = utils.prefixed("transform");
this.transitionName = utils.prefixed("transition");
this.transitionendName = {
WebkitTransition: "webkitTransitionEnd",
MozTransition: "transitionEnd",
msTransition: "MSTransitionEnd",
OTransition: "oTransitionEnd",
transition: "transitionend"
}[this.transitionName];
this._init();
}
Wheel.prototype = {
_init: function() {
this._createDOM();
this._bindEvents();
},
_createDOM: function() {
this.wheel = document.createElement("div");
this.wheel.className = "wheelpicker-wheel";
this.scroller = document.createElement("ul");
this.scroller.className = "wheelpicker-wheel-scroller";
this.setData(this.options.data, this.options.value);
this.wheel.style.height = this.options.rowHeight * this.options.rows + "px";
this.scroller.style.marginTop = this.options.rowHeight * Math.floor(this.options.rows / 2) + "px";
this.wheelHeight = this.wheel.offsetHeight;
this.wheel.appendChild(this.scroller);
this.container.appendChild(this.wheel);
},
_momentum: function(current, start, time, lowerMargin, wheelSize, deceleration, rowHeight) {
var distance = current - start;
var speed = Math.abs(distance) / time;
var destination;
var duration;
deceleration = deceleration === undefined ? 0.0006 : deceleration;
destination = current + (speed * speed) / (2 * deceleration) * (distance < 0 ? -1 : 1);
duration = speed / deceleration;
destination = Math.round(destination / rowHeight) * rowHeight;
if (destination < lowerMargin) {
destination = wheelSize ? lowerMargin - (wheelSize / 2.5 * (speed / 8)) : lowerMargin;
distance = Math.abs(destination - current);
duration = distance / speed;
} else if (destination > 0) {
destination = wheelSize ? wheelSize / 2.5 * (speed / 8) : 0;
distance = Math.abs(current) + destination;
duration = distance / speed;
}
return {
destination: Math.round(destination),
duration: duration
};
},
_resetPosition: function(duration) {
var y = this.y;
duration = duration || 0;
if (y > 0) y = 0;
if (y < this.maxScrollY) y = this.maxScrollY;
if (y === this.y) return false;
this._scrollTo(y, duration, this.easings.bounce);
return true;
},
_getClosestSelectablePosition: function(y) {
var index = Math.abs(Math.round(y / this.options.rowHeight));
if (!this.data[index].disabled) return y;
var max = Math.max(index, this.data.length - index);
for (var i = 1; i <= max; i++) {
if (!this.data[index + i].disabled) {
index += i;
break;
}
if (!this.data[index - i].disabled) {
index -= i;
break;
}
}
return index * -this.options.rowHeight;
},
_scrollTo: function(y, duration, easing) {
if (this.y === y) {
this._scrollFinish();
return false;
}
this.y = this._getClosestSelectablePosition(y);
this.scroller.style[this.transformName] = "translate3d(0," + this.y + "px,0)";
if (duration && duration > 0) {
this.isTransition = true;
this.scroller.style[this.transitionName] = duration + "ms " + easing;
} else {
this._scrollFinish();
}
},
_scrollFinish: function() {
var newIndex = Math.abs(this.y / this.options.rowHeight);
if (this.selectedIndex != newIndex) {
this.items[this.selectedIndex].classList.remove("wheelpicker-item-selected");
this.items[newIndex].classList.add("wheelpicker-item-selected");
this.selectedIndex = newIndex;
if (this.options.onSelect) this.options.onSelect(this.data[newIndex], newIndex);
}
},
_getCurrentY: function() {
var matrixValues = utils.getStyle(this.scroller, this.transformName).match(/-?\d+(\.\d+)?/g);
return parseInt(matrixValues[matrixValues.length - 1]);
},
_start: function(event) {
event.preventDefault();
if (!this.data.length) return;
if (this.isTransition) {
this.isTransition = false;
this.y = this._getCurrentY();
this.scroller.style[this.transformName] = "translate3d(0," + this.y + "px,0)";
this.scroller.style[this.transitionName] = "";
}
this.startY = this.y;
this.lastY = event.touches ? event.touches[0].pageY : event.pageY;
this.startTime = Date.now();
this.isTouching = true;
},
_move: function(event) {
if (!this.isTouching) return false;
var y = event.changedTouches ? event.changedTouches[0].pageY : event.pageY;
var deltaY = y - this.lastY;
var targetY = this.y + deltaY;
var now = Date.now();
this.lastY = y;
if (targetY > 0 || targetY < this.maxScrollY) {
targetY = this.y + deltaY / 3;
}
this.y = Math.round(targetY);
this.scroller.style[this.transformName] = "translate3d(0," + this.y + "px,0)";
if (now - this.startTime > this.momentumThresholdTime) {
this.startTime = now;
this.startY = y;
}
return false;
},
_end: function(event) {
if (!this.isTouching) return false;
var deltaTime = Date.now() - this.startTime;
var duration = this.options.adjustTime;
var easing = this.easings.scroll;
var distanceY = Math.abs(this.y - this.startY);
var momentumVals;
var y;
this.isTouching = false;
if (deltaTime < this.options.momentumThresholdTime && distanceY <= 10 && event.target.classList.contains("wheelpicker-item")) {
this._scrollTo(event.target._wsIdx * -this.options.rowHeight, duration, easing);
return false;
}
if (this._resetPosition(this.options.bounceTime)) return;
if (deltaTime < this.options.momentumThresholdTime && distanceY > this.options.momentumThresholdDistance) {
momentumVals = this._momentum(this.y, this.startY, deltaTime, this.maxScrollY, this.wheelHeight, 0.0007, this.options.rowHeight);
y = momentumVals.destination;
duration = momentumVals.duration;
} else {
y = Math.round(this.y / this.options.rowHeight) * this.options.rowHeight;
}
if (y > 0 || y < this.maxScrollY) {
easing = this.easings.scrollBounce;
}
this._scrollTo(y, duration, easing);
},
_transitionEnd: function() {
this.isTransition = false;
this.scroller.style[this.transitionName] = "";
if (!this._resetPosition(this.options.bounceTime)) this._scrollFinish();
},
_bindEvents: function() {
this.wheel.addEventListener(this.pointerEvents.start, this._start.bind(this));
this.wheel.addEventListener(this.pointerEvents.move, this._move.bind(this));
this.wheel.addEventListener(this.pointerEvents.end, this._end.bind(this));
this.wheel.addEventListener(this.pointerEvents.cancel, this._end.bind(this));
this.scroller.addEventListener(this.transitionendName, this._transitionEnd.bind(this));
},
getData: function() {
return this.data;
},
setData: function(data, value) {
var defaultValue = value || (data && data.length ? (data[0].value || data[0]) : null);
this.items = [];
this.scroller.innerHTML = "";
this.data = data.map(function(item, idx) {
var li = document.createElement("li");
li.className = "wheelpicker-item";
item = typeof item === "object" ? item : {
text: item,
value: item
};
if (item.disabled) li.className += " wheelpicker-item-disabled";
if (item.value === defaultValue) {
li.className += " wheelpicker-item-selected";
this.selectedIndex = idx;
}
li._wsIdx = idx;
li.innerHTML = item.text;
this.items.push(li);
this.scroller.appendChild(li);
return item;
}, this);
this.y = this.selectedIndex * -this.options.rowHeight;
this.scroller.style[this.transformName] = "translate3d(0," + this.y + "px,0)";
this.maxScrollY = -this.options.rowHeight * (this.data.length - 1);
},
getSelectedItem: function() {
return this.data[this.selectedIndex];
},
getValue: function() {
var selected = this.getSelectedItem();
return selected ? selected.value : null;
},
setValue: function(value, noAnimation) {
var index;
var item;
for (var i = 0, len = this.data.length; i < len; i++) {
item = this.data[i];
if (item.value === value) {
if (!item.disabled) index = i;
break;
}
}
if (index >= 0) {
this._scrollTo(index * -this.options.rowHeight, noAnimation ? 0 : this.options.adjustTime, this.easings.scroll);
}
return index;
}
};
module.exports = Wheel;