// Utils import { createNamespace } from '../utils'; import { isHidden } from '../utils/dom/style'; import { preventDefault } from '../utils/dom/event'; import { doubleRaf } from '../utils/dom/raf'; import { range } from '../utils/format/number'; // Mixins import { TouchMixin } from '../mixins/touch'; import { ParentMixin } from '../mixins/relation'; import { BindEventMixin } from '../mixins/bind-event'; const [createComponent, bem] = createNamespace('swipe'); export default createComponent({ mixins: [ TouchMixin, ParentMixin('vanSwipe'), BindEventMixin(function (bind, isBind) { bind(window, 'resize', this.resize, true); bind(window, 'orientationchange', this.resize, true); bind(window, 'visibilitychange', this.onVisibilityChange); if (isBind) { this.initialize(); } else { this.clear(); } }), ], props: { width: [Number, String], height: [Number, String], autoplay: [Number, String], vertical: Boolean, lazyRender: Boolean, indicatorColor: String, loop: { type: Boolean, default: true, }, duration: { type: [Number, String], default: 500, }, touchable: { type: Boolean, default: true, }, initialSwipe: { type: [Number, String], default: 0, }, showIndicators: { type: Boolean, default: true, }, stopPropagation: { type: Boolean, default: true, }, }, emits: ['change'], data() { return { rect: null, offset: 0, active: 0, deltaX: 0, deltaY: 0, swiping: false, computedWidth: 0, computedHeight: 0, }; }, watch: { children() { this.initialize(); }, initialSwipe() { this.initialize(); }, autoplay(autoplay) { if (autoplay > 0) { this.autoPlay(); } else { this.clear(); } }, }, computed: { count() { return this.children.length; }, maxCount() { return Math.ceil(Math.abs(this.minOffset) / this.size); }, delta() { return this.vertical ? this.deltaY : this.deltaX; }, size() { return this[this.vertical ? 'computedHeight' : 'computedWidth']; }, trackSize() { return this.count * this.size; }, activeIndicator() { return (this.active + this.count) % this.count; }, isCorrectDirection() { const expect = this.vertical ? 'vertical' : 'horizontal'; return this.direction === expect; }, trackStyle() { const mainAxis = this.vertical ? 'height' : 'width'; const crossAxis = this.vertical ? 'width' : 'height'; return { [mainAxis]: `${this.trackSize}px`, [crossAxis]: this[crossAxis] ? `${this[crossAxis]}px` : '', transitionDuration: `${this.swiping ? 0 : this.duration}ms`, transform: `translate${this.vertical ? 'Y' : 'X'}(${this.offset}px)`, }; }, indicatorStyle() { return { backgroundColor: this.indicatorColor, }; }, minOffset() { return ( (this.vertical ? this.rect.height : this.rect.width) - this.size * this.count ); }, }, mounted() { this.bindTouchEvent(this.$refs.track); }, methods: { // initialize swipe position initialize(active = +this.initialSwipe) { if (!this.$el || isHidden(this.$el)) { return; } clearTimeout(this.timer); const rect = this.$el.getBoundingClientRect(); this.rect = rect; this.swiping = true; this.active = active; this.computedWidth = Math.round(+this.width || rect.width); this.computedHeight = Math.round(+this.height || rect.height); this.offset = this.getTargetOffset(active); this.children.forEach((swipe) => { swipe.offset = 0; }); this.autoPlay(); }, // @exposed-api resize() { this.initialize(this.activeIndicator); }, onVisibilityChange() { if (document.hidden) { this.clear(); } else { this.autoPlay(); } }, onTouchStart(event) { if (!this.touchable) return; this.clear(); this.touchStartTime = Date.now(); this.touchStart(event); this.correctPosition(); }, onTouchMove(event) { if (!this.touchable || !this.swiping) return; this.touchMove(event); if (this.isCorrectDirection) { preventDefault(event, this.stopPropagation); this.move({ offset: this.delta }); } }, onTouchEnd() { if (!this.touchable || !this.swiping) return; const { size, delta } = this; const duration = Date.now() - this.touchStartTime; const speed = delta / duration; const shouldSwipe = Math.abs(speed) > 0.25 || Math.abs(delta) > size / 2; if (shouldSwipe && this.isCorrectDirection) { const offset = this.vertical ? this.offsetY : this.offsetX; let pace = 0; if (this.loop) { pace = offset > 0 ? (delta > 0 ? -1 : 1) : 0; } else { pace = -Math[delta > 0 ? 'ceil' : 'floor'](delta / size); } this.move({ pace, emitChange: true, }); } else if (delta) { this.move({ pace: 0 }); } this.swiping = false; this.autoPlay(); }, getTargetActive(pace) { const { active, count, maxCount } = this; if (pace) { if (this.loop) { return range(active + pace, -1, count); } return range(active + pace, 0, maxCount); } return active; }, getTargetOffset(targetActive, offset = 0) { let currentPosition = targetActive * this.size; if (!this.loop) { currentPosition = Math.min(currentPosition, -this.minOffset); } let targetOffset = Math.round(offset - currentPosition); if (!this.loop) { targetOffset = range(targetOffset, this.minOffset, 0); } return targetOffset; }, move({ pace = 0, offset = 0, emitChange }) { const { loop, count, active, children, trackSize, minOffset } = this; if (count <= 1) { return; } const targetActive = this.getTargetActive(pace); const targetOffset = this.getTargetOffset(targetActive, offset); // auto move first and last swipe in loop mode if (loop) { if (children[0] && targetOffset !== minOffset) { const outRightBound = targetOffset < minOffset; children[0].offset = outRightBound ? trackSize : 0; } if (children[count - 1] && targetOffset !== 0) { const outLeftBound = targetOffset > 0; children[count - 1].offset = outLeftBound ? -trackSize : 0; } } this.active = targetActive; this.offset = targetOffset; if (emitChange && targetActive !== active) { this.$emit('change', this.activeIndicator); } }, // @exposed-api prev() { this.correctPosition(); this.resetTouchStatus(); doubleRaf(() => { this.swiping = false; this.move({ pace: -1, emitChange: true, }); }); }, // @exposed-api next() { this.correctPosition(); this.resetTouchStatus(); doubleRaf(() => { this.swiping = false; this.move({ pace: 1, emitChange: true, }); }); }, // @exposed-api swipeTo(index, options = {}) { this.correctPosition(); this.resetTouchStatus(); doubleRaf(() => { let targetIndex; if (this.loop && index === this.count) { targetIndex = this.active === 0 ? 0 : index; } else { targetIndex = index % this.count; } if (options.immediate) { doubleRaf(() => { this.swiping = false; }); } else { this.swiping = false; } this.move({ pace: targetIndex - this.active, emitChange: true, }); }); }, correctPosition() { this.swiping = true; if (this.active <= -1) { this.move({ pace: this.count }); } if (this.active >= this.count) { this.move({ pace: -this.count }); } }, clear() { clearTimeout(this.timer); }, autoPlay() { const { autoplay } = this; if (autoplay > 0 && this.count > 1) { this.clear(); this.timer = setTimeout(() => { this.next(); this.autoPlay(); }, autoplay); } }, genIndicator() { const { count, activeIndicator } = this; if (this.$slots.indicator) { return this.$slots.indicator(); } if (this.showIndicators && count > 1) { return (