diff --git a/src/picker/PickerColumn.js b/src/picker/PickerColumn.js
index e9be705c9..54d5a18df 100644
--- a/src/picker/PickerColumn.js
+++ b/src/picker/PickerColumn.js
@@ -1,8 +1,15 @@
+import { reactive, ref, watch } from 'vue';
+import { PICKER_KEY } from './shared';
+
+// Utils
+import { range } from '../utils/format/number';
import { deepClone } from '../utils/deep-clone';
import { createNamespace, isObject } from '../utils';
-import { range } from '../utils/format/number';
import { preventDefault } from '../utils/dom/event';
-import { TouchMixin } from '../mixins/touch';
+
+// Composition
+import { useTouch } from '../composition/use-touch';
+import { useParent } from '../composition/use-relation';
const DEFAULT_DURATION = 200;
@@ -27,8 +34,6 @@ function isOptionDisabled(option) {
}
export default createComponent({
- mixins: [TouchMixin],
-
props: {
valueKey: String,
readonly: Boolean,
@@ -46,257 +51,210 @@ export default createComponent({
emits: ['change'],
- data() {
- return {
+ setup(props, { emit }) {
+ let moving;
+ let startOffset;
+ let touchStartTime;
+ let momentumOffset;
+ let transitionEndTrigger;
+
+ const wrapper = ref();
+
+ const state = reactive({
+ index: props.defaultIndex,
offset: 0,
duration: 0,
- options: deepClone(this.initialOptions),
- currentIndex: this.defaultIndex,
- };
- },
+ options: deepClone(props.initialOptions),
+ });
- created() {
- if (this.$parent.children) {
- this.$parent.children.push(this);
- }
+ const touch = useTouch();
- this.setIndex(this.currentIndex);
- },
+ const count = () => state.options.length;
- mounted() {
- this.bindTouchEvent(this.$el);
- },
+ const baseOffset = () =>
+ (props.itemHeight * (props.visibleItemCount - 1)) / 2;
- unmounted() {
- const { children } = this.$parent;
+ const adjustIndex = (index) => {
+ index = range(index, 0, count());
- if (children) {
- children.splice(children.indexOf(this), 1);
- }
- },
-
- watch: {
- initialOptions: 'setOptions',
-
- defaultIndex(val) {
- this.setIndex(val);
- },
- },
-
- computed: {
- count() {
- return this.options.length;
- },
-
- baseOffset() {
- return (this.itemHeight * (this.visibleItemCount - 1)) / 2;
- },
- },
-
- methods: {
- setOptions(options) {
- if (JSON.stringify(options) !== JSON.stringify(this.options)) {
- this.options = deepClone(options);
- this.setIndex(this.defaultIndex);
+ for (let i = index; i < count(); i++) {
+ if (!isOptionDisabled(state.options[i])) return i;
}
- },
-
- onTouchStart(event) {
- if (this.readonly) {
- return;
- }
-
- this.touchStart(event);
-
- if (this.moving) {
- const translateY = getElementTranslateY(this.$refs.wrapper);
- this.offset = Math.min(0, translateY - this.baseOffset);
- this.startOffset = this.offset;
- } else {
- this.startOffset = this.offset;
- }
-
- this.duration = 0;
- this.transitionEndTrigger = null;
- this.touchStartTime = Date.now();
- this.momentumOffset = this.startOffset;
- },
-
- onTouchMove(event) {
- if (this.readonly) {
- return;
- }
-
- this.touchMove(event);
-
- if (this.direction === 'vertical') {
- this.moving = true;
- preventDefault(event, true);
- }
-
- 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() {
- if (this.readonly) {
- return;
- }
-
- 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;
- }
-
- const index = this.getIndexByOffset(this.offset);
- this.duration = DEFAULT_DURATION;
- this.setIndex(index, true);
-
- // compatible with desktop scenario
- // use setTimeout to skip the click event triggered after touchstart
- setTimeout(() => {
- this.moving = false;
- }, 0);
- },
-
- onTransitionEnd() {
- this.stopMomentum();
- },
-
- onClickItem(index) {
- if (this.moving || this.readonly) {
- return;
- }
-
- this.transitionEndTrigger = null;
- 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;
+ if (!isOptionDisabled(state.options[i])) return i;
}
- },
+ };
- getOptionText(option) {
- if (isObject(option) && this.valueKey in option) {
- return option[this.valueKey];
- }
- return option;
- },
-
- setIndex(index, emitChange) {
- index = this.adjustIndex(index) || 0;
-
- const offset = -index * this.itemHeight;
+ const setIndex = (index, emitChange) => {
+ index = adjustIndex(index) || 0;
+ const offset = -index * props.itemHeight;
const trigger = () => {
- if (index !== this.currentIndex) {
- this.currentIndex = index;
+ if (index !== state.index) {
+ state.index = index;
if (emitChange) {
- this.$emit('change', index);
+ emit('change', index);
}
}
};
// trigger the change event after transitionend when moving
- if (this.moving && offset !== this.offset) {
- this.transitionEndTrigger = trigger;
+ if (moving && offset !== state.offset) {
+ transitionEndTrigger = trigger;
} else {
trigger();
}
- this.offset = offset;
- },
+ state.offset = offset;
+ };
- setValue(value) {
- const { options } = this;
- for (let i = 0; i < options.length; i++) {
- if (this.getOptionText(options[i]) === value) {
- return this.setIndex(i);
- }
+ const setOptions = (options) => {
+ if (JSON.stringify(options) !== JSON.stringify(state.options)) {
+ state.options = deepClone(options);
+ setIndex(props.defaultIndex);
}
- },
+ };
- getValue() {
- return this.options[this.currentIndex];
- },
+ const onClickItem = (index) => {
+ if (moving || props.readonly) {
+ return;
+ }
- getIndexByOffset(offset) {
- return range(Math.round(-offset / this.itemHeight), 0, this.count - 1);
- },
+ transitionEndTrigger = null;
+ state.duration = DEFAULT_DURATION;
+ setIndex(index, true);
+ };
- momentum(distance, duration) {
+ const getOptionText = (option) => {
+ if (isObject(option) && props.valueKey in option) {
+ return option[props.valueKey];
+ }
+ return option;
+ };
+
+ const getIndexByOffset = (offset) =>
+ range(Math.round(-offset / props.itemHeight), 0, count() - 1);
+
+ const momentum = (distance, duration) => {
const speed = Math.abs(distance / duration);
- distance = this.offset + (speed / 0.003) * (distance < 0 ? -1 : 1);
+ distance = state.offset + (speed / 0.003) * (distance < 0 ? -1 : 1);
- const index = this.getIndexByOffset(distance);
+ const index = getIndexByOffset(distance);
- this.duration = +this.swipeDuration;
- this.setIndex(index, true);
- },
+ state.duration = +props.swipeDuration;
+ setIndex(index, true);
+ };
- stopMomentum() {
- this.moving = false;
- this.duration = 0;
+ const stopMomentum = () => {
+ moving = false;
+ state.duration = 0;
- if (this.transitionEndTrigger) {
- this.transitionEndTrigger();
- this.transitionEndTrigger = null;
+ if (transitionEndTrigger) {
+ transitionEndTrigger();
+ transitionEndTrigger = null;
}
- },
+ };
- genOptions() {
+ const onTouchStart = (event) => {
+ if (props.readonly) {
+ return;
+ }
+
+ touch.start(event);
+
+ if (moving) {
+ const translateY = getElementTranslateY(wrapper.value);
+ state.offset = Math.min(0, translateY - baseOffset());
+ startOffset = state.offset;
+ } else {
+ startOffset = state.offset;
+ }
+
+ state.duration = 0;
+ touchStartTime = Date.now();
+ momentumOffset = startOffset;
+ transitionEndTrigger = null;
+ };
+
+ const onTouchMove = (event) => {
+ if (props.readonly) {
+ return;
+ }
+
+ touch.move(event);
+
+ if (touch.isVertical()) {
+ moving = true;
+ preventDefault(event, true);
+ }
+
+ state.offset = range(
+ startOffset + touch.deltaY.value,
+ -(count() * props.itemHeight),
+ props.itemHeight
+ );
+
+ const now = Date.now();
+ if (now - touchStartTime > MOMENTUM_LIMIT_TIME) {
+ touchStartTime = now;
+ momentumOffset = state.offset;
+ }
+ };
+
+ const onTouchEnd = () => {
+ if (props.readonly) {
+ return;
+ }
+
+ const distance = state.offset - momentumOffset;
+ const duration = Date.now() - touchStartTime;
+ const allowMomentum =
+ duration < MOMENTUM_LIMIT_TIME &&
+ Math.abs(distance) > MOMENTUM_LIMIT_DISTANCE;
+
+ if (allowMomentum) {
+ momentum(distance, duration);
+ return;
+ }
+
+ const index = getIndexByOffset(state.offset);
+ state.duration = DEFAULT_DURATION;
+ setIndex(index, true);
+
+ // compatible with desktop scenario
+ // use setTimeout to skip the click event triggered after touchstart
+ setTimeout(() => {
+ moving = false;
+ }, 0);
+ };
+
+ const renderOptions = () => {
const optionStyle = {
- height: `${this.itemHeight}px`,
+ height: `${props.itemHeight}px`,
};
- return this.options.map((option, index) => {
- const text = this.getOptionText(option);
+ return state.options.map((option, index) => {
+ const text = getOptionText(option);
const disabled = isOptionDisabled(option);
const data = {
- style: optionStyle,
role: 'button',
+ style: optionStyle,
tabindex: disabled ? -1 : 0,
- class: [
- bem('item', {
- disabled,
- selected: index === this.currentIndex,
- }),
- ],
+ class: bem('item', {
+ disabled,
+ selected: index === state.index,
+ }),
onClick: () => {
- this.onClickItem(index);
+ onClickItem(index);
},
};
const childData = {
class: 'van-ellipsis',
- [this.allowHtml ? 'innerHTML' : 'textContent']: text,
+ [props.allowHtml ? 'innerHTML' : 'textContent']: text,
};
return (
@@ -305,27 +263,63 @@ export default createComponent({
);
});
- },
- },
-
- render() {
- const wrapperStyle = {
- transform: `translate3d(0, ${this.offset + this.baseOffset}px, 0)`,
- transitionDuration: `${this.duration}ms`,
- transitionProperty: this.duration ? 'all' : 'none',
};
- return (
-
+ const setValue = (value) => {
+ const { options } = state;
+ for (let i = 0; i < options.length; i++) {
+ if (getOptionText(options[i]) === value) {
+ return setIndex(i);
+ }
+ }
+ };
+
+ const getValue = () => state.options[state.index];
+
+ setIndex(state.index);
+
+ useParent(PICKER_KEY, {
+ state,
+ getValue,
+ setValue,
+ setOptions,
+ stopMomentum,
+ });
+
+ watch(() => props.initialOptions, setOptions);
+
+ watch(
+ () => props.defaultIndex,
+ (value) => {
+ setIndex(value);
+ }
);
+
+ return () => {
+ const wrapperStyle = {
+ transform: `translate3d(0, ${state.offset + baseOffset()}px, 0)`,
+ transitionDuration: `${state.duration}ms`,
+ transitionProperty: state.duration ? 'all' : 'none',
+ };
+
+ return (
+
+ );
+ };
},
});
diff --git a/src/picker/index.js b/src/picker/index.js
index afce13dce..721e0ae32 100644
--- a/src/picker/index.js
+++ b/src/picker/index.js
@@ -1,10 +1,15 @@
+import { ref, watch, computed, provide, reactive } from 'vue';
+import { pickerProps, PICKER_KEY, DEFAULT_ITEM_HEIGHT } from './shared';
+
// Utils
import { createNamespace } from '../utils';
import { preventDefault } from '../utils/dom/event';
import { BORDER_UNSET_TOP_BOTTOM } from '../utils/constant';
-import { pickerProps, DEFAULT_ITEM_HEIGHT } from './shared';
import { unitToPx } from '../utils/format/unit';
+// Composition
+import { useExpose } from '../composition/use-expose';
+
// Components
import Loading from '../loading';
import PickerColumn from './PickerColumn';
@@ -34,63 +39,34 @@ export default createComponent({
emits: ['confirm', 'cancel', 'change'],
- data() {
- return {
- children: [],
- formattedColumns: [],
- };
- },
+ setup(props, { emit, slots }) {
+ const children = reactive([]);
+ const formattedColumns = ref([]);
- computed: {
- itemPxHeight() {
- return this.itemHeight ? unitToPx(this.itemHeight) : DEFAULT_ITEM_HEIGHT;
- },
+ const itemHeight = computed(() =>
+ props.itemHeight ? unitToPx(props.itemHeight) : DEFAULT_ITEM_HEIGHT
+ );
- dataType() {
- const { columns } = this;
+ const dataType = computed(() => {
+ const { columns } = props;
const firstColumn = columns[0] || {};
if (firstColumn.children) {
return 'cascade';
}
-
if (firstColumn.values) {
return 'object';
}
-
return 'text';
- },
- },
+ });
- watch: {
- columns: {
- handler() {
- this.format();
- },
- immediate: true,
- },
- },
-
- methods: {
- format() {
- const { columns, dataType } = this;
-
- if (dataType === 'text') {
- this.formattedColumns = [{ values: columns }];
- } else if (dataType === 'cascade') {
- this.formatCascade();
- } else {
- this.formattedColumns = columns;
- }
- },
-
- formatCascade() {
+ const formatCascade = () => {
const formatted = [];
- let cursor = { children: this.columns };
+ let cursor = { children: props.columns };
while (cursor && cursor.children) {
- const defaultIndex = cursor.defaultIndex ?? +this.defaultIndex;
+ const defaultIndex = cursor.defaultIndex ?? +props.defaultIndex;
formatted.push({
values: cursor.children,
@@ -101,20 +77,35 @@ export default createComponent({
cursor = cursor.children[defaultIndex];
}
- this.formattedColumns = formatted;
- },
+ formattedColumns.value = formatted;
+ };
- emit(event) {
- if (this.dataType === 'text') {
- this.$emit(event, this.getColumnValue(0), this.getColumnIndex(0));
+ const format = () => {
+ const { columns } = props;
+
+ if (dataType.value === 'text') {
+ formattedColumns.value = [{ values: columns }];
+ } else if (dataType.value === 'cascade') {
+ formatCascade();
} else {
- this.$emit(event, this.getValues(), this.getIndexes());
+ formattedColumns.value = columns;
}
- },
+ };
- onCascadeChange(columnIndex) {
- let cursor = { children: this.columns };
- const indexes = this.getIndexes();
+ // get indexes of all columns
+ const getIndexes = () => children.map((child) => child.state.index);
+
+ // set options of column by index
+ const setColumnValues = (index, options) => {
+ const column = children[index];
+ if (column) {
+ column.setOptions(options);
+ }
+ };
+
+ const onCascadeChange = (columnIndex) => {
+ let cursor = { children: props.columns };
+ const indexes = getIndexes();
for (let i = 0; i <= columnIndex; i++) {
cursor = cursor.children[indexes[i]];
@@ -122,169 +113,154 @@ export default createComponent({
while (cursor && cursor.children) {
columnIndex++;
- this.setColumnValues(columnIndex, cursor.children);
+ setColumnValues(columnIndex, cursor.children);
cursor = cursor.children[cursor.defaultIndex || 0];
}
- },
-
- onChange(columnIndex) {
- if (this.dataType === 'cascade') {
- this.onCascadeChange(columnIndex);
- }
-
- if (this.dataType === 'text') {
- this.$emit('change', this.getColumnValue(0), this.getColumnIndex(0));
- } else {
- this.$emit('change', this.getValues(), columnIndex);
- }
- },
+ };
// get column instance by index
- getColumn(index) {
- return this.children[index];
- },
+ const getColumn = (index) => children[index];
- // @exposed-api
// get column value by index
- getColumnValue(index) {
- const column = this.getColumn(index);
+ const getColumnValue = (index) => {
+ const column = getColumn(index);
return column && column.getValue();
- },
+ };
- // @exposed-api
// set column value by index
- setColumnValue(index, value) {
- const column = this.getColumn(index);
+ const setColumnValue = (index, value) => {
+ const column = getColumn(index);
if (column) {
column.setValue(value);
- if (this.dataType === 'cascade') {
- this.onCascadeChange(index);
+ if (dataType.value === 'cascade') {
+ onCascadeChange(index);
}
}
- },
+ };
- // @exposed-api
// get column option index by column index
- getColumnIndex(columnIndex) {
- return (this.getColumn(columnIndex) || {}).currentIndex;
- },
+ const getColumnIndex = (index) => (getColumn(index) || {}).state.index;
- // @exposed-api
// set column option index by column index
- setColumnIndex(columnIndex, optionIndex) {
- const column = this.getColumn(columnIndex);
+ const setColumnIndex = (columnIndex, optionIndex) => {
+ const column = getColumn(columnIndex);
if (column) {
column.setIndex(optionIndex);
-
- if (this.dataType === 'cascade') {
- this.onCascadeChange(columnIndex);
+ if (props.dataType === 'cascade') {
+ onCascadeChange(columnIndex);
}
}
- },
+ };
- // @exposed-api
// get options of column by index
- getColumnValues(index) {
- return (this.children[index] || {}).options;
- },
+ const getColumnValues = (index) => (children[index] || {}).state.options;
- // @exposed-api
- // set options of column by index
- setColumnValues(index, options) {
- const column = this.children[index];
-
- if (column) {
- column.setOptions(options);
- }
- },
-
- // @exposed-api
// get values of all columns
- getValues() {
- return this.children.map((child) => child.getValue());
- },
+ const getValues = () => children.map((child) => child.getValue());
- // @exposed-api
// set values of all columns
- setValues(values) {
+ const setValues = (values) => {
values.forEach((value, index) => {
- this.setColumnValue(index, value);
+ setColumnValue(index, value);
});
- },
+ };
- // @exposed-api
- // get indexes of all columns
- getIndexes() {
- return this.children.map((child) => child.currentIndex);
- },
-
- // @exposed-api
// set indexes of all columns
- setIndexes(indexes) {
+ const setIndexes = (indexes) => {
indexes.forEach((optionIndex, columnIndex) => {
- this.setColumnIndex(columnIndex, optionIndex);
+ setColumnIndex(columnIndex, optionIndex);
});
- },
+ };
- // @exposed-api
- confirm() {
- this.children.forEach((child) => child.stopMomentum());
- this.emit('confirm');
- },
+ const emitAction = (event) => {
+ if (dataType.value === 'text') {
+ emit(event, getColumnValue(0), getColumnIndex(0));
+ } else {
+ emit(event, getValues(), getIndexes());
+ }
+ };
- cancel() {
- this.emit('cancel');
- },
-
- genTitle() {
- if (this.$slots.title) {
- return this.$slots.title();
+ const onChange = (columnIndex) => {
+ if (dataType.value === 'cascade') {
+ onCascadeChange(columnIndex);
}
- if (this.title) {
- return {this.title}
;
+ if (dataType.value === 'text') {
+ emit('change', getColumnValue(0), getColumnIndex(0));
+ } else {
+ emit('change', getValues(), columnIndex);
}
- },
+ };
- genToolbar() {
- if (this.showToolbar) {
+ const confirm = () => {
+ children.forEach((child) => child.stopMomentum());
+ emitAction('confirm');
+ };
+
+ const cancel = () => {
+ emitAction('cancel');
+ };
+
+ const renderTitle = () => {
+ if (slots.title) {
+ return slots.title();
+ }
+ if (props.title) {
+ return {props.title}
;
+ }
+ };
+
+ const renderToolbar = () => {
+ if (props.showToolbar) {
return (
- {this.$slots.default
- ? this.$slots.default()
+ {slots.default
+ ? slots.default()
: [
-
);
}
- },
+ };
- genColumns() {
- const { itemPxHeight } = this;
- const wrapHeight = itemPxHeight * this.visibleItemCount;
+ const renderColumnItems = () =>
+ formattedColumns.value.map((item, columnIndex) => (
+ {
+ onChange(columnIndex);
+ }}
+ />
+ ));
- const frameStyle = { height: `${itemPxHeight}px` };
+ const renderColumns = () => {
+ const wrapHeight = itemHeight.value * props.visibleItemCount;
+ const frameStyle = { height: `${itemHeight.value}px` };
const columnsStyle = { height: `${wrapHeight}px` };
const maskStyle = {
- backgroundSize: `100% ${(wrapHeight - itemPxHeight) / 2}px`,
+ backgroundSize: `100% ${(wrapHeight - itemHeight.value) / 2}px`,
};
return (
@@ -293,7 +269,7 @@ export default createComponent({
style={columnsStyle}
onTouchmove={preventDefault}
>
- {this.genColumnItems()}
+ {renderColumnItems()}
);
- },
+ };
- genColumnItems() {
- return this.formattedColumns.map((item, columnIndex) => (
- {
- this.onChange(columnIndex);
- }}
- />
- ));
- },
- },
+ provide(PICKER_KEY, { children });
- render() {
- return (
+ watch(() => props.columns, format, { immediate: true });
+
+ useExpose({
+ confirm,
+ getValues,
+ setValues,
+ getIndexes,
+ setIndexes,
+ getColumnIndex,
+ setColumnIndex,
+ getColumnValue,
+ setColumnValue,
+ getColumnValues,
+ setColumnValues,
+ });
+
+ return () => (
- {this.toolbarPosition === 'top' ? this.genToolbar() : null}
- {this.loading ? : null}
- {this.$slots['columns-top']?.()}
- {this.genColumns()}
- {this.$slots['columns-bottom']?.()}
- {this.toolbarPosition === 'bottom' ? this.genToolbar() : null}
+ {props.toolbarPosition === 'top' ? renderToolbar() : null}
+ {props.loading ? : null}
+ {slots['columns-top']?.()}
+ {renderColumns()}
+ {slots['columns-bottom']?.()}
+ {props.toolbarPosition === 'bottom' ? renderToolbar() : null}
);
},
diff --git a/src/picker/shared.ts b/src/picker/shared.ts
index 0a59b3dfe..b9ac9f9f3 100644
--- a/src/picker/shared.ts
+++ b/src/picker/shared.ts
@@ -8,6 +8,8 @@ export type SharedPickerProps = {
confirmButtonText?: string;
};
+export const PICKER_KEY = 'vanPicker';
+
export const DEFAULT_ITEM_HEIGHT = 44;
export const pickerProps = {