[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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 540 additions and 702 deletions

View File

@ -5,7 +5,7 @@
</demo-block>
<demo-block :title="$t('title2')">
<van-area :areaList="areaList" value="110101" />
<van-area :areaList="areaList" :value="value" />
</demo-block>
<demo-block :title="$t('title3')">
@ -31,7 +31,8 @@ export default {
data() {
return {
areaList: AreaList
areaList: AreaList,
value: '110101'
};
}
};

View File

@ -1,19 +1,26 @@
<template>
<demo-section>
<demo-block :title="$t('basicUsage')">
<van-picker :columns="columns" @change="onChange" />
<van-picker :columns="$t('column1')" @change="onChange1" />
</demo-block>
<demo-block :title="$t('title2')">
<van-picker :columns="$t('column2')" />
</demo-block>
<demo-block :title="$t('title3')">
<van-picker
showToolbar
:title="$t('area')"
:columns="columns"
@change="onChange"
:columns="$t('column1')"
@cancel="onCancel"
@confirm="onConfirm"
/>
</demo-block>
<demo-block :title="$t('title4')">
<van-picker :columns="columns" @change="onChange2" />
</demo-block>
</demo-section>
</template>
@ -21,26 +28,45 @@
export default {
i18n: {
'zh-CN': {
area: '地区选择',
title2: '带 toolbar 的 Picker',
column: {
'浙江': ['杭州', '宁波', '温州', '嘉兴', '湖州', '绍兴', '金华', '衢州'],
'福建': ['福州', '厦门', '莆田', '三明', '泉州', '漳州', '南平', '龙岩']
}
area: '标题',
title2: '禁用选项',
title3: '展示顶部栏',
title4: '多列联动',
column1: ['杭州', '宁波', '温州', '嘉兴', '湖州'],
column2: [
{ text: '杭州', disabled: true },
{ text: '宁波' },
{ text: '温州' }
],
column3: {
浙江: ['杭州', '宁波', '温州', '嘉兴', '湖州'],
福建: ['福州', '厦门', '莆田', '三明', '泉州']
},
toastContent: (value, index) => `当前值:${value}, 当前索引:${index}`
},
'en-US': {
area: 'Title',
title2: 'Disable Option',
title3: 'Show Toolbar',
title4: 'Multi Columns',
title2: 'Picker with toolbar',
column: {
'Group1': ['Delaware', 'Florida', 'Georqia', 'Indiana', 'Maine'],
'Group2': ['Alabama', 'Kansas', 'Louisiana', 'Texas']
}
column1: ['Delaware', 'Florida', 'Georqia', 'Indiana', 'Maine'],
column2: [
{ text: 'Delaware', disabled: true },
{ text: 'Florida' },
{ text: 'Georqia' }
],
column3: {
Group1: ['Delaware', 'Florida', 'Georqia', 'Indiana', 'Maine'],
Group2: ['Alabama', 'Kansas', 'Louisiana', 'Texas']
},
toastContent: (value, index) => `Value: ${value}, Index${index}`
}
},
computed: {
columns() {
const column = this.$t('column');
const column = this.$t('column3');
return [
{
values: Object.keys(column),
@ -48,27 +74,26 @@ export default {
},
{
values: column[Object.keys(column)[0]],
className: 'column2'
className: 'column2',
defaultIndex: 2
}
];
}
},
methods: {
onChange(picker, values) {
picker.setColumnValues(1, this.$t('column')[values[0]]);
onChange1(picker, value, index) {
Toast(this.$t('toastContent', value, index));
},
onChange2(picker, values) {
picker.setColumnValues(1, this.$t('column3')[values[0]]);
},
onConfirm(value, index) {
Toast(this.$t('toastContent', value, index));
},
onCancel() {
Toast('picker cancel');
},
onConfirm() {
Toast('picker confirm');
Toast(this.$t('cancel'));
}
}
};
</script>
<style lang="postcss">
</style>

View File

@ -11,55 +11,79 @@ Vue.component(Picker.name, Picker);
#### Basic Usage
```html
<van-picker :columns="pickerColumns" @change="onChange" />
<van-picker :columns="columns" @change="onChange" />
```
```javascript
const states = {
'Group1': ['Delaware', 'Florida', 'Georqia', 'Indiana', 'Maine'],
'Group2': ['Alabama', 'Kansas', 'Louisiana', 'Texas']
};
export default {
data() {
return {
pickerColumns: [
{
values: Object.keys(states),
className: 'column1'
},
{
values: states.Group1,
className: 'column2'
}
]
columns: ['Delaware', 'Florida', 'Georqia', 'Indiana', 'Maine']
};
},
methods: {
onChange(picker, values) {
picker.setColumnValues(1, citys[values[0]]);
}
onChange(picker, value, index) {
Toast(`Value: ${value}, Index: ${index}`);
}
};
```
#### Disable option
#### Picker with toolbar
```html
<van-picker :columns="columns" />
```
```javascript
export default {
data() {
return {
columns: [
{ text: 'Delaware', disabled: true },
{ text: 'Florida' },
{ text: 'Georqia' }
]
};
}
};
```
#### Show Toolbar
```html
<van-picker
showToolbar
:title="title"
:columns="pickerColumns"
@change="onChange"
:title="Title"
:columns="columns"
@cancel="onCancel"
@confirm="onConfirm"
/>
```
```javascript
export default {
data() {
return {
columns: ['Delaware', 'Florida', 'Georqia', 'Indiana', 'Maine']
}
},
methods: {
onConfirm(value, index) {
Toast(`Value: ${value}, Index: ${index}`);
},
onCancel() {
Toast('Cancel');
}
}
};
```
#### Multi columns
```html
<van-picker :columns="columns" @change="onChange" />
```
```javascript
const states = {
'Group1': ['Delaware', 'Florida', 'Georqia', 'Indiana', 'Maine'],
@ -69,29 +93,22 @@ const states = {
export default {
data() {
return {
title: 'Title',
pickerColumns: [
columns: [
{
values: Object.keys(states),
className: 'column1'
},
{
values: states.Group1,
className: 'column2'
className: 'column2',
defaultIndex: 2
}
]
};
},
methods: {
onChange(picker, values) {
picker.setColumnValues(1, citys[values[0]]);
},
onCancel() {
Toast('Cancel');
},
onConfirm() {
Toast('Confirm');
picker.setColumnValues(1, states[values[0]]);
}
}
};
@ -101,11 +118,12 @@ export default {
| Attribute | Description | Type | Default | Accepted Values |
|-----------|-----------|-----------|-------------|-------------|
| visibileColumnCount | Count of columns to show | `Number` | `5` | - |
| itemHeight | Item height | `Number` | `44` | - |
| columns | Columns data | `Array` | - | - |
| showToolbar | Whether to show toolbar | `Boolean` | `true` | - |
| title | Toolbar title | `String` | - | - |
| columns | Columns data | `Array` | `[]` | - |
| showToolbar | Whether to show toolbar | `Boolean` | `false` | - |
| title | Toolbar title | `String` | `''` | - |
| itemHeight | Option height | `Number` | `44` | - |
| visibileColumnCount | Count of visible columns | `Number` | `5` | - |
| valueKey | Key of option text | `String` | `text` | - |
### Data struct of columns
@ -116,13 +134,17 @@ export default {
| className | ClassName for this column |
### Picker instance
The first argument of change event's callback function is a picker instance with some methods
You can get the picker instance in 'change' event, and
| Method | Description |
|-----------|-----------|
| getColumnValue(index) | get current value of the column |
| setColumnValue(index, value) | set current value of the column |
| getColumnValues(index) | get all values of the column |
| setColumnValues(index, values) | set all values of the column |
| getValues() | get current values of all columns |
| setValues(values) | set current values of all columns |
| getValues() | Get current values of all columns |
| setValues(values) | Set current values of all columns |
| getIndexes() | Get current indexes of all columns |
| setIndexes(indexes) | Set current indexes of all columns |
| getColumnValue(columnIndex) | Get current value of the column |
| setColumnValue(columnIndex, value) | Set current value of the column |
| getColumnIndex(columnIndex) | Get current index of the column |
| setColumnIndex(columnIndex, optionIndex) | Set current index of the column |
| getColumnValues(columnIndex) | Get columns data of the column |
| setColumnValues(columnIndex, values) | Set columns data of the column |

View File

@ -9,87 +9,108 @@ Vue.component(Picker.name, Picker);
### 代码演示
#### 基础用法
```html
<van-picker :columns="pickerColumns" @change="onChange" />
<van-picker :columns="columns" @change="onChange" />
```
```javascript
const citys = {
'浙江': ['杭州', '宁波', '温州', '嘉兴', '湖州', '绍兴', '金华', '衢州'],
'福建': ['福州', '厦门', '莆田', '三明', '泉州', '漳州', '南平', '龙岩']
};
export default {
data() {
return {
pickerColumns: [
{
values: Object.keys(citys),
className: 'column1'
},
{
values: citys['浙江'],
className: 'column2'
}
]
columns: ['杭州', '宁波', '温州', '嘉兴', '湖州']
};
},
methods: {
onChange(picker, values) {
picker.setColumnValues(1, citys[values[0]]);
}
onChange(picker, value, index) {
Toast(`当前值:${value}, 当前索引:${index}`);
}
};
```
#### 带 toolbar 的 Picker
#### 禁用选项
选项可以为对象结构,通过设置 disabled 来禁用该选项
```html
<van-picker :columns="columns" />
```
```javascript
export default {
data() {
return {
columns: [
{ text: '杭州', disabled: true },
{ text: '宁波' },
{ text: '温州' }
]
};
}
};
```
#### 展示顶部栏
```html
<van-picker
showToolbar
:title="title"
:columns="pickerColumns"
@change="onChange"
:title="标题"
:columns="columns"
@cancel="onCancel"
@confirm="onConfirm"
/>
```
```javascript
export default {
data() {
return {
columns: ['杭州', '宁波', '温州', '嘉兴', '湖州']
}
},
methods: {
onConfirm(value, index) {
Toast(`当前值:${value}, 当前索引:${index}`);
},
onCancel() {
Toast('取消');
}
}
};
```
#### 多列联动
```html
<van-picker :columns="columns" @change="onChange" />
```
```javascript
const citys = {
'浙江': ['杭州', '宁波', '温州', '嘉兴', '湖州', '绍兴', '金华', '衢州'],
'福建': ['福州', '厦门', '莆田', '三明', '泉州', '漳州', '南平', '龙岩']
'浙江': ['杭州', '宁波', '温州', '嘉兴', '湖州'],
'福建': ['福州', '厦门', '莆田', '三明', '泉州']
};
export default {
data() {
return {
title: '地区选择',
pickerColumns: [
columns: [
{
values: Object.keys(citys),
className: 'column1'
},
{
values: citys['浙江'],
className: 'column2'
className: 'column2',
defaultIndex: 2
}
]
};
},
methods: {
onChange(picker, values) {
picker.setColumnValues(1, citys[values[0]]);
},
onCancel() {
Toast('picker cancel');
},
onConfirm() {
Toast('picker confirm');
}
}
};
@ -99,31 +120,34 @@ export default {
| 参数 | 说明 | 类型 | 默认值 | 可选值 |
|-----------|-----------|-----------|-------------|-------------|
| visibileColumnCount | 每一列可见备选元素的个数 | `Number` | `5` | - |
| itemHeight | 选中元素区高度 | `Number` | `44` | - |
| columns | 对象数组,配置每一列显示的数据 | `Array` | - | - |
| showToolbar | 是否在组件顶部显示一个toolbar | `Boolean` | `true` | - |
| title | 在toolbar上显示的标题文字 | `String` | - | - |
| columns | 对象数组,配置每一列显示的数据 | `Array` | `[]` | - |
| showToolbar | 是否显示顶部栏 | `Boolean` | `false` | - |
| title | 顶部栏标题 | `String` | `''` | - |
| itemHeight | 选项高度 | `Number` | `44` | - |
| visibileColumnCount | 可见的选项个数 | `Number` | `5` | - |
| valueKey | 选项对象中,文字对应的 key | `String` | `text` | - |
### columns
`API`中的`columns`为一个对象数组,数组中的每一个对象配置每一列,每一列有以下`key`
### Columns 数据结构
当传入多列数据时,`columns`为一个对象数组,数组中的每一个对象配置每一列,每一列有以下`key`
| key | 说明 |
|-----------|-----------|
| values | 列中对应的备选值 |
| defaultIndex | 初始选中值的索引,默认为0 |
| className | 为对应列添加特殊`class` |
| defaultIndex | 初始选中项的索引,默认为 0 |
| className | 为对应列添加额外`class` |
### change事件
`change`事件中,可以获取到`picker`实例,对`picker`进行相应的更新等操作:
### Picker 实例
`change`事件中,可以获取到`picker`实例,通过实例方法可以灵活控制 Picker 内容
| 函数 | 说明 |
|-----------|-----------|
| getColumnValue(index) | 获取对应列中选中的值 |
| setColumnValue(index, value) | 设置对应列中选中的值 |
| getColumnValues(index) | 获取对应列中所有的备选值 |
| setColumnValues(index, values) | 设置对应列中所有的备选值 |
| getValues() | 获取所有列中被选中的值,返回一个数组 |
| setValues(values) | `values`为一个数组,设置所有列中被选中的值 |
| getValues() | 获取所有列选中的值,返回一个数组 |
| setValues(values) | 设置所有列选中的值 |
| getIndexes() | 获取所有列选中值对应的索引,返回一个数组 |
| setIndexes(indexes) | 设置所有列选中值对应的索引 |
| getColumnValue(columnIndex) | 获取对应列选中的值 |
| setColumnValue(columnIndex, value) | 设置对应列选中的值 |
| getColumnIndex(columnIndex) | 获取对应列选中项的索引 |
| setColumnIndex(columnIndex, optionIndex) | 设置对应列选中项的索引 |
| getColumnValues(columnIndex) | 获取对应列中所有选项 |
| setColumnValues(columnIndex, values) | 设置对应列中所有选项 |

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))),

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);
}
},

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));
}
}
}
};

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);
}
};

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);
});
}
}
};

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;
}

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;
}
}

View File

@ -1,6 +1,7 @@
import Area from 'packages/area';
import { mount } from 'avoriaz';
import AreaList from '../mock/area.json';
import { setTimeout } from 'timers';
describe('Area', () => {
let wrapper;
@ -47,17 +48,17 @@ describe('Area', () => {
value: '110101'
}
});
expect(wrapper.hasClass('van-area')).to.be.true;
expect(wrapper.vm.$refs.picker.getColumnValue(2).code).to.equal('110101');
wrapper.setProps({
value: '110102'
});
wrapper.vm.$nextTick(() => {
setTimeout(() => {
expect(wrapper.vm.$refs.picker.getColumnValue(2).code).to.equal('110102');
done();
});
}, 50);
});
it('create an area with invalid areaList', () => {

View File

@ -1,15 +1,6 @@
import DatetimePicker from 'packages/datetime-picker';
import { mount } from 'avoriaz';
import { triggerTouch } from '../utils';
const dragHelper = (el, position) => {
triggerTouch(el, 'touchstart', 0, 0);
triggerTouch(el, 'touchmove', 0, position / 4);
triggerTouch(el, 'touchmove', 0, position / 3);
triggerTouch(el, 'touchmove', 0, position / 2);
triggerTouch(el, 'touchmove', 0, position);
triggerTouch(el, 'touchend', 0, position);
};
import { dragHelper } from '../utils';
const testTime = '10:00';
const testDate = new Date('2017/03/10 10:00');
@ -64,12 +55,12 @@ describe('DatetimePicker', () => {
}
});
const [hour, minute] = wrapper.find('.van-picker-column-wrapper');
const [hour, minute] = wrapper.find('.van-picker-column ul');
dragHelper(hour, -50);
dragHelper(minute, -50);
setTimeout(() => {
expect(wrapper.vm.innerValue).to.equal('5:05');
expect(wrapper.vm.innerValue).to.equal('10:01');
done();
}, 10);
});
@ -86,7 +77,7 @@ describe('DatetimePicker', () => {
});
setTimeout(() => {
const [year, month, day] = wrapper.find('.van-picker-column-wrapper');
const [year, month, day] = wrapper.find('.van-picker-column ul');
dragHelper(year, -50);
dragHelper(month, -50);
dragHelper(day, -50);
@ -94,9 +85,9 @@ describe('DatetimePicker', () => {
const newYear = wrapper.vm.innerValue.getFullYear();
const newMonth = wrapper.vm.innerValue.getMonth() + 1;
const newDay = wrapper.vm.innerValue.getDate();
expect(newYear).to.equal(2022);
expect(newMonth).to.equal(8);
expect(newDay).to.equal(15);
expect(newYear).to.equal(2018);
expect(newMonth).to.equal(4);
expect(newDay).to.equal(1);
done();
}, 10);
}, 10);
@ -114,7 +105,7 @@ describe('DatetimePicker', () => {
});
setTimeout(() => {
const [year, month, day, hour, minute] = wrapper.find('.van-picker-column-wrapper');
const [year, month, day, hour, minute] = wrapper.find('.van-picker-column ul');
dragHelper(year, -50);
dragHelper(month, -50);
dragHelper(day, -50);
@ -126,11 +117,11 @@ describe('DatetimePicker', () => {
const newDay = wrapper.vm.innerValue.getDate();
const newHour = wrapper.vm.innerValue.getHours();
const newMinute = wrapper.vm.innerValue.getMinutes();
expect(newYear).to.equal(2022);
expect(newMonth).to.equal(8);
expect(newDay).to.equal(15);
expect(newHour).to.equal(15);
expect(newMinute).to.equal(5);
expect(newYear).to.equal(2018);
expect(newMonth).to.equal(4);
expect(newDay).to.equal(1);
expect(newHour).to.equal(11);
expect(newMinute).to.equal(1);
done();
}, 10);
}, 10);

View File

@ -1,8 +1,7 @@
import Picker from 'packages/picker';
import PickerColumn from 'packages/picker/PickerColumn';
import { mount } from 'avoriaz';
const itemHeight = 44;
import { dragHelper } from '../utils';
const pickerColumns = [
{
@ -29,7 +28,6 @@ describe('Picker', () => {
});
expect(wrapper.hasClass('van-picker')).to.be.true;
expect(wrapper.contains('.van-picker__columns--2')).to.be.true;
expect(wrapper.vm.getColumnValues(0).length).to.equal(2);
expect(wrapper.vm.getValues().length).to.equal(2);
@ -42,7 +40,6 @@ describe('Picker', () => {
}
});
expect(wrapper.contains('.van-picker__columns--2')).to.be.true;
expect(wrapper.vm.getColumnValues(0).length).to.equal(2);
expect(wrapper.vm.getColumnValues(1).length).to.equal(6);
@ -52,16 +49,25 @@ describe('Picker', () => {
wrapper.vm.setColumnValue(0, 'normal');
expect(wrapper.vm.getColumnValue(0)).to.equal('normal');
wrapper.vm.setColumnIndex(0, 0);
expect(wrapper.vm.getColumnValue(0)).to.equal('vip');
wrapper.vm.setColumnValue(1, '1991');
expect(wrapper.vm.getColumnValue(1)).to.equal('1991');
wrapper.vm.setColumnValues(0, ['vip', 'normal', 'other']);
expect(wrapper.vm.getColumnValues(0).length).to.equal(3);
expect(wrapper.vm.getValues().length).to.equal(2);
wrapper.vm.setValues(['vip', '1992']);
expect(wrapper.vm.getColumnValue(0)).to.equal('vip');
expect(wrapper.vm.getColumnValue(1)).to.equal('1992');
expect(wrapper.vm.getColumnIndex(0)).to.equal(0);
expect(wrapper.vm.getColumnIndex(1)).to.equal(2);
expect(wrapper.vm.getColumnIndex(2)).to.equal(undefined);
wrapper.vm.setIndexes([1, 4]);
expect(wrapper.vm.getColumnValue(0)).to.equal('normal');
expect(wrapper.vm.getColumnValue(1)).to.equal('1994');
expect(wrapper.vm.getColumnValue(2)).to.equal(undefined);
});
it('create a invalid columns picker', () => {
@ -72,7 +78,7 @@ describe('Picker', () => {
});
expect(wrapper.hasClass('van-picker')).to.be.true;
expect(wrapper.vm.values.length).to.equal(0);
expect(wrapper.vm.currentColumns.length).to.equal(0);
});
it('set invalid index columns', () => {
@ -99,10 +105,7 @@ describe('Picker', () => {
});
const eventStub = sinon.stub(wrapper.vm, '$emit');
const firstColumn = wrapper.find(PickerColumn)[0];
firstColumn.vm.currentValue = 'normal';
firstColumn.update();
wrapper.vm.setColumnValue(0, 'normal');
wrapper.vm.$nextTick(() => {
expect(eventStub.calledOnce).to.be.true;
expect(eventStub.calledWith('change'));
@ -119,7 +122,7 @@ describe('Picker', () => {
expect(wrapper.hasClass('van-picker')).to.be.true;
expect(wrapper.contains('.van-picker__toolbar')).to.be.true;
expect(wrapper.vm.values.length).to.equal(0);
expect(wrapper.vm.currentColumns.length).to.equal(0);
const eventStub = sinon.stub(wrapper.vm, '$emit');
const cancelBtn = wrapper.find('.van-picker__cancel')[0];
@ -164,25 +167,20 @@ describe('PickerColumn', () => {
wrapper = mount(PickerColumn);
expect(wrapper.hasClass('van-picker-column')).to.be.true;
expect(wrapper.vm.values.length).to.equal(0);
expect(wrapper.vm.visibleContentHeight).to.equal(itemHeight * 5);
expect(wrapper.vm.dragRange[0]).to.equal(3 * itemHeight);
expect(wrapper.vm.dragRange[1]).to.equal(2 * itemHeight);
expect(wrapper.vm.options.length).to.equal(0);
});
it('change picker-column value', (done) => {
wrapper = mount(PickerColumn, {
propsData: {
values: [1, 2, 3, 4, 5],
options: [1, 2, 3, 4, 5],
value: 1
}
});
expect(wrapper.hasClass('van-picker-column')).to.be.true;
expect(wrapper.vm.values.length).to.equal(5);
expect(wrapper.vm.options.length).to.equal(5);
wrapper.vm.value = 3;
wrapper.update();
wrapper.vm.setValue(3);
wrapper.vm.$nextTick(() => {
expect(wrapper.vm.currentValue).to.equal(3);
done();
@ -192,77 +190,46 @@ describe('PickerColumn', () => {
it('change picker-column values', (done) => {
wrapper = mount(PickerColumn);
expect(wrapper.hasClass('van-picker-column')).to.be.true;
expect(wrapper.vm.values.length).to.equal(0);
expect(wrapper.vm.options.length).to.equal(0);
wrapper.vm.values = [1, 2];
wrapper.update();
wrapper.vm.options = [1, 2];
wrapper.vm.$nextTick(() => {
expect(wrapper.vm.values.length).to.equal(2);
expect(wrapper.vm.currentValues.length).to.equal(2);
expect(wrapper.vm.options.length).to.equal(2);
done();
});
});
it('create a picker test translate', () => {
it('select disabled options', () => {
wrapper = mount(PickerColumn, {
propsData: {
values: [1, 2, 3, 4, 5],
value: 1
options: [
{ text: '1', disabled: true },
{ text: '2' },
{ text: '3', disabled: true },
{ text: '4', disabled: true }
],
valueKey: 'text'
}
});
expect(wrapper.vm.currentIndex).to.equal(1);
expect(wrapper.vm.values.length).to.equal(5);
expect(wrapper.vm.value2Translate(2)).to.equal((1 - Math.floor(5 / 2)) * (-itemHeight));
expect(wrapper.vm.translate2Value(0)).to.equal(3);
wrapper.vm.setIndex(3);
expect(wrapper.vm.currentIndex).to.equal(1);
});
it('test draggable', done => {
it('drag options', () => {
wrapper = mount(PickerColumn, {
propsData: {
values: [1, 2, 3, 4, 5]
},
attachToDocument: true
options: pickerColumns[1].values
}
});
expect(wrapper.vm.currentIndex).to.equal(0);
expect(wrapper.vm.values.length).to.equal(5);
const column = wrapper.find('.van-picker-column ul')[0];
dragHelper(column, 0);
expect(wrapper.vm.currentIndex).to.equal(0);
setTimeout(() => {
const nColumn = wrapper.find('.van-picker-column-wrapper')[0];
const eventMouseObject = new window.Event('mousedown');
eventMouseObject.pageY = 0;
nColumn.element.dispatchEvent(eventMouseObject);
const eventTouchObject = new window.Event('touchstart');
eventTouchObject.changedTouches = [{ pageY: 0 }];
nColumn.element.dispatchEvent(eventTouchObject);
}, 500);
setTimeout(() => {
const nColumn = wrapper.find('.van-picker-column-wrapper')[0];
const eventMouseMoveObject = new window.Event('mousemove');
eventMouseMoveObject.pageY = 40;
document.dispatchEvent(eventMouseMoveObject);
const eventObject = new window.Event('touchmove');
eventObject.changedTouches = [{ pageY: 40 }];
nColumn.element.dispatchEvent(eventObject);
// 结束滚动
const eventMouseUpObject = new window.Event('mouseup');
document.dispatchEvent(eventMouseUpObject);
const eventEndObject = new window.Event('touchend');
eventEndObject.changedTouches = [{}];
nColumn.element.dispatchEvent(eventEndObject);
}, 1000);
setTimeout(() => {
const nItem = wrapper.find('.van-picker-column__item');
expect(nItem[1].hasClass('van-picker-column__item--selected')).to.be.true;
done();
}, 1200);
dragHelper(column, -100);
expect(wrapper.vm.currentIndex).to.equal(2);
});
});

View File

@ -73,3 +73,12 @@ export function triggerTouch(wrapper, eventName, x, y) {
el.dispatchEvent(event);
}
export function dragHelper(el, position) {
triggerTouch(el, 'touchstart', 0, 0);
triggerTouch(el, 'touchmove', 0, position / 4);
triggerTouch(el, 'touchmove', 0, position / 3);
triggerTouch(el, 'touchmove', 0, position / 2);
triggerTouch(el, 'touchmove', 0, position);
triggerTouch(el, 'touchend', 0, position);
}