[Improvement] Picker: rewrite (#370)

* [Improvement] Picker code review

* fix: Picker text cases

* fix: Picker watch defaultIndex

* [Improvement] Picker support simple data struct

* [bugfix] Picker defaultIndex out of range
This commit is contained in:
neverland
2017-12-05 13:07:25 +08:00
committed by GitHub
parent b07f55bb51
commit 32801b453b
15 changed files with 540 additions and 702 deletions
+1 -1
View File
@@ -59,6 +59,7 @@ export default {
const columns = [];
const curValue = this.value || '';
const { columnsNum } = this;
columns.push({
values: [DEFAULT_PROVINCE].concat(this.computedAreaList(PROVINCE_TYPE)),
@@ -66,7 +67,6 @@ export default {
defaultIndex: this.getAreaIndex(PROVINCE_TYPE, curValue)
});
const columnsNum = this.columnsNum;
if (+columnsNum > 1) {
columns.push({
values: [DEFAULT_CITY].concat(this.computedAreaList(CITY_TYPE, curValue.slice(0, 2))),
+2 -15
View File
@@ -225,7 +225,7 @@ export default {
},
onChange(picker) {
const values = picker.$children.filter(child => child.currentValue !== undefined).map(child => child.currentValue);
const values = picker.getValues();
let value;
if (this.type === 'time') {
@@ -279,20 +279,7 @@ export default {
if (!this.$refs.picker) {
return;
}
const setColumnValue = this.$refs.picker.setColumnValue;
if (this.type === 'time') {
setColumnValue(0, values[0]);
setColumnValue(1, values[1]);
} else {
setColumnValue(0, values[0]);
setColumnValue(1, values[1]);
setColumnValue(2, values[2]);
if (this.type === 'datetime') {
setColumnValue(3, values[3]);
setColumnValue(4, values[4]);
}
}
[].forEach.call(this.$refs.picker.$children, child => child.doOnValueChange());
this.$refs.picker.setValues(values);
}
},
+127 -194
View File
@@ -1,245 +1,178 @@
<template>
<div class="van-picker-column" :class="classNames">
<div class="van-picker-column-wrapper" :class="{ dragging: isDragging }" ref="wrapper" :style="{ height: visibleContentHeight + 'px' }">
<div
v-for="(item, index) in currentValues"
:key="index"
class="van-picker-column__item"
:class="{ 'van-picker-column__item--selected': item === currentValue }"
:style="{ height: itemHeight + 'px', lineHeight: itemHeight + 'px' }">
{{ typeof item === 'object' && item[valueKey] ? item[valueKey] : item }}
</div>
</div>
<div class="van-picker-column" :class="className">
<div class="van-picker-column__frame van-hairline--top-bottom" :style="frameStyle" />
<ul
:style="wrapperStyle"
@touchstart="onTouchStart"
@touchmove.prevent="onTouchMove"
@touchend="onTouchEnd"
@touchcancel="onTouchEnd"
>
<li
v-for="(option, index) in options"
v-text="getOptionText(option)"
:class="{
'van-picker-column--disabled': isDisabled(option),
'van-picker-column--selected': index === currentIndex
}"
@click="setIndex(index)"
/>
</ul>
</div>
</template>
<script>
import translateUtil from '../utils/transition';
import draggable from './draggable';
const DEFAULT_ITEM_HEIGHT = 44;
const DEFAULT_DURATION = 200;
const range = (num, arr) => Math.min(Math.max(num, arr[0]), arr[1]);
export default {
name: 'van-picker-column',
props: {
/**
* 每一列可见备选元素的个数
*/
valueKey: String,
className: String,
options: {
type: Array,
default: () => []
},
itemHeight: {
type: Number,
default: 44
},
visibileColumnCount: {
type: Number,
default: 5
},
/**
* 该列所有的可选值
*/
values: {
type: Array,
default() {
return [];
}
},
/**
* 每列添加额外的`className`
*/
className: {
type: String,
default: ''
},
/**
* 行高
*/
itemHeight: {
defaultIndex: {
type: Number,
default: DEFAULT_ITEM_HEIGHT
},
value: {},
valueKey: String
default: 0
}
},
data() {
return {
currentValue: this.value,
currentValues: this.values,
isDragging: false
startY: 0,
offset: 0,
duration: 0,
startOffset: 0,
currentIndex: this.defaultIndex
};
},
created() {
this.$parent && this.$parent.children.push(this);
},
mounted() {
this.setIndex(this.currentIndex);
},
destroyed() {
this.$parent && this.$parent.children.splice(this.$parent.children.indexOf(this), 1);
},
watch: {
values(val) {
this.currentValues = val;
defaultIndex() {
this.setIndex(this.defaultIndex);
},
currentValues(val) {
/* istanbul ignore else */
if (this.valueIndex === -1) {
this.currentValue = (val || [])[0];
options(next, prev) {
if (JSON.stringify(next) !== JSON.stringify(prev)) {
this.setIndex(this.defaultIndex);
}
},
value(val) {
this.currentValue = val;
},
currentValue(val) {
this.doOnValueChange();
this.$emit('input', val);
this.$emit('columnChange', this);
currentIndex(index) {
this.$emit('change', index);
}
},
computed: {
/**
* picker可见备选元素总高度
*/
visibleContentHeight() {
return this.itemHeight * this.visibileColumnCount;
count() {
return this.options.length;
},
/**
* 当前选中值在`values`中的索引
*/
valueIndex() {
return this.currentValues.indexOf(this.currentValue);
wrapperStyle() {
const { itemHeight, visibileColumnCount } = this;
return {
transition: `${this.duration}ms`,
transform: `translate3d(0, ${this.offset}px, 0)`,
lineHeight: itemHeight + 'px',
height: itemHeight * visibileColumnCount + 'px',
paddingTop: itemHeight * (visibileColumnCount - 1) / 2 + 'px'
};
},
/**
* 计算picker的拖动范围
*/
dragRange() {
var values = this.currentValues;
var visibileColumnCount = this.visibileColumnCount;
var itemHeight = this.itemHeight;
return [-itemHeight * (values.length - Math.ceil(visibileColumnCount / 2)), itemHeight * Math.floor(visibileColumnCount / 2)];
},
/**
* 计算`classNames`
*/
classNames() {
return this.className.split(' ');
}
},
mounted() {
this.initEvents();
this.doOnValueChange();
},
methods: {
/**
* 将当前`value`值转换成需要垂直方向需要`translate`的值
*/
value2Translate(value) {
const values = this.currentValues;
const valueIndex = values.indexOf(value);
const offset = Math.floor(this.visibileColumnCount / 2);
const itemHeight = this.itemHeight;
if (valueIndex !== -1) {
return (valueIndex - offset) * (-itemHeight);
frameStyle() {
return {
height: this.itemHeight + 'px'
}
},
/**
* 根据当前`translate`的值转换成当前选中的`value`
*/
translate2Value(translate) {
const itemHeight = this.itemHeight;
translate = Math.round(translate / itemHeight) * itemHeight;
currentValue() {
return this.options[this.currentIndex];
}
},
const index = -(translate - Math.floor(this.visibileColumnCount / 2) * itemHeight) / itemHeight;
return this.currentValues[index];
methods: {
onTouchStart(event) {
this.startY = event.touches[0].clientY;
this.startOffset = this.offset;
this.duration = 0;
},
/**
* 初始化拖动事件
*/
initEvents() {
var el = this.$refs.wrapper;
var dragState = {};
onTouchMove(event) {
const deltaY = event.touches[0].clientY - this.startY;
this.offset = range(this.startOffset + deltaY, [
-(this.count * this.itemHeight),
this.itemHeight
]);
},
var velocityTranslate;
var prevTranslate;
var pickerItems; // eslint-disable-line
onTouchEnd() {
if (this.offset !== this.startOffset) {
this.duration = DEFAULT_DURATION;
const index = range(Math.round(-this.offset / this.itemHeight), [
0,
this.count - 1
]);
this.setIndex(index);
}
},
draggable(el, {
start: (event) => {
// 存储当前状态
dragState = {
range: this.dragRange,
start: new Date(),
startLeft: event.pageX,
startTop: event.pageY,
startTranslateTop: translateUtil.getElementTranslate(el).top
};
adjustIndex(index) {
index = range(index, [0, this.count]);
for (let i = index; i < this.count; i++) {
if (!this.isDisabled(this.options[i])) return i;
}
for (let i = index - 1; i >= 0; i--) {
if (!this.isDisabled(this.options[i])) return i;
}
},
pickerItems = el.querySelectorAll('.van-picker-item'); // eslint-disable-line
},
isDisabled(option) {
return typeof option === 'object' && option.disabled;
},
drag: (event) => {
this.isDragging = true;
getOptionText(option) {
return typeof option === 'object' && this.valueKey in option ? option[this.valueKey] : option;
},
dragState.left = event.pageX;
dragState.top = event.pageY;
setIndex(index) {
index = this.adjustIndex(index);
this.offset = -index * this.itemHeight;
this.currentIndex = index;
},
const deltaY = dragState.top - dragState.startTop;
const translate = dragState.startTranslateTop + deltaY;
translateUtil.translateElement(el, null, translate);
velocityTranslate = translate - prevTranslate || translate;
prevTranslate = translate;
},
end: () => {
/* istanbul ignore else */
if (this.isDragging) {
this.isDragging = false;
var momentumRatio = 7;
var currentTranslate = translateUtil.getElementTranslate(el).top;
var duration = new Date() - dragState.start;
var momentumTranslate;
if (duration < 300) {
momentumTranslate = currentTranslate + velocityTranslate * momentumRatio;
}
var dragRange = dragState.range;
this.$nextTick(() => {
var translate;
var itemHeight = this.itemHeight;
if (momentumTranslate) {
translate = Math.round(momentumTranslate / itemHeight) * itemHeight;
} else {
translate = Math.round(currentTranslate / itemHeight) * itemHeight;
}
translate = Math.max(Math.min(translate, dragRange[1]), dragRange[0]);
translateUtil.translateElement(el, null, translate);
this.currentValue = this.translate2Value(translate);
});
}
dragState = {};
setValue(value) {
const { options, valueKey } = this;
for (let i = 0; i < options.length; i++) {
if (this.getOptionText(options[i]) === value) {
this.setIndex(i);
return;
}
});
},
/**
* `value`改变时调用
*/
doOnValueChange() {
const value = this.currentValue;
const wrapper = this.$refs.wrapper;
translateUtil.translateElement(wrapper, null, this.value2Translate(value));
}
}
}
};
-51
View File
@@ -1,51 +0,0 @@
import Vue from 'vue';
let isDragging = false;
const supportTouch = !Vue.prototype.$isServer && 'ontouchstart' in window;
export default function(element, options) {
const moveFn = function(event) {
if (options.drag) {
options.drag(supportTouch ? event.changedTouches[0] || event.touches[0] : event);
}
};
const endFn = function(event) {
if (!supportTouch) {
document.removeEventListener('mousemove', moveFn);
document.removeEventListener('mouseup', endFn);
}
document.onselectstart = null;
document.ondragstart = null;
isDragging = false;
if (options.end) {
options.end(supportTouch ? event.changedTouches[0] || event.touches[0] : event);
}
};
element.addEventListener(supportTouch ? 'touchstart' : 'mousedown', function(event) {
if (isDragging) return;
document.onselectstart = function() { return false; };
document.ondragstart = function() { return false; };
if (!supportTouch) {
document.addEventListener('mousemove', moveFn);
document.addEventListener('mouseup', endFn);
}
isDragging = true;
if (options.start) {
event.preventDefault();
options.start(supportTouch ? event.changedTouches[0] || event.touches[0] : event);
}
});
if (supportTouch) {
element.addEventListener('touchmove', moveFn);
element.addEventListener('touchend', endFn);
element.addEventListener('touchcancel', endFn);
}
};
+94 -102
View File
@@ -1,34 +1,32 @@
<template>
<div class="van-picker">
<div class="van-picker__toolbar van-hairline--top-bottom" v-show="showToolbar">
<div class="van-picker__toolbar van-hairline--top-bottom" v-if="showToolbar">
<slot>
<a href="javascript:void(0)" class="van-picker__cancel" @click="handlePickerCancel">{{ $t('cancel') }}</a>
<a href="javascript:void(0)" class="van-picker__confirm" @click="handlePickerConfirm">{{ $t('confirm') }}</a>
<div v-if="title" class="van-picker__title">{{ title }}</div>
<div class="van-picker__cancel" @click="emit('cancel')">{{ $t('cancel') }}</div>
<div class="van-picker__confirm" @click="emit('confirm')">{{ $t('confirm') }}</div>
<div class="van-picker__title" v-if="title" v-text="title" />
</slot>
</div>
<div class="van-picker__columns" :class="`van-picker__columns--${columns.length}`">
<div class="van-picker__columns">
<van-picker-column
v-for="(item, index) in columns"
v-for="(item, index) in currentColumns"
:key="index"
v-model="values[index]"
:values="item.values"
:className="item.className"
:itemHeight="itemHeight"
:visibleItemCount="visibileColumnCount"
:valueKey="valueKey"
@columnChange="columnValueChange(index)"
:options="item.values"
:className="item.className"
:defaultIndex="item.defaultIndex"
:itemHeight="itemHeight"
:visibileColumnCount="visibileColumnCount"
@change="onChange(index)"
/>
<div class="van-picker-center-highlight" :style="{ height: itemHeight + 'px', marginTop: -itemHeight / 2 + 'px' }"></div>
</div>
</div>
</template>
<script>
import PickerColumn from './PickerColumn';
import { i18n } from '../locale';
const DEFAULT_ITEM_HEIGHT = 44;
import Column from './PickerColumn';
import deepClone from '../utils/deep-clone';
export default {
name: 'van-picker',
@@ -36,132 +34,126 @@ export default {
mixins: [i18n],
components: {
[PickerColumn.name]: PickerColumn
[Column.name]: Column
},
props: {
/**
* 每一列可见备选元素的个数
*/
visibileColumnCount: {
type: Number,
default: 5
title: String,
valueKey: {
type: String,
default: 'text'
},
/**
* 选中元素区高度
*/
itemHeight: {
type: Number,
default: DEFAULT_ITEM_HEIGHT
},
/**
* 对象数组,配置每一列显示的数据
*/
itemHeight: Number,
showToolbar: Boolean,
visibileColumnCount: Number,
columns: {
type: Array,
default() {
return [];
}
default: () => []
},
/**
* 否在组件顶部显示一个toolbar
*/
showToolbar: {
type: Boolean,
default: false
},
/**
* 顶部toolbar 显示的title
*/
title: String,
valueKey: String
},
computed: {
values() {
const columns = this.columns || [];
const values = [];
data() {
return {
children: [],
currentColumns: []
};
},
columns.forEach(column => {
values.push(column.value || column.values[column.defaultIndex || 0]);
});
created() {
this.initColumns();
},
return values;
}
watch: {
columns() {
this.initColumns();
},
},
methods: {
handlePickerCancel() {
this.$emit('cancel', this.values);
},
handlePickerConfirm() {
this.$emit('confirm', this.values);
},
/**
* 处理列`change`事件
*/
columnValueChange(index) {
this.$emit('change', this, this.values, index);
initColumns() {
const columns = this.columns.map(deepClone);
this.isSimpleColumn = columns.length && !columns[0].values;
this.currentColumns = this.isSimpleColumn ? [{ values: columns }] : columns;
},
/**
* 获取对应索引的列的实例
*/
emit(event) {
if (this.isSimpleColumn) {
this.$emit(event, this.getColumnValue(0), this.getColumnIndex(0));
} else {
this.$emit(event, this.getValues(), this.getIndexes());
}
},
onChange(columnIndex) {
if (this.isSimpleColumn) {
this.$emit('change', this, this.getColumnValue(0), this.getColumnIndex(0));
} else {
this.$emit('change', this, this.getValues(), columnIndex);
}
},
// get column instance by index
getColumn(index) {
const children = this.$children.filter(child => child.$options.name === 'van-picker-column');
return children[index];
return this.children[index];
},
/**
* 获取对应列中选中的值
*/
// get column value by index
getColumnValue(index) {
const column = this.getColumn(index);
return column && column.values[column.valueIndex];
return (this.getColumn(index) || {}).currentValue;
},
/**
* 设置对应列中选中的值
*/
// set column value by index
setColumnValue(index, value) {
const column = this.getColumn(index);
if (column) {
column.currentValue = value;
}
column && column.setValue(value);
},
/**
* 获取对应列中所有的备选值
*/
// get column option index by column index
getColumnIndex(columnIndex) {
return (this.getColumn(columnIndex) || {}).currentIndex;
},
// set column option index by column index
setColumnIndex(columnIndex, optionIndex) {
const column = this.getColumn(columnIndex);
column && column.setIndex(optionIndex);
},
// get options of column by index
getColumnValues(index) {
const column = this.getColumn(index);
return column && column.currentValues;
return (this.currentColumns[index] || {}).values;
},
/**
* 设置对应列中所有的备选值
*/
setColumnValues(index, values) {
const column = this.getColumn(index);
// set options of column by index
setColumnValues(index, options) {
const column = this.currentColumns[index];
if (column) {
column.currentValues = values;
column.values = options;
}
},
/**
* 获取所有列中被选中的值,返回一个数组
*/
// get values of all columns
getValues() {
return this.values;
return this.children.map(child => child.currentValue);
},
/**
* `values`为一个数组,设置所有列中被选中的值
*/
// set values of all columns
setValues(values) {
values.forEach((value, index) => {
this.setColumnValue(index, value);
});
},
// get indexes of all columns
getIndexes() {
return this.children.map(child => child.currentIndex);
},
// set indexes of all columns
setIndexes(indexes) {
indexes.forEach((optionIndex, columnIndex) => {
this.setColumnIndex(columnIndex, optionIndex);
});
}
}
};
+10
View File
@@ -0,0 +1,10 @@
import deepAssign from './deep-assign';
export default function deepClone(obj) {
if (Array.isArray(obj)) {
return obj.map(item => deepClone(item));
} else if (typeof obj === 'object') {
return deepAssign({}, obj);
}
return obj;
}
+31 -104
View File
@@ -1,7 +1,9 @@
@import './common/var.css';
@import './mixins/ellipsis.css';
.van-picker {
overflow: hidden;
user-select: none;
background-color: $white;
&__toolbar {
@@ -12,6 +14,7 @@
&__cancel,
&__confirm {
color: $blue;
padding: 0 15px;
&:active {
@@ -20,132 +23,56 @@
}
&__cancel {
color: $blue;
float: left;
}
&__confirm {
color: $blue;
float: right;
}
&__title {
height: 40px;
padding: 0 10px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
word-wrap: normal;
word-break: break-all;
text-align: center;
@mixin ellipsis;
}
&__columns {
display: flex;
position: relative;
overflow: hidden;
&--1 {
.van-picker-column {
width: 100%;
}
}
&--2 {
.van-picker-column {
width: 50%;
}
}
&--3 {
.van-picker-column {
width: 33.333%;
}
}
&--4 {
.van-picker-column {
width: 25%;
}
}
&--5 {
.van-picker-column {
width: 20%;
}
}
}
.van-picker-center-highlight {
box-sizing: border-box;
position: absolute;
left: 0;
width: 100%;
top: 50%;
margin-top: -18px;
pointer-events: none;
}
.van-picker-center-highlight:before,
.van-picker-center-highlight:after {
content: '';
position: absolute;
height: 1px;
width: 100%;
background-color: #eaeaea;
display: block;
transform: scaleY(0.5);
}
.van-picker-center-highlight:before {
left: 0;
top: 0;
bottom: auto;
right: auto;
}
.van-picker-center-highlight:after {
left: 0;
bottom: 0;
right: auto;
top: auto;
}
&-column {
font-size: 18px;
flex: 1;
overflow: hidden;
font-size: 18px;
position: relative;
max-height: 100%;
float: left;
text-align: center;
&__item {
height: 44px;
line-height: 44px;
padding: 0 10px;
white-space: nowrap;
position: relative;
overflow: hidden;
text-overflow: ellipsis;
color: #707274;
left: 0;
top: 0;
width: 100%;
ul {
box-sizing: border-box;
transition-duration: .3s;
}
&--selected {
color: $black;
}
li {
padding: 0 10px;
color: $gray-darker;
@mixin ellipsis;
}
li&--selected {
color: $black;
}
li&--disabled {
opacity: .3;
}
&__frame {
top: 50%;
left: 0;
width: 100%;
z-index: 1;
position: absolute;
pointer-events: none;
transform: translateY(-50%);
}
}
.picker-column-wrapper {
transition-duration: 0.3s;
transition-timing-function: ease-out;
}
.picker-column-wrapper.dragging,
.picker-column-wrapper.dragging .picker-item {
transition-duration: 0s;
}
}