[improvement] rename packages dir to src (#3659)

This commit is contained in:
neverland
2019-06-27 11:25:57 +08:00
committed by GitHub
parent 8489918dca
commit 75c79b7044
619 changed files with 21 additions and 21 deletions

270
src/picker/PickerColumn.js Normal file
View File

@@ -0,0 +1,270 @@
import { deepClone } from '../utils/deep-clone';
import { createNamespace, isObj } from '../utils';
import { range } from '../utils/format/number';
import { preventDefault } from '../utils/dom/event';
import { TouchMixin } from '../mixins/touch';
const DEFAULT_DURATION = 200;
// 惯性滑动思路:
// 在手指离开屏幕时,如果和上一次 move 时的间隔小于 `MOMENTUM_LIMIT_TIME` 且 move
// 距离大于 `MOMENTUM_LIMIT_DISTANCE` 时,执行惯性滑动,持续 `MOMENTUM_DURATION`
const MOMENTUM_DURATION = 1000;
const MOMENTUM_LIMIT_TIME = 300;
const MOMENTUM_LIMIT_DISTANCE = 15;
const [createComponent, bem] = createNamespace('picker-column');
function getElementTranslateY(element) {
const { transform } = window.getComputedStyle(element);
const translateY = transform.slice(7, transform.length - 1).split(', ')[5];
return Number(translateY);
}
function isOptionDisabled(option) {
return isObj(option) && option.disabled;
}
export default createComponent({
mixins: [TouchMixin],
props: {
valueKey: String,
className: String,
itemHeight: Number,
defaultIndex: Number,
initialOptions: Array,
visibleItemCount: Number
},
data() {
return {
offset: 0,
duration: 0,
options: deepClone(this.initialOptions),
currentIndex: this.defaultIndex
};
},
created() {
if (this.$parent.children) {
this.$parent.children.push(this);
}
this.setIndex(this.currentIndex);
},
destroyed() {
const { children } = this.$parent;
if (children) {
children.splice(children.indexOf(this), 1);
}
},
watch: {
defaultIndex() {
this.setIndex(this.defaultIndex);
}
},
computed: {
count() {
return this.options.length;
}
},
methods: {
onTouchStart(event) {
this.touchStart(event);
if (this.moving) {
const translateY = getElementTranslateY(this.$refs.wrapper);
this.startOffset = Math.min(0, translateY);
} else {
this.startOffset = this.offset;
}
this.duration = 0;
this.moving = false;
this.transitionEndTrigger = null;
this.touchStartTime = Date.now();
this.momentumOffset = this.startOffset;
},
onTouchMove(event) {
preventDefault(event);
this.moving = true;
this.touchMove(event);
this.offset = range(
this.startOffset + this.deltaY,
-(this.count * this.itemHeight),
this.itemHeight
);
const now = Date.now();
if (now - this.touchStartTime > MOMENTUM_LIMIT_TIME) {
this.touchStartTime = now;
this.momentumOffset = this.offset;
}
},
onTouchEnd() {
const distance = this.offset - this.momentumOffset;
const duration = Date.now() - this.touchStartTime;
const allowMomentum =
duration < MOMENTUM_LIMIT_TIME &&
Math.abs(distance) > MOMENTUM_LIMIT_DISTANCE;
if (allowMomentum) {
this.momentum(distance, duration);
return;
}
if (this.offset !== this.startOffset) {
this.duration = DEFAULT_DURATION;
const index = this.getIndexByOffset(this.offset);
this.setIndex(index, true);
}
},
onTransitionEnd() {
this.stopMomentum();
},
onClickItem(index) {
this.duration = DEFAULT_DURATION;
this.setIndex(index, true);
},
adjustIndex(index) {
index = range(index, 0, this.count);
for (let i = index; i < this.count; i++) {
if (!isOptionDisabled(this.options[i])) return i;
}
for (let i = index - 1; i >= 0; i--) {
if (!isOptionDisabled(this.options[i])) return i;
}
},
getOptionText(option) {
return isObj(option) && this.valueKey in option ? option[this.valueKey] : option;
},
setIndex(index, userAction) {
index = this.adjustIndex(index) || 0;
this.offset = -index * this.itemHeight;
const trigger = () => {
if (index !== this.currentIndex) {
this.currentIndex = index;
if (userAction) {
this.$emit('change', index);
}
}
};
// 若有触发过 `touchmove` 事件,那应该
// 在 `transitionend` 后再触发 `change` 事件
if (this.moving) {
this.transitionEndTrigger = trigger;
} else {
trigger();
}
},
setValue(value) {
const { options } = this;
for (let i = 0; i < options.length; i++) {
if (this.getOptionText(options[i]) === value) {
return this.setIndex(i);
}
}
},
getValue() {
return this.options[this.currentIndex];
},
getIndexByOffset(offset) {
return range(
Math.round(-offset / this.itemHeight),
0,
this.count - 1
);
},
momentum(distance, duration) {
const speed = Math.abs(distance / duration);
distance = this.offset + speed / 0.002 * (distance < 0 ? -1 : 1);
const index = this.getIndexByOffset(distance);
this.duration = MOMENTUM_DURATION;
this.setIndex(index, true);
},
stopMomentum() {
this.moving = false;
this.duration = 0;
if (this.transitionEndTrigger) {
this.transitionEndTrigger();
this.transitionEndTrigger = null;
}
}
},
render(h) {
const { itemHeight, visibleItemCount } = this;
const baseOffset = (itemHeight * (visibleItemCount - 1)) / 2;
const wrapperStyle = {
transform: `translate3d(0, ${this.offset + baseOffset}px, 0)`,
transitionDuration: `${this.duration}ms`,
transitionProperty: this.duration ? 'transform' : 'none',
lineHeight: `${itemHeight}px`
};
const optionStyle = {
height: `${itemHeight}px`
};
return (
<div
class={[bem(), this.className]}
onTouchstart={this.onTouchStart}
onTouchmove={this.onTouchMove}
onTouchend={this.onTouchEnd}
onTouchcancel={this.onTouchEnd}
>
<ul
ref="wrapper"
style={wrapperStyle}
class={bem('wrapper')}
onTransitionend={this.onTransitionEnd}
>
{this.options.map((option, index) => (
<li
style={optionStyle}
class={[
'van-ellipsis',
bem('item', { disabled: isOptionDisabled(option) })
]}
domPropsInnerHTML={this.getOptionText(option)}
onClick={() => {
this.onClickItem(index);
}}
/>
))}
</ul>
</div>
);
}
});

170
src/picker/demo/index.vue Normal file
View File

@@ -0,0 +1,170 @@
<template>
<demo-section>
<demo-block :title="$t('basicUsage')">
<van-picker
:columns="$t('column1')"
@change="onChange1"
/>
</demo-block>
<demo-block :title="$t('defaultIndex')">
<van-picker
:columns="$t('column1')"
:default-index="2"
@change="onChange1"
/>
</demo-block>
<demo-block :title="$t('title3')">
<van-picker
show-toolbar
:title="$t('title')"
:columns="$t('column1')"
@cancel="onCancel"
@confirm="onConfirm"
/>
</demo-block>
<demo-block :title="$t('withPopup')">
<van-field
readonly
clickable
:label="$t('city')"
:value="fieldValue"
:placeholder="$t('chooseCity')"
@click="onClickField"
/>
<van-popup
v-model="showPicker"
position="bottom"
>
<van-picker
show-toolbar
:columns="$t('column1')"
@cancel="onCancel2"
@confirm="onConfirm2"
/>
</van-popup>
</demo-block>
<demo-block :title="$t('title2')">
<van-picker :columns="$t('column2')" />
</demo-block>
<demo-block :title="$t('title4')">
<van-picker
:columns="columns"
@change="onChange2"
/>
</demo-block>
<demo-block :title="$t('loadingStatus')">
<van-picker
loading
:columns="columns"
/>
</demo-block>
</demo-section>
</template>
<script>
export default {
i18n: {
'zh-CN': {
city: '城市',
title2: '禁用选项',
title3: '展示顶部栏',
title4: '多列联动',
defaultIndex: '默认选中项',
withPopup: '搭配弹出层使用',
chooseCity: '选择城市',
column1: ['杭州', '宁波', '温州', '嘉兴', '湖州'],
column2: [
{ text: '杭州', disabled: true },
{ text: '宁波' },
{ text: '温州' }
],
column3: {
浙江: ['杭州', '宁波', '温州', '嘉兴', '湖州'],
福建: ['福州', '厦门', '莆田', '三明', '泉州']
},
toastContent: (value, index) => `当前值:${value}, 当前索引:${index}`
},
'en-US': {
city: 'City',
title2: 'Disable Option',
title3: 'Show Toolbar',
title4: 'Multi Columns',
defaultIndex: 'Default Index',
withPopup: 'With Popup',
chooseCity: 'Choose City',
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}`
}
},
data() {
return {
showPicker: false,
fieldValue: ''
};
},
computed: {
columns() {
const column = this.$t('column3');
return [
{
values: Object.keys(column),
className: 'column1'
},
{
values: column[Object.keys(column)[0]],
className: 'column2',
defaultIndex: 2
}
];
}
},
methods: {
onChange1(picker, value, index) {
this.$toast(this.$t('toastContent', value, index));
},
onChange2(picker, values) {
picker.setColumnValues(1, this.$t('column3')[values[0]]);
},
onConfirm(value, index) {
this.$toast(this.$t('toastContent', value, index));
},
onCancel() {
this.$toast(this.$t('cancel'));
},
onClickField() {
this.showPicker = true;
},
onConfirm2(value) {
this.showPicker = false;
this.fieldValue = value;
},
onCancel2() {
this.showPicker = false;
}
}
};
</script>

238
src/picker/en-US.md Normal file
View File

@@ -0,0 +1,238 @@
# Picker
### Intro
The Picker component is usually used with [Popup](#/en-US/popup) Component.
### Install
``` javascript
import { Picker } from 'vant';
Vue.use(Picker);
```
## Usage
### Basic Usage
```html
<van-picker :columns="columns" @change="onChange" />
```
```javascript
export default {
data() {
return {
columns: ['Delaware', 'Florida', 'Georqia', 'Indiana', 'Maine']
};
},
methods: {
onChange(picker, value, index) {
Toast(`Value: ${value}, Index: ${index}`);
}
}
};
```
### Default Index
```html
<van-picker
:columns="columns"
:default-index="2"
@change="onChange"
/>
```
### Show Toolbar
```html
<van-picker
show-toolbar
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');
}
}
};
```
### With Popup
```html
<van-field
readonly
clickable
label="City"
:value="value"
placeholder="Choose City"
@click="showPicker = true"
/>
<van-popup v-model="showPicker" position="bottom">
<van-picker
show-toolbar
:columns="columns"
@cancel="showPicker = false"
@confirm="onConfirm"
/>
</van-popup>
```
```js
export default {
data() {
return {
value: '',
showPicker: false,
columns: ['Delaware', 'Florida', 'Georqia', 'Indiana', 'Maine']
}
},
methods: {
onConfirm(value) {
this.value = value;
this.showPicker = false;
}
}
};
```
### Disable option
```html
<van-picker :columns="columns" />
```
```javascript
export default {
data() {
return {
columns: [
{ text: 'Delaware', disabled: true },
{ text: 'Florida' },
{ text: 'Georqia' }
]
};
}
};
```
### Multi columns
```html
<van-picker :columns="columns" @change="onChange" />
```
```javascript
const states = {
'Group1': ['Delaware', 'Florida', 'Georqia', 'Indiana', 'Maine'],
'Group2': ['Alabama', 'Kansas', 'Louisiana', 'Texas']
};
export default {
data() {
return {
columns: [
{
values: Object.keys(states),
className: 'column1'
},
{
values: states.Group1,
className: 'column2',
defaultIndex: 2
}
]
};
},
methods: {
onChange(picker, values) {
picker.setColumnValues(1, states[values[0]]);
}
}
};
```
### Loading
When Picker columns data is acquired asynchronously, use `loading` prop to show loading prompt
```html
<van-picker :columns="columns" loading />
```
## API
### Props
| Attribute | Description | Type | Default |
|------|------|------|------|
| columns | Columns data | `Array` | `[]` |
| show-toolbar | Whether to show toolbar | `Boolean` | `false` |
| toolbar-position | Toolbar position, cat be set to `bottom` | `String` | `top` |
| title | Toolbar title | `String` | `''` |
| loading | Whether to show loading prompt | `Boolean` | `false` |
| value-key | Key of option text | `String` | `text` |
| item-height | Option height | `Number` | `44` |
| confirm-button-text | Text of confirm button | `String` | `Confirm` |
| cancel-button-text | Text of cancel button | `String` | `Cancel` |
| visible-item-count | Count of visible columns | `Number` | `5` |
| default-index | Default value index of single column picker | `Number` | `0` |
### Events
Picker events will pass different parameters according to the columns are single or multiple
| Event | Description | Arguments |
|------|------|------|
| confirm | Triggered when click confirm button | Single columncurrent valuecurrent index<br>Multiple columnscurrent valuescurrent indexes |
| cancel | Triggered when click cancel button | Single columncurrent valuecurrent index<br>Multiple columnscurrent valuescurrent indexes |
| change | Triggered when current option changed | Single columnPicker instance, current valuecurrent index<br>Multiple columnsPicker instance, current valuescolumn index |
### Slots
| Name | Description |
|------|------|
| title | Custom title |
### Data struct of columns
| key | Description |
|------|------|
| values | Value of column |
| defaultIndex | Default value index |
| className | ClassName for this column |
### Methods
Use ref to get picker instance and call instance methods
| Name | Attribute | Return value | Description |
|------|------|------|------|
| getValues | - | values | Get current values of all columns |
| setValues | values | - | Set current values of all columns |
| getIndexes | - | indexes | Get current indexes of all columns |
| setIndexes | indexes | - | Set current indexes of all columns |
| getColumnValue | columnIndex | value | Get current value of the column |
| setColumnValue | columnIndex, value | - | Set current value of the column |
| getColumnIndex | columnIndex | optionIndex | Get current index of the column |
| setColumnIndex | columnIndex, optionIndex | - | Set current index of the column |
| getColumnValues | columnIndex | values | Get columns data of the column |
| setColumnValues | columnIndex, values | - | Set columns data of the column |

203
src/picker/index.js Normal file
View File

@@ -0,0 +1,203 @@
import { createNamespace } from '../utils';
import { preventDefault } from '../utils/dom/event';
import { deepClone } from '../utils/deep-clone';
import { pickerProps } from './shared';
import { BLUE } from '../utils/color';
import Loading from '../loading';
import PickerColumn from './PickerColumn';
const [createComponent, bem, t] = createNamespace('picker');
export default createComponent({
props: {
...pickerProps,
columns: Array,
defaultIndex: Number,
toolbarPosition: {
type: String,
default: 'top'
},
valueKey: {
type: String,
default: 'text'
}
},
data() {
return {
children: []
};
},
computed: {
simple() {
return this.columns.length && !this.columns[0].values;
}
},
watch: {
columns() {
this.setColumns();
}
},
methods: {
setColumns() {
const columns = this.simple ? [{ values: this.columns }] : this.columns;
columns.forEach((column, index) => {
this.setColumnValues(index, deepClone(column.values));
});
},
emit(event) {
if (this.simple) {
this.$emit(event, this.getColumnValue(0), this.getColumnIndex(0));
} else {
this.$emit(event, this.getValues(), this.getIndexes());
}
},
onChange(columnIndex) {
if (this.simple) {
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) {
return this.children[index];
},
// get column value by index
getColumnValue(index) {
const column = this.getColumn(index);
return column && column.getValue();
},
// set column value by index
setColumnValue(index, value) {
const column = this.getColumn(index);
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) {
return (this.children[index] || {}).options;
},
// set options of column by index
setColumnValues(index, options) {
const column = this.children[index];
if (column && JSON.stringify(column.options) !== JSON.stringify(options)) {
column.options = options;
column.setIndex(0);
}
},
// get values of all columns
getValues() {
return this.children.map(child => child.getValue());
},
// 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);
});
},
onConfirm() {
this.children.map(child => child.stopMomentum());
this.emit('confirm');
},
onCancel() {
this.emit('cancel');
}
},
render(h) {
const { itemHeight } = this;
const wrapHeight = itemHeight * this.visibleItemCount;
const columns = this.simple ? [this.columns] : this.columns;
const frameStyle = {
height: `${itemHeight}px`
};
const columnsStyle = {
height: `${wrapHeight}px`
};
const maskStyle = {
backgroundSize: `100% ${(wrapHeight - itemHeight) / 2}px`
};
const Toolbar = this.showToolbar && (
<div class={['van-hairline--top-bottom', bem('toolbar')]}>
{this.slots() || [
<div role="button" tabindex="0" class={bem('cancel')} onClick={this.onCancel}>
{this.cancelButtonText || t('cancel')}
</div>,
this.slots('title') ||
(this.title && (
<div class={['van-ellipsis', bem('title')]}>{this.title}</div>
)),
<div role="button" tabindex="0" class={bem('confirm')} onClick={this.onConfirm}>
{this.confirmButtonText || t('confirm')}
</div>
]}
</div>
);
return (
<div class={bem()}>
{this.toolbarPosition === 'top' ? Toolbar : h()}
{this.loading ? <Loading class={bem('loading')} color={BLUE} /> : h()}
<div class={bem('columns')} style={columnsStyle} onTouchmove={preventDefault}>
{columns.map((item, index) => (
<PickerColumn
valueKey={this.valueKey}
className={item.className}
itemHeight={this.itemHeight}
defaultIndex={item.defaultIndex || this.defaultIndex}
visibleItemCount={this.visibleItemCount}
initialOptions={this.simple ? item : item.values}
onChange={() => {
this.onChange(index);
}}
/>
))}
<div class={bem('mask')} style={maskStyle} />
<div class={['van-hairline--top-bottom', bem('frame')]} style={frameStyle} />
</div>
{this.toolbarPosition === 'bottom' ? Toolbar : h()}
</div>
);
}
});

96
src/picker/index.less Normal file
View File

@@ -0,0 +1,96 @@
@import '../style/var';
.van-picker {
position: relative;
background-color: @picker-background-color;
user-select: none;
-webkit-text-size-adjust: 100%; /* avoid iOS text size adjust */
&__toolbar {
display: flex;
justify-content: space-between;
height: @picker-toolbar-height;
line-height: @picker-toolbar-height;
}
&__cancel,
&__confirm {
padding: @picker-action-padding;
color: @picker-action-text-color;
font-size: @picker-action-font-size;
&:active {
background-color: @picker-action-active-color;
}
}
&__title {
max-width: 50%;
font-weight: 500;
font-size: @picker-title-font-size;
text-align: center;
}
&__columns {
position: relative;
display: flex;
}
&__loading {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 2;
display: flex;
align-items: center;
justify-content: center;
background-color: rgba(255, 255, 255, .9);
}
&__loading .van-loading,
&__frame {
position: absolute;
top: 50%;
left: 0;
z-index: 3;
width: 100%;
transform: translateY(-50%);
pointer-events: none;
}
&__mask {
position: absolute;
top: 0;
left: 0;
z-index: 2;
width: 100%;
height: 100%;
background-image: linear-gradient(180deg, hsla(0, 0%, 100%, .9), hsla(0, 0%, 100%, .4)),
linear-gradient(0deg, hsla(0, 0%, 100%, .9), hsla(0, 0%, 100%, .4));
background-repeat: no-repeat;
background-position: top, bottom;
pointer-events: none;
}
&-column {
flex: 1;
overflow: hidden;
font-size: @picker-option-font-size;
text-align: center;
&__wrapper {
transition-timing-function: cubic-bezier(0.23, 1, 0.68, 1);
}
&__item {
padding: 0 5px;
color: @picker-option-text-color;
&--disabled {
opacity: @picker-option-disabled-opacity;
}
}
}
}

25
src/picker/shared.ts Normal file
View File

@@ -0,0 +1,25 @@
export type SharedPickerProps = {
title?: string;
loading?: boolean;
itemHeight: number;
showToolbar?: boolean;
visibleItemCount: number;
cancelButtonText?: string;
confirmButtonText?: string;
}
export const pickerProps = {
title: String,
loading: Boolean,
showToolbar: Boolean,
cancelButtonText: String,
confirmButtonText: String,
visibleItemCount: {
type: Number,
default: 5
},
itemHeight: {
type: Number,
default: 44
}
};

View File

@@ -0,0 +1,144 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders demo correctly 1`] = `
<div>
<div>
<div class="van-picker">
<!---->
<div class="van-picker__columns" style="height: 220px;">
<div class="van-picker-column">
<ul class="van-picker-column__wrapper" style="transform: translate3d(0, 88px, 0); transition-duration: 0ms; transition-property: none; line-height: 44px;">
<li class="van-ellipsis van-picker-column__item" style="height: 44px;">杭州</li>
<li class="van-ellipsis van-picker-column__item" style="height: 44px;">宁波</li>
<li class="van-ellipsis van-picker-column__item" style="height: 44px;">温州</li>
<li class="van-ellipsis van-picker-column__item" style="height: 44px;">嘉兴</li>
<li class="van-ellipsis van-picker-column__item" style="height: 44px;">湖州</li>
</ul>
</div>
<div class="van-picker__mask" style="background-size: 100% 88px;"></div>
<div class="van-hairline--top-bottom van-picker__frame" style="height: 44px;"></div>
</div>
<!---->
</div>
</div>
<div>
<div class="van-picker">
<!---->
<div class="van-picker__columns" style="height: 220px;">
<div class="van-picker-column">
<ul class="van-picker-column__wrapper" style="transform: translate3d(0, 0px, 0); transition-duration: 0ms; transition-property: none; line-height: 44px;">
<li class="van-ellipsis van-picker-column__item" style="height: 44px;">杭州</li>
<li class="van-ellipsis van-picker-column__item" style="height: 44px;">宁波</li>
<li class="van-ellipsis van-picker-column__item" style="height: 44px;">温州</li>
<li class="van-ellipsis van-picker-column__item" style="height: 44px;">嘉兴</li>
<li class="van-ellipsis van-picker-column__item" style="height: 44px;">湖州</li>
</ul>
</div>
<div class="van-picker__mask" style="background-size: 100% 88px;"></div>
<div class="van-hairline--top-bottom van-picker__frame" style="height: 44px;"></div>
</div>
<!---->
</div>
</div>
<div>
<div class="van-picker">
<div class="van-hairline--top-bottom van-picker__toolbar">
<div role="button" tabindex="0" class="van-picker__cancel">取消</div>
<div class="van-ellipsis van-picker__title">标题</div>
<div role="button" tabindex="0" class="van-picker__confirm">确认</div>
</div>
<!---->
<div class="van-picker__columns" style="height: 220px;">
<div class="van-picker-column">
<ul class="van-picker-column__wrapper" style="transform: translate3d(0, 88px, 0); transition-duration: 0ms; transition-property: none; line-height: 44px;">
<li class="van-ellipsis van-picker-column__item" style="height: 44px;">杭州</li>
<li class="van-ellipsis van-picker-column__item" style="height: 44px;">宁波</li>
<li class="van-ellipsis van-picker-column__item" style="height: 44px;">温州</li>
<li class="van-ellipsis van-picker-column__item" style="height: 44px;">嘉兴</li>
<li class="van-ellipsis van-picker-column__item" style="height: 44px;">湖州</li>
</ul>
</div>
<div class="van-picker__mask" style="background-size: 100% 88px;"></div>
<div class="van-hairline--top-bottom van-picker__frame" style="height: 44px;"></div>
</div>
<!---->
</div>
</div>
<div>
<div class="van-cell van-cell--clickable van-field">
<div class="van-cell__title van-field__label"><span>城市</span></div>
<div class="van-cell__value">
<div class="van-field__body"><input type="text" placeholder="选择城市" readonly="readonly" class="van-field__control"></div>
</div>
</div>
<!---->
</div>
<div>
<div class="van-picker">
<!---->
<div class="van-picker__columns" style="height: 220px;">
<div class="van-picker-column">
<ul class="van-picker-column__wrapper" style="transform: translate3d(0, 44px, 0); transition-duration: 0ms; transition-property: none; line-height: 44px;">
<li class="van-ellipsis van-picker-column__item van-picker-column__item--disabled" style="height: 44px;">杭州</li>
<li class="van-ellipsis van-picker-column__item" style="height: 44px;">宁波</li>
<li class="van-ellipsis van-picker-column__item" style="height: 44px;">温州</li>
</ul>
</div>
<div class="van-picker__mask" style="background-size: 100% 88px;"></div>
<div class="van-hairline--top-bottom van-picker__frame" style="height: 44px;"></div>
</div>
<!---->
</div>
</div>
<div>
<div class="van-picker">
<!---->
<div class="van-picker__columns" style="height: 220px;">
<div class="van-picker-column column1">
<ul class="van-picker-column__wrapper" style="transform: translate3d(0, 88px, 0); transition-duration: 0ms; transition-property: none; line-height: 44px;">
<li class="van-ellipsis van-picker-column__item" style="height: 44px;">浙江</li>
<li class="van-ellipsis van-picker-column__item" style="height: 44px;">福建</li>
</ul>
</div>
<div class="van-picker-column column2">
<ul class="van-picker-column__wrapper" style="transform: translate3d(0, 0px, 0); transition-duration: 0ms; transition-property: none; line-height: 44px;">
<li class="van-ellipsis van-picker-column__item" style="height: 44px;">杭州</li>
<li class="van-ellipsis van-picker-column__item" style="height: 44px;">宁波</li>
<li class="van-ellipsis van-picker-column__item" style="height: 44px;">温州</li>
<li class="van-ellipsis van-picker-column__item" style="height: 44px;">嘉兴</li>
<li class="van-ellipsis van-picker-column__item" style="height: 44px;">湖州</li>
</ul>
</div>
<div class="van-picker__mask" style="background-size: 100% 88px;"></div>
<div class="van-hairline--top-bottom van-picker__frame" style="height: 44px;"></div>
</div>
<!---->
</div>
</div>
<div>
<div class="van-picker">
<div class="van-loading van-loading--circular van-picker__loading"><span class="van-loading__spinner van-loading__spinner--circular" style="color: rgb(25, 137, 250);"><svg viewBox="25 25 50 50" class="van-loading__circular"><circle cx="50" cy="50" r="20" fill="none"></circle></svg></span></div>
<div class="van-picker__columns" style="height: 220px;">
<div class="van-picker-column column1">
<ul class="van-picker-column__wrapper" style="transform: translate3d(0, 88px, 0); transition-duration: 0ms; transition-property: none; line-height: 44px;">
<li class="van-ellipsis van-picker-column__item" style="height: 44px;">浙江</li>
<li class="van-ellipsis van-picker-column__item" style="height: 44px;">福建</li>
</ul>
</div>
<div class="van-picker-column column2">
<ul class="van-picker-column__wrapper" style="transform: translate3d(0, 0px, 0); transition-duration: 0ms; transition-property: none; line-height: 44px;">
<li class="van-ellipsis van-picker-column__item" style="height: 44px;">杭州</li>
<li class="van-ellipsis van-picker-column__item" style="height: 44px;">宁波</li>
<li class="van-ellipsis van-picker-column__item" style="height: 44px;">温州</li>
<li class="van-ellipsis van-picker-column__item" style="height: 44px;">嘉兴</li>
<li class="van-ellipsis van-picker-column__item" style="height: 44px;">湖州</li>
</ul>
</div>
<div class="van-picker__mask" style="background-size: 100% 88px;"></div>
<div class="van-hairline--top-bottom van-picker__frame" style="height: 44px;"></div>
</div>
<!---->
</div>
</div>
</div>
`;

View File

@@ -0,0 +1,58 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`column watch default index 1`] = `
<div class="van-picker-column">
<ul class="van-picker-column__wrapper" style="transform: translate3d(0, -75px, 0); transition-duration: 0ms; transition-property: none; line-height: 50px;">
<li class="van-ellipsis van-picker-column__item van-picker-column__item--disabled" style="height: 50px;">1</li>
<li class="van-ellipsis van-picker-column__item" style="height: 50px;">1990</li>
<li class="van-ellipsis van-picker-column__item" style="height: 50px;">1991</li>
<li class="van-ellipsis van-picker-column__item" style="height: 50px;">1992</li>
<li class="van-ellipsis van-picker-column__item" style="height: 50px;">1993</li>
<li class="van-ellipsis van-picker-column__item" style="height: 50px;">1994</li>
<li class="van-ellipsis van-picker-column__item" style="height: 50px;">1995</li>
</ul>
</div>
`;
exports[`column watch default index 2`] = `
<div class="van-picker-column">
<ul class="van-picker-column__wrapper" style="transform: translate3d(0, -125px, 0); transition-duration: 0ms; transition-property: none; line-height: 50px;">
<li class="van-ellipsis van-picker-column__item van-picker-column__item--disabled" style="height: 50px;">1</li>
<li class="van-ellipsis van-picker-column__item" style="height: 50px;">1990</li>
<li class="van-ellipsis van-picker-column__item" style="height: 50px;">1991</li>
<li class="van-ellipsis van-picker-column__item" style="height: 50px;">1992</li>
<li class="van-ellipsis van-picker-column__item" style="height: 50px;">1993</li>
<li class="van-ellipsis van-picker-column__item" style="height: 50px;">1994</li>
<li class="van-ellipsis van-picker-column__item" style="height: 50px;">1995</li>
</ul>
</div>
`;
exports[`render title slot 1`] = `
<div class="van-picker">
<div class="van-hairline--top-bottom van-picker__toolbar">
<div role="button" tabindex="0" class="van-picker__cancel">取消</div>Custom title<div role="button" tabindex="0" class="van-picker__confirm">确认</div>
</div>
<!---->
<div class="van-picker__columns" style="height: 220px;">
<div class="van-picker__mask" style="background-size: 100% 88px;"></div>
<div class="van-hairline--top-bottom van-picker__frame" style="height: 44px;"></div>
</div>
<!---->
</div>
`;
exports[`toolbar-position prop 1`] = `
<div class="van-picker">
<!---->
<!---->
<div class="van-picker__columns" style="height: 220px;">
<div class="van-picker__mask" style="background-size: 100% 88px;"></div>
<div class="van-hairline--top-bottom van-picker__frame" style="height: 44px;"></div>
</div>
<div class="van-hairline--top-bottom van-picker__toolbar">
<div role="button" tabindex="0" class="van-picker__cancel">取消</div>
<div role="button" tabindex="0" class="van-picker__confirm">确认</div>
</div>
</div>
`;

View File

@@ -0,0 +1,4 @@
import Demo from '../demo';
import demoTest from '../../../test/demo-test';
demoTest(Demo);

View File

@@ -0,0 +1,194 @@
import Picker from '..';
import PickerColumn from '../PickerColumn';
import { mount, triggerDrag, later } from '../../../test/utils';
const simpleColumn = ['1990', '1991', '1992', '1993', '1994', '1995'];
const columns = [
{
values: ['vip', 'normal'],
className: 'column1'
},
{
values: simpleColumn,
className: 'column2'
}
];
test('simple columns confirm & cancel event', () => {
const wrapper = mount(Picker, {
propsData: {
showToolbar: true,
columns: simpleColumn
}
});
wrapper.find('.van-picker__confirm').trigger('click');
wrapper.find('.van-picker__cancel').trigger('click');
expect(wrapper.emitted('confirm')[0]).toEqual(['1990', 0]);
expect(wrapper.emitted('cancel')[0]).toEqual(['1990', 0]);
wrapper.destroy();
});
test('multiple columns confirm & cancel event', () => {
const wrapper = mount(Picker, {
propsData: {
showToolbar: true
}
});
wrapper.find('.van-picker__confirm').trigger('click');
wrapper.find('.van-picker__cancel').trigger('click');
expect(wrapper.emitted('confirm')[0]).toEqual([[], []]);
expect(wrapper.emitted('cancel')[0]).toEqual([[], []]);
});
test('set picker values', () => {
const wrapper = mount(Picker, {
propsData: {
columns
}
});
const { vm } = wrapper;
expect(vm.getColumnValues(-1)).toEqual(undefined);
expect(vm.getColumnValues(1).length).toEqual(6);
expect(vm.getColumnValue(1)).toEqual('1990');
vm.setColumnValue(0, 'normal');
expect(vm.getColumnValue(0)).toEqual('normal');
vm.setColumnIndex(0, 0);
expect(vm.getColumnValue(0)).toEqual('vip');
vm.setColumnValue(1, '1991');
expect(vm.getColumnValue(1)).toEqual('1991');
vm.setColumnValues(0, ['vip', 'normal', 'other']);
expect(vm.getColumnValues(0).length).toEqual(3);
expect(vm.getValues().length).toEqual(2);
vm.setColumnValues(-1, []);
expect(vm.getValues().length).toEqual(2);
vm.setValues(['vip', '1992']);
expect(vm.getColumnIndex(1)).toEqual(2);
expect(vm.getColumnIndex(2)).toEqual(undefined);
expect(vm.getIndexes(2)).toEqual([0, 2]);
vm.setIndexes([1, 4]);
expect(vm.getColumnValue(1)).toEqual('1994');
expect(vm.getColumnValue(2)).toEqual(undefined);
});
test('drag columns', async () => {
const wrapper = mount(Picker, {
propsData: {
columns
}
});
triggerDrag(wrapper.find('.van-picker-column'), 0, -100);
wrapper.find('.van-picker-column ul').trigger('transitionend');
// 由于在极短的时间(大约几毫秒)移动 `100px`,因此再计算惯性滚动的距离时,
// 会得到一个很大的值,导致会滚动到且选中列表的最后一项
expect(wrapper.emitted('change')[0][1]).toEqual(['normal', '1990']);
});
test('drag simple columns', async () => {
const wrapper = mount(Picker, {
propsData: {
columns: simpleColumn
}
});
triggerDrag(wrapper.find('.van-picker-column'), 0, -100);
wrapper.find('.van-picker-column ul').trigger('transitionend');
// 由于在极短的时间(大约几毫秒)移动 `100px`,因此再计算惯性滚动的距离时,
// 会得到一个很大的值,导致会滚动到且选中列表的最后一项
expect(wrapper.emitted('change')[0][1]).toEqual('1995');
});
test('column watch default index', async () => {
const disabled = { disabled: true, text: 1 };
const wrapper = mount(PickerColumn, {
propsData: {
initialOptions: [disabled, ...simpleColumn],
valueKey: 'text',
itemHeight: 50
}
});
await later();
expect(wrapper).toMatchSnapshot();
wrapper.vm.defaultIndex = 2;
expect(wrapper).toMatchSnapshot();
});
test('render title slot', () => {
const wrapper = mount({
template: `
<picker show-toolbar>
<template v-slot:title>Custom title</template>
</picker>
`,
components: {
Picker
}
});
expect(wrapper).toMatchSnapshot();
});
test('simulation finger swipe again before transitionend', () => {
// mock getComputedStyle
// see: https://github.com/jsdom/jsdom/issues/2588
const originGetComputedStyle = window.getComputedStyle;
window.getComputedStyle = ele => {
const style = originGetComputedStyle(ele);
return {
...style,
transform: 'matrix(1, 0, 0, 1, 0, -5)'
};
};
const wrapper = mount(Picker, {
propsData: {
columns: simpleColumn
}
});
triggerDrag(wrapper.find('.van-picker-column'), 0, -5);
triggerDrag(wrapper.find('.van-picker-column'), -5, -100);
wrapper.find('.van-picker-column ul').trigger('transitionend');
expect(wrapper.emitted('change')[0][1]).toEqual('1995');
});
test('click column\'s item', () => {
const columns = [
{ text: '杭州' },
{ text: '宁波' },
{ text: '温州', disabled: true },
{ text: '嘉兴', disabled: true }
];
const wrapper = mount(Picker, {
propsData: {
columns
}
});
wrapper.findAll('.van-picker-column__item').at(3).trigger('click');
expect(wrapper.emitted('change')[0][1]).toEqual(columns[1]);
});
test('toolbar-position prop', () => {
const wrapper = mount(Picker, {
propsData: {
showToolbar: true,
toolbarPosition: 'bottom'
}
});
expect(wrapper).toMatchSnapshot();
});

249
src/picker/zh-CN.md Normal file
View File

@@ -0,0 +1,249 @@
# Picker 选择器
### 介绍
选择器组件通常与 [弹出层](#/zh-CN/popup) 组件配合使用
### 引入
``` javascript
import { Picker } from 'vant';
Vue.use(Picker);
```
## 代码演示
### 基础用法
对于单列选择器,传入数值格式的 columns 即可,同时可以监听选项改变的 change 事件
```html
<van-picker :columns="columns" @change="onChange" />
```
```javascript
export default {
data() {
return {
columns: ['杭州', '宁波', '温州', '嘉兴', '湖州']
};
},
methods: {
onChange(picker, value, index) {
Toast(`当前值:${value}, 当前索引:${index}`);
}
}
};
```
### 默认选中项
单列选择器可以直接通过`default-index`属性设置初始选中项的索引值
```html
<van-picker
:columns="columns"
:default-index="2"
@change="onChange"
/>
```
### 展示顶部栏
通常选择器组件会传入`show-toolbar`属性以展示顶部操作栏,并可以监听对应的`confirm`和`cancel`事件
```html
<van-picker
show-toolbar
title="标题"
:columns="columns"
@cancel="onCancel"
@confirm="onConfirm"
/>
```
```javascript
export default {
data() {
return {
columns: ['杭州', '宁波', '温州', '嘉兴', '湖州']
}
},
methods: {
onConfirm(value, index) {
Toast(`当前值:${value}, 当前索引:${index}`);
},
onCancel() {
Toast('取消');
}
}
};
```
### 搭配弹出层使用
```html
<van-field
readonly
clickable
label="城市"
:value="value"
placeholder="选择城市"
@click="showPicker = true"
/>
<van-popup v-model="showPicker" position="bottom">
<van-picker
show-toolbar
:columns="columns"
@cancel="showPicker = false"
@confirm="onConfirm"
/>
</van-popup>
```
```js
export default {
data() {
return {
value: '',
showPicker: false,
columns: ['杭州', '宁波', '温州', '嘉兴', '湖州']
}
},
methods: {
onConfirm(value) {
this.value = value;
this.showPicker = false;
}
}
};
```
### 禁用选项
选项可以为对象结构,通过设置 disabled 来禁用该选项
```html
<van-picker :columns="columns" />
```
```javascript
export default {
data() {
return {
columns: [
{ text: '杭州', disabled: true },
{ text: '宁波' },
{ text: '温州' }
]
};
}
};
```
### 多列联动
```html
<van-picker :columns="columns" @change="onChange" />
```
```javascript
const citys = {
'浙江': ['杭州', '宁波', '温州', '嘉兴', '湖州'],
'福建': ['福州', '厦门', '莆田', '三明', '泉州']
};
export default {
data() {
return {
columns: [
{
values: Object.keys(citys),
className: 'column1'
},
{
values: citys['浙江'],
className: 'column2',
defaultIndex: 2
}
]
};
},
methods: {
onChange(picker, values) {
picker.setColumnValues(1, citys[values[0]]);
}
}
};
```
### 加载状态
若选择器数据是异步获取的,可以通过 `loading` 属性显示加载提示
```html
<van-picker :columns="columns" loading />
```
## API
### Props
| 参数 | 说明 | 类型 | 默认值 | 版本 |
|------|------|------|------|------|
| columns | 对象数组,配置每一列显示的数据 | `Array` | `[]` | - |
| show-toolbar | 是否显示顶部栏 | `Boolean` | `false` | - |
| toolbar-position | 顶部栏位置,可选值为`bottom` | `String` | `top` | 2.0.0 |
| title | 顶部栏标题 | `String` | `''` | - |
| loading | 是否显示加载状态 | `Boolean` | `false` | - |
| value-key | 选项对象中,文字对应的 key | `String` | `text` | - |
| item-height | 选项高度 | `Number` | `44` | - |
| confirm-button-text | 确认按钮文字 | `String` | `确认` | - |
| cancel-button-text | 取消按钮文字 | `String` | `取消` | - |
| visible-item-count | 可见的选项个数 | `Number` | `5` | - |
| default-index | 单列选择器的默认选中项索引,<br>多列选择器请参考下方的 Columns 配置 | `Number` | `0` | 1.6.9 |
### Events
Picker 组件的事件会根据 columns 是单列或多列返回不同的参数
| 事件名 | 说明 | 回调参数 |
|------|------|------|
| confirm | 点击完成按钮时触发 | 单列:选中值,选中值对应的索引<br>多列:所有列选中值,所有列选中值对应的索引 |
| cancel | 点击取消按钮时触发 | 单列:选中值,选中值对应的索引<br>多列:所有列选中值,所有列选中值对应的索引 |
| change | 选项改变时触发 | 单列Picker 实例,选中值,选中值对应的索引<br>多列Picker 实例,所有列选中值,当前列对应的索引 |
### Slots
| 名称 | 说明 |
|------|------|
| title | 自定义标题内容 |
### Columns 数据结构
当传入多列数据时,`columns`为一个对象数组,数组中的每一个对象配置每一列,每一列有以下`key`
| key | 说明 |
|------|------|
| values | 列中对应的备选值 |
| defaultIndex | 初始选中项的索引,默认为 0 |
| className | 为对应列添加额外的`class` |
### 方法
通过 ref 可以获取到 picker 实例并调用实例方法
| 方法名 | 参数 | 返回值 | 介绍 |
|------|------|------|------|
| getValues | - | values | 获取所有列选中的值 |
| setValues | values | - | 设置所有列选中的值 |
| getIndexes | - | indexes | 获取所有列选中值对应的索引 |
| setIndexes | indexes | - | 设置所有列选中值对应的索引 |
| getColumnValue | columnIndex | value | 获取对应列选中的值 |
| setColumnValue | columnIndex, value | - | 设置对应列选中的值 |
| getColumnIndex | columnIndex | optionIndex | 获取对应列选中项的索引 |
| setColumnIndex | columnIndex, optionIndex | - | 设置对应列选中项的索引 |
| getColumnValues | columnIndex | values | 获取对应列中所有选项 |
| setColumnValues | columnIndex, values | - | 设置对应列中所有选项 |