-
picker html结构
-
Picker index.js
// 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'; // Components import Loading from '../loading'; import PickerColumn from './PickerColumn'; const [createComponent, bem, t] = createNamespace('picker'); export default createComponent({ props: { ...pickerProps, defaultIndex: { type: [Number, String], default: 0, }, columns: { type: Array, default: () => [], }, toolbarPosition: { type: String, default: 'top', }, valueKey: { type: String, default: 'text', }, }, data() { return { children: [], formattedColumns: [], }; }, computed: { itemPxHeight() { return this.itemHeight ? unitToPx(this.itemHeight) : DEFAULT_ITEM_HEIGHT; }, dataType() { const { columns } = this; const firstColumn = columns[0] || {}; if (firstColumn.children) { return 'cascade'; } if (firstColumn.values) { return 'object'; } return 'text'; }, }, watch: { columns: { handler: '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 = []; let cursor = { children: this.columns }; while (cursor && cursor.children) { const { children } = cursor; let defaultIndex = cursor.defaultIndex ?? +this.defaultIndex; while (children[defaultIndex] && children[defaultIndex].disabled) { if (defaultIndex < children.length - 1) { defaultIndex++; } else { defaultIndex = 0; break; } } formatted.push({ values: cursor.children, className: cursor.className, defaultIndex, }); cursor = children[defaultIndex]; } this.formattedColumns = formatted; }, emit(event) { if (this.dataType === 'text') { this.$emit(event, this.getColumnValue(0), this.getColumnIndex(0)); } else { let values = this.getValues(); // compatible with old version of wrong parameters // should be removed in next major version // see: https://github.com/vant-ui/vant/issues/5905 if (this.dataType === 'cascade') { values = values.map((item) => item[this.valueKey]); } this.$emit(event, values, this.getIndexes()); } }, onCascadeChange(columnIndex) { let cursor = { children: this.columns }; const indexes = this.getIndexes(); for (let i = 0; i <= columnIndex; i++) { cursor = cursor.children[indexes[i]]; } while (cursor && cursor.children) { columnIndex++; this.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, this.getColumnValue(0), this.getColumnIndex(0) ); } else { let values = this.getValues(); // compatible with old version of wrong parameters // should be removed in next major version // see: https://github.com/vant-ui/vant/issues/5905 if (this.dataType === 'cascade') { values = values.map((item) => item[this.valueKey]); } this.$emit('change', this, values, columnIndex); } }, // get column instance by index getColumn(index) { return this.children[index]; }, // @exposed-api // get column value by index getColumnValue(index) { const column = this.getColumn(index); return column && column.getValue(); }, // @exposed-api // set column value by index setColumnValue(index, value) { const column = this.getColumn(index); if (column) { column.setValue(value); if (this.dataType === 'cascade') { this.onCascadeChange(index); } } }, // @exposed-api // get column option index by column index getColumnIndex(columnIndex) { return (this.getColumn(columnIndex) || {}).currentIndex; }, // @exposed-api // set column option index by column index setColumnIndex(columnIndex, optionIndex) { const column = this.getColumn(columnIndex); if (column) { column.setIndex(optionIndex); if (this.dataType === 'cascade') { this.onCascadeChange(columnIndex); } } }, // @exposed-api // get options of column by index getColumnValues(index) { 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 getValues() { return this.children.map((child) => child.getValue()); }, // @exposed-api // set values of all columns setValues(values) { values.forEach((value, index) => { this.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) { indexes.forEach((optionIndex, columnIndex) => { this.setColumnIndex(columnIndex, optionIndex); }); }, // @exposed-api confirm() { this.children.forEach((child) => child.stopMomentum()); this.emit('confirm'); }, cancel() { this.emit('cancel'); }, genTitle() { const titleSlot = this.slots('title'); if (titleSlot) { return titleSlot; } if (this.title) { return <div class={['van-ellipsis', bem('title')]}>{this.title}</div>; } }, genCancel() { return ( <button type="button" class={bem('cancel')} onClick={this.cancel}> {this.slots('cancel') || this.cancelButtonText || t('cancel')} </button> ); }, genConfirm() { return ( <button type="button" class={bem('confirm')} onClick={this.confirm}> {this.slots('confirm') || this.confirmButtonText || t('confirm')} </button> ); }, genToolbar() { if (this.showToolbar) { return ( //class van-picker__toolbar <div class={bem('toolbar')}> {this.slots() || [ this.genCancel(), this.genTitle(), this.genConfirm(), ]} </div> ); } }, genColumns() { const { itemPxHeight } = this; //高度 const wrapHeight = itemPxHeight * this.visibleItemCount; const frameStyle = { height: `${itemPxHeight}px` }; const columnsStyle = { height: `${wrapHeight}px` }; const maskStyle = { //宽度、高度 backgroundSize: `100% ${(wrapHeight - itemPxHeight) / 2}px`, }; return ( //阻止事件冒泡 <div class={bem('columns')} style={columnsStyle} // onTouchmove={preventDefault} > {this.genColumnItems()} {/*渐变背景*/} <div class={bem('mask')} style={maskStyle} /> {/*中间选中背景*/} <div class={[BORDER_UNSET_TOP_BOTTOM, bem('frame')]} style={frameStyle} /> { /* 1,后面两个div中的css样式设置了:pointer-events: none,会导致后面两个div z-index优先级虽然高,但是不能响应所有事件 2,所有touch、点击事件都会落在genColumnItems中的div上面 */ } </div> ); }, genColumnItems() { //增加column 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} scopedSlots={{ option: this.$scopedSlots.option, }} onChange={() => { this.onChange(columnIndex); }} /> )); }, }, render(h) { return ( //clsss van-picker <div class={bem()}> {/*顶部工具栏*/} {this.toolbarPosition === 'top' ? this.genToolbar() : h()} {/*Loading*/} {this.loading ? <Loading class={bem('loading')} /> : h()} {/**/} {this.slots('columns-top')} {this.genColumns()} {this.slots('columns-bottom')} {this.toolbarPosition === 'bottom' ? this.genToolbar() : h()} </div> ); }, });
-
PickerColumn.js
import { deepClone } from '../utils/deep-clone'; import { createNamespace, inBrowser, isObject } from '../utils'; import { range } from '../utils/format/number'; import { preventDefault, on, off } from '../utils/dom/event'; import { TouchMixin } from '../mixins/touch'; const DEFAULT_DURATION = 200; // 惯性滑动思路: // 在手指离开屏幕时,如果和上一次 move 时的间隔小于 `MOMENTUM_LIMIT_TIME` 且 move // 距离大于 `MOMENTUM_LIMIT_DISTANCE` 时,执行惯性滑动 export const MOMENTUM_LIMIT_TIME = 300; export const MOMENTUM_LIMIT_DISTANCE = 15; const [createComponent, bem] = createNamespace('picker-column'); function getElementTranslateY(element) { const style = window.getComputedStyle(element); const transform = style.transform || style.webkitTransform; const translateY = transform.slice(7, transform.length - 1).split(', ')[5]; return Number(translateY); } function isOptionDisabled(option) { return isObject(option) && option.disabled; } // use standard WheelEvent: // https://developer.mozilla.org/en-US/docs/Web/API/WheelEvent const supportMousewheel = inBrowser && 'onwheel' in window; let mousewheelTimer = null; export default createComponent({ //引入Touch相关代码(会自动跟组件合并) mixins: [TouchMixin],//mixins用来抽取组件公共代码 props: { valueKey: String, readonly: Boolean, allowHtml: Boolean, className: String, itemHeight: Number,//默认44 defaultIndex: Number, swipeDuration: [Number, String], visibleItemCount: [Number, String], initialOptions: { type: Array, default: () => [], }, }, 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); }, mounted() { //调用TouchMixin中的bindTouchEvent绑定touch事件,其中$el为当前dom this.bindTouchEvent(this.$el); if (supportMousewheel) { on(this.$el, 'wheel', this.onMouseWheel, false); } }, destroyed() { const { children } = this.$parent; if (children) { children.splice(children.indexOf(this), 1); } if (supportMousewheel) { off(this.$el, 'wheel'); } }, watch: { initialOptions: 'setOptions', defaultIndex(val) { this.setIndex(val); }, }, computed: { count() { return this.options.length; }, baseOffset() { //itemHeight = 44 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; } //x y坐标 this.touchStart(event); if (this.moving) { //解析当前div translateY 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; } //计算offsetX offsetY this.touchMove(event); if (this.direction === 'vertical') { this.moving = true; preventDefault(event, true); } //计算偏移量,跟css translate3d y绑定 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; } //根据offset获取索引 const index = this.getIndexByOffset(this.offset); this.duration = DEFAULT_DURATION; //根据index设置offset this.setIndex(index, true); // compatible with desktop scenario // use setTimeout to skip the click event Emitted after touchstart setTimeout(() => { this.moving = false; }, 0); }, onMouseWheel(event) { if (this.readonly) { return; } preventDefault(event, true); // simply combine touchstart and touchmove const translateY = getElementTranslateY(this.$refs.wrapper); this.startOffset = Math.min(0, translateY - this.baseOffset); this.momentumOffset = this.startOffset; this.transitionEndTrigger = null; // directly use deltaY, see https://caniuse.com/?search=deltaY // use deltaY to detect direction for not special setting device // https://developer.mozilla.org/en-US/docs/Web/API/Element/wheel_event const { deltaY } = event; if (this.startOffset === 0 && deltaY < 0) { return; } // get offset // if necessary, can adjust distance value to make scrolling smoother const distance = -deltaY; this.offset = range( this.startOffset + distance, -(this.count * this.itemHeight), this.itemHeight ); if (mousewheelTimer) { clearTimeout(mousewheelTimer); } mousewheelTimer = setTimeout(() => { this.onTouchEnd(); this.touchStartTime = 0; }, MOMENTUM_LIMIT_TIME); }, //动画完成 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; } }, 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 trigger = () => { if (index !== this.currentIndex) { this.currentIndex = index; if (emitChange) { //通知父组件 this.$emit('change', index); } } }; // trigger the change event after transitionend when moving if (this.moving && offset !== this.offset) { this.transitionEndTrigger = trigger; } else { trigger(); } this.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); } } }, getValue() { return this.options[this.currentIndex]; }, getIndexByOffset(offset) { //获取index(取中间值) 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.003) * (distance < 0 ? -1 : 1); const index = this.getIndexByOffset(distance); this.duration = +this.swipeDuration; this.setIndex(index, true); }, stopMomentum() { this.moving = false; this.duration = 0; if (this.transitionEndTrigger) { this.transitionEndTrigger(); this.transitionEndTrigger = null; } }, genOptions() { const optionStyle = { height: `${this.itemHeight}px`, }; return this.options.map((option, index) => { const text = this.getOptionText(option); const disabled = isOptionDisabled(option); const data = { style: optionStyle, attrs: { role: 'button', tabindex: disabled ? -1 : 0, }, class: [ bem('item', { disabled, selected: index === this.currentIndex, }), ], on: { click: () => { this.onClickItem(index); }, }, }; const childData = { //dom class属性 class: 'van-ellipsis', //dom内容 domProps: { [this.allowHtml ? 'innerHTML' : 'textContent']: text, }, }; return ( <li {...data}> {this.slots('option', option) || <div {...childData} />} </li> ); }); }, }, render() { const wrapperStyle = { //y轴移动 transform: `translate3d(0, ${this.offset + this.baseOffset}px, 0)`, //动画时间 transitionDuration: `${this.duration}ms`, //过渡效果 transitionProperty: this.duration ? 'all' : 'none', }; return ( //touch事件在mounted方法中绑定在当前div中 <div class={[bem(), this.className]}> <ul ref="wrapper" style={wrapperStyle} class={bem('wrapper')} onTransitionend={this.onTransitionEnd} > {this.genOptions()} </ul> </div> ); }, });
-
事件处理主要逻辑
事件处理: 1、mounted方法中调用TouchMixin中的this.bindTouchEvent(this.$el)方法绑定touch事件 2、bindTouchEvent绑定 touchstart、touchmove方法 3、touch事件处理: onTouchStart: this.startX = event.touches[0].clientX; this.startY = event.touches[0].clientY; const translateY = getElementTranslateY(this.$refs.wrapper); this.offset = Math.min(0, translateY - this.baseOffset); onTouchMove: const touch = event.touches[0]; // safari back will set clientX to negative number this.deltaX = touch.clientX < 0 ? 0 : touch.clientX - this.startX; this.deltaY = touch.clientY - this.startY; this.offsetX = Math.abs(this.deltaX); this.offsetY = Math.abs(this.deltaY); this.offset = this.startOffset + this.deltaY // onTouchEnd: const index = this.getIndexByOffset(this.offset); 注意:touch事件的处理,利用了css的pointer-events: none属性,导致z-index虽然越高,但是不能响应事件,事件会交由低z-index的div容器处理
vant2x picker源码阅读
最新推荐文章于 2024-06-20 17:41:16 发布