vivo-ui
Version:
vivo ui component lib for vue
358 lines (326 loc) • 16.1 kB
JavaScript
import Tween from '../../tools/tween/index';
import { raf, caf } from '../../tools/prefix/index';
import ResizeSensor from '../../vendor/css-element-queries/ResizeSensor';
(function () {
if (typeof document !== 'undefined') {
var head = document.head || document.getElementsByTagName('head')[0],
style = document.createElement('style'),
css = " .vui-picker { -webkit-box-orient: vertical; -webkit-box-direction: normal; -webkit-flex-direction: column; -moz-box-orient: vertical; -moz-box-direction: normal; -ms-flex-direction: column; flex-direction: column; min-height: 4rem; height: 100%; color: #000; font-size: .44rem; line-height: 1rem; text-align: center; background: #fff; } .vui-picker ul { margin: 0; padding: 0; list-style-type: none; } .vui-picker, .vui-picker-labels, .vui-picker-content, .vui-picker-column { display: -webkit-box; display: -webkit-flex; display: -moz-box; display: -ms-flexbox; display: flex; } .vui-picker-labels { -webkit-box-flex: 0; -webkit-flex: none; -moz-box-flex: 0; -ms-flex: none; flex: none; border-bottom: 1px solid #d0d0d0; } .vui-picker-labels > li, .vui-picker-column { -webkit-box-flex: 1; -webkit-flex: auto; -moz-box-flex: 1; -ms-flex: auto; flex: auto; width: 0; } .vui-picker-labels > li { white-space: nowrap; overflow: hidden; -o-text-overflow: ellipsis; text-overflow: ellipsis; } .vui-picker-content { -webkit-box-flex: 1; -webkit-flex: auto; -moz-box-flex: 1; -ms-flex: auto; flex: auto; position: relative; } .vui-picker-content::before { content: \"\"; position: absolute; left: 0; right: 0; top: 50%; -webkit-transform: translateY(-50%); -moz-transform: translateY(-50%); -ms-transform: translateY(-50%); -o-transform: translateY(-50%); transform: translateY(-50%); -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; height: 1rem; border-top: 1px solid #d0d0d0; border-bottom: 1px solid #d0d0d0; } .vui-picker-column { -webkit-box-orient: vertical; -webkit-box-direction: normal; -webkit-flex-direction: column; -moz-box-orient: vertical; -moz-box-direction: normal; -ms-flex-direction: column; flex-direction: column; -webkit-box-pack: center; -webkit-justify-content: center; -moz-box-pack: center; -ms-flex-pack: center; justify-content: center; overflow: hidden; } .vui-picker-column ul { height: 1rem; } .vui-picker-column li { display: -webkit-box; display: -webkit-flex; display: -moz-box; display: -ms-flexbox; display: flex; -webkit-box-orient: vertical; -webkit-box-direction: normal; -webkit-flex-direction: column; -moz-box-orient: vertical; -moz-box-direction: normal; -ms-flex-direction: column; flex-direction: column; -webkit-box-pack: center; -webkit-justify-content: center; -moz-box-pack: center; -ms-flex-pack: center; justify-content: center; height: 100%; } .vui-picker-column li::before { content: attr(data-value); white-space: nowrap; overflow: hidden; -o-text-overflow: ellipsis; text-overflow: ellipsis; } /*3D样式*/ .vui-picker-enable3d .vui-picker-column { -webkit-perspective: 2000px; -moz-perspective: 2000px; perspective: 2000px; } .vui-picker-enable3d .vui-picker-column ul { -webkit-transform-style: preserve-3d; -moz-transform-style: preserve-3d; transform-style: preserve-3d; } .vui-picker-enable3d .vui-picker-column li { float: left; width: 100%; height: 100%; visibility: hidden; -webkit-backface-visibility: hidden; -moz-backface-visibility: hidden; backface-visibility: hidden; } .vui-picker-enable3d .vui-picker-column li:not(:first-child) { margin-left: -100%; } .vui-picker-enable3d .vui-picker-column .vui-picker-visible { visibility: visible; } ";style.type = 'text/css';if (style.styleSheet) {
style.styleSheet.cssText = css;
} else {
style.appendChild(document.createTextNode(css));
}head.appendChild(style);
}
})();
var index = { render: function render() {
var _vm = this;var _h = _vm.$createElement;var _c = _vm._self._c || _h;return _c('div', { staticClass: "vui-picker", class: { 'vui-picker-enable3d': _vm.enable3d } }, [_vm.labels.length ? _c('ul', { staticClass: "vui-picker-labels" }, _vm._l(_vm.labels, function (label) {
return _c('li', [_vm._v(_vm._s(label))]);
})) : _vm._e(), _vm._v(" "), _c('ul', { ref: "content", staticClass: "vui-picker-content", style: _vm.maskStyle, on: { "touchmove": function touchmove($event) {
$event.preventDefault();
} } }, _vm._l(_vm.columns, function (column, index) {
return _c('li', { staticClass: "vui-picker-column", on: { "touchmove": function touchmove($event) {
_vm.touchmove($event, index);
}, "touchend": _vm.touchend } }, [_c('ul', { style: _vm.style[index] }, _vm._l(column, function (item, pos) {
return _c('li', { ref: !index && !pos ? 'item' : undefined, refInFor: true, class: { 'vui-picker-visible': _vm.itemVisible(index, pos) }, style: _vm.itemStyle(pos), attrs: { "data-value": item }, on: { "click": function click($event) {
_vm.location(index, pos);
} } });
}))]);
}))]);
}, staticRenderFns: [],
name: 'vui-picker',
props: {
data: [Array, Object], // 数据
selected: Array, // 各列选中的子项
sync: { // 滑动父列时,是否立即同步更新子列,false表示等父列滑动停下来后再更新子列
type: Boolean,
default: false
},
enable3d: { // 是否开启3d旋转
type: Boolean,
default: false
}
},
data: function data() {
return {
contentHeight: undefined, // 内容区域的高度
itemHeight: undefined, // 各列的每个子项的高度
offsets: [], // 所有列的偏移量,表示当前滑动到第几个子项了,比如0,1,2.5等
positions: [] // 所有列选择的子项的位置索引,比如0,1
};
},
computed: {
style: function style() {
var _this = this;
return this.offsets.map(function (offset) {
if (_this.enable3d) {
return {
transform: 'translate3d(0,0,' + -_this.radius + 'px) rotateX(' + offset * _this.spacing + 'deg)'
};
}
return {
transform: 'translate3d(0,' + -offset * _this.itemHeight + 'px,0)'
};
});
},
maskStyle: function maskStyle() {
// 蒙层样式
var pos = (this.contentHeight - this.itemHeight) / 2 / this.contentHeight * 100;
return {
'mask-box-image': 'linear-gradient(to bottom, rgba(255, 255, 255, .15), rgba(255, 255, 255, .5) ' + pos + '%, #fff ' + pos + '%, #fff ' + (100 - pos) + '%, rgba(255, 255, 255, .5) ' + (100 - pos) + '%, rgba(255, 255, 255, .15))'
};
},
radius: function radius() {
// 3d旋转半径
return this.contentHeight / 2;
},
spacing: function spacing() {
// 3d旋转时每个子项之间的角度间距
return Math.asin(Math.min(this.contentHeight / 2 / this.radius, 1)) / Math.PI * 180 / this.half;
},
half: function half() {
// 一半区域可见子项的数目
return (this.contentHeight / this.itemHeight - 1) / (this.enable3d ? 1.5 : 2);
},
cascaded: function cascaded() {
// 是否级联
return !(this.data instanceof Array);
},
labels: function labels() {
var labels = [];
if (!this.cascaded) {
this.data.forEach(function (column) {
column.name && labels.push(column.name);
});
} else {
var column = this.data;
this.positions.forEach(function (pos) {
column.name && labels.push(column.name);
column = column.options[pos].children;
});
}
return labels;
},
columns: function columns() {
// 各列的数据
if (!this.cascaded) {
return this.data.map(function (column) {
return column.options;
});
} else {
var columns = [];
var column = this.data;
this.positions.forEach(function (pos) {
columns.push(column.options.map(function (option) {
return option.hasOwnProperty('value') ? option.value : option;
}));
column = column.options[pos].children;
});
return columns;
}
}
},
watch: {
data: 'update',
selected: 'update',
positions: function positions(value) {
this.$emit('update', value.slice());
}
},
methods: {
itemVisible: function itemVisible(index, pos) {
// 3d旋转时item是否可见
return this.enable3d && Math.abs(pos - this.offsets[index] - 0.5) <= this.half + 0.5;
},
itemStyle: function itemStyle(pos) {
if (this.enable3d) {
return {
transform: 'rotateX(' + -pos * this.spacing + 'deg) translate3d(0,0,' + this.radius + 'px)'
};
}
},
update: function update() {
var _this2 = this;
this.stop(true);
// 设置positions
if (this.selected !== undefined) {
this.positions = this.selected.slice();
this.offsets = this.selected.slice();
} else {
this.positions = [];
this.offsets = [];
if (!this.cascaded) {
this.data.forEach(function () {
_this2.positions.push(0);
_this2.offsets.push(0);
});
} else {
var column = this.data;
while (column) {
this.positions.push(0);
this.offsets.push(0);
column = column.options[0].children;
}
}
}
},
stop: function stop(_stop) {
// 停止滑动
if (this.scrollingIndex !== undefined) {
var pos = Math.round(this.offsets[this.scrollingIndex]);
var length = this.columns[this.scrollingIndex].length;
pos < 0 && (pos = 0);
pos >= length && (pos = length - 1);
this.updatePosition(this.scrollingIndex, pos, _stop);
caf(this.rafId); // requestAnimationFrame还未执行时就可能stop了
this.scrollingIndex = undefined;
this.touchId = undefined;
}
},
touchmove: function touchmove(event, index) {
var _this3 = this;
if (this.scrollingIndex === undefined) {
// 没有列正在滑动
this.scrollingIndex = index;
this.touchId = event.changedTouches[0].identifier;
this.speed = 0;
this.prevTime = Date.now();
this.prevY = event.changedTouches[0].clientY;
} else {
var touch = [].slice.call(event.changedTouches).filter(function (touch) {
return touch.identifier === _this3.touchId;
})[0];
if (touch) {
// 同一个触点移动
var currentTime = Date.now();
if (currentTime === this.prevTime) {
// 滑得过快时这两个时间可能相等
return;
}
var currentY = touch.clientY;
var offset = (this.prevY - currentY) / this.itemHeight;
var des = this.offsets[this.scrollingIndex] + offset;
// 限制不滑出content区域
if (des > this.columns[this.scrollingIndex].length - (this.enable3d ? 2 : 1) + this.half) {
des = this.columns[this.scrollingIndex].length - (this.enable3d ? 2 : 1) + this.half;
} else if (des < -this.half + (this.enable3d ? 1 : 0)) {
des = -this.half + (this.enable3d ? 1 : 0);
}
var pos = Math.round(des);
this.$set(this.offsets, this.scrollingIndex, des);
// 更新值
if (this.sync && pos > -1 && pos < this.columns[this.scrollingIndex].length && pos !== this.positions[this.scrollingIndex]) {
this.updatePosition(this.scrollingIndex, pos);
}
this.speed = offset * (1000 / (currentTime - this.prevTime));
this.prevTime = currentTime;
this.prevY = currentY;
} else if (this.touchId === undefined && index === this.scrollingIndex) {
// 引起列滑动的触点已经移除了,并且在当前滚动的列上继续滑动
this.stop(); // 停止当前滑动列的滑动
}
}
},
touchend: function touchend(event) {
var _this4 = this;
if ([].slice.call(event.changedTouches).some(function (touch) {
return touch.identifier === _this4.touchId;
})) {
// 同一个触点移除了
var prev = this.offsets[this.scrollingIndex];
var length = this.columns[this.scrollingIndex].length;
var offset = (this.speed > 0 ? 1 : -1) * Math.pow(Math.abs(this.speed), 0.7); // 缓冲变化量
var duration = 40 + Math.min(parseInt(2 * Math.abs(offset)), 40); // 缓冲动画有多少帧
var back = void 0; // 是否回弹
// 获取该变量
switch (true) {
case prev < 0:
offset = -prev;
break;
case prev < length - 1:
if (this.speed < 0 && prev + offset < 0) {
offset = -prev;
back = true;
} else if (this.speed > 0 && offset + prev > length - 1) {
offset = length - 1 - prev;
back = true;
} else {
offset = Math.round(offset + prev) - prev;
}
break;
default:
offset = length - 1 - prev;
}
this.touchId = undefined;
this.buffer(prev, offset, duration, back); // 缓冲
}
},
buffer: function buffer(b, c, duration, back) {
var _this5 = this;
/*
* t: current time(当前时间);
* b: beginning value(初始值);
* c: change in value(变化量);
* d: duration(持续时间);
* back: 是否有回弹
*/
var t = 0;
var scroll = function scroll() {
var offset = (back ? Tween.Back.easeOut : Tween.Cubic.easeOut)(t, b, c, duration);
var pos = Math.round(offset);
_this5.$set(_this5.offsets, _this5.scrollingIndex, offset);
// 更新值
if (!_this5.sync && t === duration || _this5.sync && pos > -1 && pos < _this5.columns[_this5.scrollingIndex].length && pos !== _this5.positions[_this5.scrollingIndex]) {
_this5.updatePosition(_this5.scrollingIndex, pos);
}
t < duration ? _this5.rafId = raf(scroll) : _this5.scrollingIndex = undefined;
t++;
};
this.rafId = raf(scroll);
},
updatePosition: function updatePosition(index, pos, stop) {
// 更新列选择的值
// 如果更新位置时停止滑动
stop && this.$set(this.offsets, index, pos);
if (!this.cascaded) {
this.$set(this.positions, index, pos);
} else {
this.positions.splice(index);
this.positions.push(pos);
this.offsets.splice(index + 1);
var column = this.data;
this.positions.forEach(function (i) {
column = column.options[i].children;
});
while (column) {
this.positions.push(0);
this.offsets.push(0);
column = column.options[0].children;
}
}
},
location: function location(index, pos) {
// 点击了列的某个子项,进行定位
if ([undefined, index].indexOf(this.scrollingIndex) !== -1) {
this.stop();
this.scrollingIndex = index;
var offset = pos - this.offsets[index];
this.buffer(this.offsets[index], offset, 40 + Math.min(parseInt(2 * Math.abs(offset)), 40));
}
},
getSize: function getSize() {
// 这里不能使用offsetHeight,因为该属性始终返回四舍五入的整数,不精确,会导致显示错位
this.itemHeight = this.$refs.item[0].getBoundingClientRect().height;
this.contentHeight = this.$refs.content.getBoundingClientRect().height;
}
},
created: function created() {
// 初始化positions,offsets,并触发update事件
this.update();
},
mounted: function mounted() {
this.getSize();
// 监控尺寸变化
new ResizeSensor(this.$el, this.getSize); // eslint-disable-line no-new
}
};
export default index;