vant/src/swipe/index.js
2020-08-17 10:12:56 +08:00

425 lines
9.4 KiB
JavaScript

// 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 (
<div class={bem('indicators', { vertical: this.vertical })}>
{Array(...Array(count)).map((empty, index) => (
<i
class={bem('indicator', { active: index === activeIndicator })}
style={index === activeIndicator ? this.indicatorStyle : null}
/>
))}
</div>
);
}
},
},
render() {
return (
<div class={bem()}>
<div
ref="track"
style={this.trackStyle}
class={bem('track', { vertical: this.vertical })}
>
{this.$slots.default?.()}
</div>
{this.genIndicator()}
</div>
);
},
});