mirror of
https://gitee.com/vant-contrib/vant.git
synced 2025-04-06 03:57:59 +08:00
refactor(Picker): refactor with composition api
This commit is contained in:
parent
971c16eb3c
commit
85d0d423eb
@ -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 { deepClone } from '../utils/deep-clone';
|
||||||
import { createNamespace, isObject } from '../utils';
|
import { createNamespace, isObject } from '../utils';
|
||||||
import { range } from '../utils/format/number';
|
|
||||||
import { preventDefault } from '../utils/dom/event';
|
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;
|
const DEFAULT_DURATION = 200;
|
||||||
|
|
||||||
@ -27,8 +34,6 @@ function isOptionDisabled(option) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default createComponent({
|
export default createComponent({
|
||||||
mixins: [TouchMixin],
|
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
valueKey: String,
|
valueKey: String,
|
||||||
readonly: Boolean,
|
readonly: Boolean,
|
||||||
@ -46,257 +51,210 @@ export default createComponent({
|
|||||||
|
|
||||||
emits: ['change'],
|
emits: ['change'],
|
||||||
|
|
||||||
data() {
|
setup(props, { emit }) {
|
||||||
return {
|
let moving;
|
||||||
|
let startOffset;
|
||||||
|
let touchStartTime;
|
||||||
|
let momentumOffset;
|
||||||
|
let transitionEndTrigger;
|
||||||
|
|
||||||
|
const wrapper = ref();
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
index: props.defaultIndex,
|
||||||
offset: 0,
|
offset: 0,
|
||||||
duration: 0,
|
duration: 0,
|
||||||
options: deepClone(this.initialOptions),
|
options: deepClone(props.initialOptions),
|
||||||
currentIndex: this.defaultIndex,
|
});
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
created() {
|
const touch = useTouch();
|
||||||
if (this.$parent.children) {
|
|
||||||
this.$parent.children.push(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setIndex(this.currentIndex);
|
const count = () => state.options.length;
|
||||||
},
|
|
||||||
|
|
||||||
mounted() {
|
const baseOffset = () =>
|
||||||
this.bindTouchEvent(this.$el);
|
(props.itemHeight * (props.visibleItemCount - 1)) / 2;
|
||||||
},
|
|
||||||
|
|
||||||
unmounted() {
|
const adjustIndex = (index) => {
|
||||||
const { children } = this.$parent;
|
index = range(index, 0, count());
|
||||||
|
|
||||||
if (children) {
|
for (let i = index; i < count(); i++) {
|
||||||
children.splice(children.indexOf(this), 1);
|
if (!isOptionDisabled(state.options[i])) return i;
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
|
|
||||||
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--) {
|
for (let i = index - 1; i >= 0; i--) {
|
||||||
if (!isOptionDisabled(this.options[i])) return i;
|
if (!isOptionDisabled(state.options[i])) return i;
|
||||||
}
|
}
|
||||||
},
|
};
|
||||||
|
|
||||||
getOptionText(option) {
|
const setIndex = (index, emitChange) => {
|
||||||
if (isObject(option) && this.valueKey in option) {
|
index = adjustIndex(index) || 0;
|
||||||
return option[this.valueKey];
|
|
||||||
}
|
|
||||||
return option;
|
|
||||||
},
|
|
||||||
|
|
||||||
setIndex(index, emitChange) {
|
|
||||||
index = this.adjustIndex(index) || 0;
|
|
||||||
|
|
||||||
const offset = -index * this.itemHeight;
|
|
||||||
|
|
||||||
|
const offset = -index * props.itemHeight;
|
||||||
const trigger = () => {
|
const trigger = () => {
|
||||||
if (index !== this.currentIndex) {
|
if (index !== state.index) {
|
||||||
this.currentIndex = index;
|
state.index = index;
|
||||||
|
|
||||||
if (emitChange) {
|
if (emitChange) {
|
||||||
this.$emit('change', index);
|
emit('change', index);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// trigger the change event after transitionend when moving
|
// trigger the change event after transitionend when moving
|
||||||
if (this.moving && offset !== this.offset) {
|
if (moving && offset !== state.offset) {
|
||||||
this.transitionEndTrigger = trigger;
|
transitionEndTrigger = trigger;
|
||||||
} else {
|
} else {
|
||||||
trigger();
|
trigger();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.offset = offset;
|
state.offset = offset;
|
||||||
},
|
};
|
||||||
|
|
||||||
setValue(value) {
|
const setOptions = (options) => {
|
||||||
const { options } = this;
|
if (JSON.stringify(options) !== JSON.stringify(state.options)) {
|
||||||
for (let i = 0; i < options.length; i++) {
|
state.options = deepClone(options);
|
||||||
if (this.getOptionText(options[i]) === value) {
|
setIndex(props.defaultIndex);
|
||||||
return this.setIndex(i);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
};
|
||||||
|
|
||||||
getValue() {
|
const onClickItem = (index) => {
|
||||||
return this.options[this.currentIndex];
|
if (moving || props.readonly) {
|
||||||
},
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
getIndexByOffset(offset) {
|
transitionEndTrigger = null;
|
||||||
return range(Math.round(-offset / this.itemHeight), 0, this.count - 1);
|
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);
|
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;
|
state.duration = +props.swipeDuration;
|
||||||
this.setIndex(index, true);
|
setIndex(index, true);
|
||||||
},
|
};
|
||||||
|
|
||||||
stopMomentum() {
|
const stopMomentum = () => {
|
||||||
this.moving = false;
|
moving = false;
|
||||||
this.duration = 0;
|
state.duration = 0;
|
||||||
|
|
||||||
if (this.transitionEndTrigger) {
|
if (transitionEndTrigger) {
|
||||||
this.transitionEndTrigger();
|
transitionEndTrigger();
|
||||||
this.transitionEndTrigger = null;
|
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 = {
|
const optionStyle = {
|
||||||
height: `${this.itemHeight}px`,
|
height: `${props.itemHeight}px`,
|
||||||
};
|
};
|
||||||
|
|
||||||
return this.options.map((option, index) => {
|
return state.options.map((option, index) => {
|
||||||
const text = this.getOptionText(option);
|
const text = getOptionText(option);
|
||||||
const disabled = isOptionDisabled(option);
|
const disabled = isOptionDisabled(option);
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
style: optionStyle,
|
|
||||||
role: 'button',
|
role: 'button',
|
||||||
|
style: optionStyle,
|
||||||
tabindex: disabled ? -1 : 0,
|
tabindex: disabled ? -1 : 0,
|
||||||
class: [
|
class: bem('item', {
|
||||||
bem('item', {
|
disabled,
|
||||||
disabled,
|
selected: index === state.index,
|
||||||
selected: index === this.currentIndex,
|
}),
|
||||||
}),
|
|
||||||
],
|
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
this.onClickItem(index);
|
onClickItem(index);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const childData = {
|
const childData = {
|
||||||
class: 'van-ellipsis',
|
class: 'van-ellipsis',
|
||||||
[this.allowHtml ? 'innerHTML' : 'textContent']: text,
|
[props.allowHtml ? 'innerHTML' : 'textContent']: text,
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -305,27 +263,63 @@ export default createComponent({
|
|||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
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) => {
|
||||||
<div class={[bem(), this.className]}>
|
const { options } = state;
|
||||||
<ul
|
for (let i = 0; i < options.length; i++) {
|
||||||
ref="wrapper"
|
if (getOptionText(options[i]) === value) {
|
||||||
style={wrapperStyle}
|
return setIndex(i);
|
||||||
class={bem('wrapper')}
|
}
|
||||||
onTransitionend={this.onTransitionEnd}
|
}
|
||||||
>
|
};
|
||||||
{this.genOptions()}
|
|
||||||
</ul>
|
const getValue = () => state.options[state.index];
|
||||||
</div>
|
|
||||||
|
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 (
|
||||||
|
<div
|
||||||
|
class={[bem(), props.className]}
|
||||||
|
onTouchstart={onTouchStart}
|
||||||
|
onTouchmove={onTouchMove}
|
||||||
|
onTouchend={onTouchEnd}
|
||||||
|
onTouchcancel={onTouchEnd}
|
||||||
|
>
|
||||||
|
<ul
|
||||||
|
ref={wrapper}
|
||||||
|
style={wrapperStyle}
|
||||||
|
class={bem('wrapper')}
|
||||||
|
onTransitionend={stopMomentum}
|
||||||
|
>
|
||||||
|
{renderOptions()}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -1,10 +1,15 @@
|
|||||||
|
import { ref, watch, computed, provide, reactive } from 'vue';
|
||||||
|
import { pickerProps, PICKER_KEY, DEFAULT_ITEM_HEIGHT } from './shared';
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
import { createNamespace } from '../utils';
|
import { createNamespace } from '../utils';
|
||||||
import { preventDefault } from '../utils/dom/event';
|
import { preventDefault } from '../utils/dom/event';
|
||||||
import { BORDER_UNSET_TOP_BOTTOM } from '../utils/constant';
|
import { BORDER_UNSET_TOP_BOTTOM } from '../utils/constant';
|
||||||
import { pickerProps, DEFAULT_ITEM_HEIGHT } from './shared';
|
|
||||||
import { unitToPx } from '../utils/format/unit';
|
import { unitToPx } from '../utils/format/unit';
|
||||||
|
|
||||||
|
// Composition
|
||||||
|
import { useExpose } from '../composition/use-expose';
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
import Loading from '../loading';
|
import Loading from '../loading';
|
||||||
import PickerColumn from './PickerColumn';
|
import PickerColumn from './PickerColumn';
|
||||||
@ -34,63 +39,34 @@ export default createComponent({
|
|||||||
|
|
||||||
emits: ['confirm', 'cancel', 'change'],
|
emits: ['confirm', 'cancel', 'change'],
|
||||||
|
|
||||||
data() {
|
setup(props, { emit, slots }) {
|
||||||
return {
|
const children = reactive([]);
|
||||||
children: [],
|
const formattedColumns = ref([]);
|
||||||
formattedColumns: [],
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
const itemHeight = computed(() =>
|
||||||
itemPxHeight() {
|
props.itemHeight ? unitToPx(props.itemHeight) : DEFAULT_ITEM_HEIGHT
|
||||||
return this.itemHeight ? unitToPx(this.itemHeight) : DEFAULT_ITEM_HEIGHT;
|
);
|
||||||
},
|
|
||||||
|
|
||||||
dataType() {
|
const dataType = computed(() => {
|
||||||
const { columns } = this;
|
const { columns } = props;
|
||||||
const firstColumn = columns[0] || {};
|
const firstColumn = columns[0] || {};
|
||||||
|
|
||||||
if (firstColumn.children) {
|
if (firstColumn.children) {
|
||||||
return 'cascade';
|
return 'cascade';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (firstColumn.values) {
|
if (firstColumn.values) {
|
||||||
return 'object';
|
return 'object';
|
||||||
}
|
}
|
||||||
|
|
||||||
return 'text';
|
return 'text';
|
||||||
},
|
});
|
||||||
},
|
|
||||||
|
|
||||||
watch: {
|
const formatCascade = () => {
|
||||||
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 formatted = [];
|
const formatted = [];
|
||||||
|
|
||||||
let cursor = { children: this.columns };
|
let cursor = { children: props.columns };
|
||||||
|
|
||||||
while (cursor && cursor.children) {
|
while (cursor && cursor.children) {
|
||||||
const defaultIndex = cursor.defaultIndex ?? +this.defaultIndex;
|
const defaultIndex = cursor.defaultIndex ?? +props.defaultIndex;
|
||||||
|
|
||||||
formatted.push({
|
formatted.push({
|
||||||
values: cursor.children,
|
values: cursor.children,
|
||||||
@ -101,20 +77,35 @@ export default createComponent({
|
|||||||
cursor = cursor.children[defaultIndex];
|
cursor = cursor.children[defaultIndex];
|
||||||
}
|
}
|
||||||
|
|
||||||
this.formattedColumns = formatted;
|
formattedColumns.value = formatted;
|
||||||
},
|
};
|
||||||
|
|
||||||
emit(event) {
|
const format = () => {
|
||||||
if (this.dataType === 'text') {
|
const { columns } = props;
|
||||||
this.$emit(event, this.getColumnValue(0), this.getColumnIndex(0));
|
|
||||||
|
if (dataType.value === 'text') {
|
||||||
|
formattedColumns.value = [{ values: columns }];
|
||||||
|
} else if (dataType.value === 'cascade') {
|
||||||
|
formatCascade();
|
||||||
} else {
|
} else {
|
||||||
this.$emit(event, this.getValues(), this.getIndexes());
|
formattedColumns.value = columns;
|
||||||
}
|
}
|
||||||
},
|
};
|
||||||
|
|
||||||
onCascadeChange(columnIndex) {
|
// get indexes of all columns
|
||||||
let cursor = { children: this.columns };
|
const getIndexes = () => children.map((child) => child.state.index);
|
||||||
const indexes = this.getIndexes();
|
|
||||||
|
// 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++) {
|
for (let i = 0; i <= columnIndex; i++) {
|
||||||
cursor = cursor.children[indexes[i]];
|
cursor = cursor.children[indexes[i]];
|
||||||
@ -122,169 +113,154 @@ export default createComponent({
|
|||||||
|
|
||||||
while (cursor && cursor.children) {
|
while (cursor && cursor.children) {
|
||||||
columnIndex++;
|
columnIndex++;
|
||||||
this.setColumnValues(columnIndex, cursor.children);
|
setColumnValues(columnIndex, cursor.children);
|
||||||
cursor = cursor.children[cursor.defaultIndex || 0];
|
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
|
// get column instance by index
|
||||||
getColumn(index) {
|
const getColumn = (index) => children[index];
|
||||||
return this.children[index];
|
|
||||||
},
|
|
||||||
|
|
||||||
// @exposed-api
|
|
||||||
// get column value by index
|
// get column value by index
|
||||||
getColumnValue(index) {
|
const getColumnValue = (index) => {
|
||||||
const column = this.getColumn(index);
|
const column = getColumn(index);
|
||||||
return column && column.getValue();
|
return column && column.getValue();
|
||||||
},
|
};
|
||||||
|
|
||||||
// @exposed-api
|
|
||||||
// set column value by index
|
// set column value by index
|
||||||
setColumnValue(index, value) {
|
const setColumnValue = (index, value) => {
|
||||||
const column = this.getColumn(index);
|
const column = getColumn(index);
|
||||||
|
|
||||||
if (column) {
|
if (column) {
|
||||||
column.setValue(value);
|
column.setValue(value);
|
||||||
|
|
||||||
if (this.dataType === 'cascade') {
|
if (dataType.value === 'cascade') {
|
||||||
this.onCascadeChange(index);
|
onCascadeChange(index);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
};
|
||||||
|
|
||||||
// @exposed-api
|
|
||||||
// get column option index by column index
|
// get column option index by column index
|
||||||
getColumnIndex(columnIndex) {
|
const getColumnIndex = (index) => (getColumn(index) || {}).state.index;
|
||||||
return (this.getColumn(columnIndex) || {}).currentIndex;
|
|
||||||
},
|
|
||||||
|
|
||||||
// @exposed-api
|
|
||||||
// set column option index by column index
|
// set column option index by column index
|
||||||
setColumnIndex(columnIndex, optionIndex) {
|
const setColumnIndex = (columnIndex, optionIndex) => {
|
||||||
const column = this.getColumn(columnIndex);
|
const column = getColumn(columnIndex);
|
||||||
|
|
||||||
if (column) {
|
if (column) {
|
||||||
column.setIndex(optionIndex);
|
column.setIndex(optionIndex);
|
||||||
|
if (props.dataType === 'cascade') {
|
||||||
if (this.dataType === 'cascade') {
|
onCascadeChange(columnIndex);
|
||||||
this.onCascadeChange(columnIndex);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
};
|
||||||
|
|
||||||
// @exposed-api
|
|
||||||
// get options of column by index
|
// get options of column by index
|
||||||
getColumnValues(index) {
|
const getColumnValues = (index) => (children[index] || {}).state.options;
|
||||||
return (this.children[index] || {}).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
|
// get values of all columns
|
||||||
getValues() {
|
const getValues = () => children.map((child) => child.getValue());
|
||||||
return this.children.map((child) => child.getValue());
|
|
||||||
},
|
|
||||||
|
|
||||||
// @exposed-api
|
|
||||||
// set values of all columns
|
// set values of all columns
|
||||||
setValues(values) {
|
const setValues = (values) => {
|
||||||
values.forEach((value, index) => {
|
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
|
// set indexes of all columns
|
||||||
setIndexes(indexes) {
|
const setIndexes = (indexes) => {
|
||||||
indexes.forEach((optionIndex, columnIndex) => {
|
indexes.forEach((optionIndex, columnIndex) => {
|
||||||
this.setColumnIndex(columnIndex, optionIndex);
|
setColumnIndex(columnIndex, optionIndex);
|
||||||
});
|
});
|
||||||
},
|
};
|
||||||
|
|
||||||
// @exposed-api
|
const emitAction = (event) => {
|
||||||
confirm() {
|
if (dataType.value === 'text') {
|
||||||
this.children.forEach((child) => child.stopMomentum());
|
emit(event, getColumnValue(0), getColumnIndex(0));
|
||||||
this.emit('confirm');
|
} else {
|
||||||
},
|
emit(event, getValues(), getIndexes());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
cancel() {
|
const onChange = (columnIndex) => {
|
||||||
this.emit('cancel');
|
if (dataType.value === 'cascade') {
|
||||||
},
|
onCascadeChange(columnIndex);
|
||||||
|
|
||||||
genTitle() {
|
|
||||||
if (this.$slots.title) {
|
|
||||||
return this.$slots.title();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.title) {
|
if (dataType.value === 'text') {
|
||||||
return <div class={['van-ellipsis', bem('title')]}>{this.title}</div>;
|
emit('change', getColumnValue(0), getColumnIndex(0));
|
||||||
|
} else {
|
||||||
|
emit('change', getValues(), columnIndex);
|
||||||
}
|
}
|
||||||
},
|
};
|
||||||
|
|
||||||
genToolbar() {
|
const confirm = () => {
|
||||||
if (this.showToolbar) {
|
children.forEach((child) => child.stopMomentum());
|
||||||
|
emitAction('confirm');
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancel = () => {
|
||||||
|
emitAction('cancel');
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderTitle = () => {
|
||||||
|
if (slots.title) {
|
||||||
|
return slots.title();
|
||||||
|
}
|
||||||
|
if (props.title) {
|
||||||
|
return <div class={[bem('title'), 'van-ellipsis']}>{props.title}</div>;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderToolbar = () => {
|
||||||
|
if (props.showToolbar) {
|
||||||
return (
|
return (
|
||||||
<div class={bem('toolbar')}>
|
<div class={bem('toolbar')}>
|
||||||
{this.$slots.default
|
{slots.default
|
||||||
? this.$slots.default()
|
? slots.default()
|
||||||
: [
|
: [
|
||||||
<button
|
<button type="button" class={bem('cancel')} onClick={cancel}>
|
||||||
type="button"
|
{props.cancelButtonText || t('cancel')}
|
||||||
class={bem('cancel')}
|
|
||||||
onClick={this.cancel}
|
|
||||||
>
|
|
||||||
{this.cancelButtonText || t('cancel')}
|
|
||||||
</button>,
|
</button>,
|
||||||
this.genTitle(),
|
renderTitle(),
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class={bem('confirm')}
|
class={bem('confirm')}
|
||||||
onClick={this.confirm}
|
onClick={confirm}
|
||||||
>
|
>
|
||||||
{this.confirmButtonText || t('confirm')}
|
{props.confirmButtonText || t('confirm')}
|
||||||
</button>,
|
</button>,
|
||||||
]}
|
]}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
};
|
||||||
|
|
||||||
genColumns() {
|
const renderColumnItems = () =>
|
||||||
const { itemPxHeight } = this;
|
formattedColumns.value.map((item, columnIndex) => (
|
||||||
const wrapHeight = itemPxHeight * this.visibleItemCount;
|
<PickerColumn
|
||||||
|
readonly={props.readonly}
|
||||||
|
valueKey={props.valueKey}
|
||||||
|
allowHtml={props.allowHtml}
|
||||||
|
className={item.className}
|
||||||
|
itemHeight={itemHeight.value}
|
||||||
|
defaultIndex={item.defaultIndex ?? +props.defaultIndex}
|
||||||
|
swipeDuration={props.swipeDuration}
|
||||||
|
visibleItemCount={props.visibleItemCount}
|
||||||
|
initialOptions={item.values}
|
||||||
|
onChange={() => {
|
||||||
|
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 columnsStyle = { height: `${wrapHeight}px` };
|
||||||
const maskStyle = {
|
const maskStyle = {
|
||||||
backgroundSize: `100% ${(wrapHeight - itemPxHeight) / 2}px`,
|
backgroundSize: `100% ${(wrapHeight - itemHeight.value) / 2}px`,
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -293,7 +269,7 @@ export default createComponent({
|
|||||||
style={columnsStyle}
|
style={columnsStyle}
|
||||||
onTouchmove={preventDefault}
|
onTouchmove={preventDefault}
|
||||||
>
|
>
|
||||||
{this.genColumnItems()}
|
{renderColumnItems()}
|
||||||
<div class={bem('mask')} style={maskStyle} />
|
<div class={bem('mask')} style={maskStyle} />
|
||||||
<div
|
<div
|
||||||
class={[BORDER_UNSET_TOP_BOTTOM, bem('frame')]}
|
class={[BORDER_UNSET_TOP_BOTTOM, bem('frame')]}
|
||||||
@ -301,37 +277,34 @@ export default createComponent({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
};
|
||||||
|
|
||||||
genColumnItems() {
|
provide(PICKER_KEY, { children });
|
||||||
return this.formattedColumns.map((item, columnIndex) => (
|
|
||||||
<PickerColumn
|
|
||||||
readonly={this.readonly}
|
|
||||||
valueKey={this.valueKey}
|
|
||||||
allowHtml={this.allowHtml}
|
|
||||||
className={item.className}
|
|
||||||
itemHeight={this.itemPxHeight}
|
|
||||||
defaultIndex={item.defaultIndex ?? +this.defaultIndex}
|
|
||||||
swipeDuration={this.swipeDuration}
|
|
||||||
visibleItemCount={this.visibleItemCount}
|
|
||||||
initialOptions={item.values}
|
|
||||||
onChange={() => {
|
|
||||||
this.onChange(columnIndex);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
render() {
|
watch(() => props.columns, format, { immediate: true });
|
||||||
return (
|
|
||||||
|
useExpose({
|
||||||
|
confirm,
|
||||||
|
getValues,
|
||||||
|
setValues,
|
||||||
|
getIndexes,
|
||||||
|
setIndexes,
|
||||||
|
getColumnIndex,
|
||||||
|
setColumnIndex,
|
||||||
|
getColumnValue,
|
||||||
|
setColumnValue,
|
||||||
|
getColumnValues,
|
||||||
|
setColumnValues,
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => (
|
||||||
<div class={bem()}>
|
<div class={bem()}>
|
||||||
{this.toolbarPosition === 'top' ? this.genToolbar() : null}
|
{props.toolbarPosition === 'top' ? renderToolbar() : null}
|
||||||
{this.loading ? <Loading class={bem('loading')} /> : null}
|
{props.loading ? <Loading class={bem('loading')} /> : null}
|
||||||
{this.$slots['columns-top']?.()}
|
{slots['columns-top']?.()}
|
||||||
{this.genColumns()}
|
{renderColumns()}
|
||||||
{this.$slots['columns-bottom']?.()}
|
{slots['columns-bottom']?.()}
|
||||||
{this.toolbarPosition === 'bottom' ? this.genToolbar() : null}
|
{props.toolbarPosition === 'bottom' ? renderToolbar() : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -8,6 +8,8 @@ export type SharedPickerProps = {
|
|||||||
confirmButtonText?: string;
|
confirmButtonText?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const PICKER_KEY = 'vanPicker';
|
||||||
|
|
||||||
export const DEFAULT_ITEM_HEIGHT = 44;
|
export const DEFAULT_ITEM_HEIGHT = 44;
|
||||||
|
|
||||||
export const pickerProps = {
|
export const pickerProps = {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user