vant/packages/image-preview/ImagePreview.js

306 lines
7.0 KiB
JavaScript

import { createNamespace, isServer } from '../utils';
import { range } from '../utils/format/number';
import { preventDefault } from '../utils/dom/event';
import { PopupMixin } from '../mixins/popup';
import { TouchMixin } from '../mixins/touch';
import Swipe from '../swipe';
import SwipeItem from '../swipe-item';
const [createComponent, bem] = createNamespace('image-preview');
function getDistance(touches) {
return Math.sqrt(
Math.abs(
(touches[0].clientX - touches[1].clientX) *
(touches[0].clientY - touches[1].clientY)
)
);
}
export default createComponent({
mixins: [PopupMixin, TouchMixin],
props: {
images: Array,
className: null,
lazyLoad: Boolean,
asyncClose: Boolean,
startPosition: Number,
showIndicators: Boolean,
closeOnPopstate: Boolean,
loop: {
type: Boolean,
default: true
},
overlay: {
type: Boolean,
default: true
},
showIndex: {
type: Boolean,
default: true
},
minZoom: {
type: Number,
default: 1 / 3
},
maxZoom: {
type: Number,
default: 3
},
overlayClass: {
type: String,
default: 'van-image-preview__overlay'
},
closeOnClickOverlay: {
type: Boolean,
default: true
}
},
data() {
this.bindStatus = false;
return {
scale: 1,
moveX: 0,
moveY: 0,
moving: false,
zooming: false,
active: 0
};
},
computed: {
imageStyle() {
const { scale } = this;
const style = {
transition: this.zooming || this.moving ? '' : '.3s all'
};
if (scale !== 1) {
style.transform = `scale3d(${scale}, ${scale}, 1) translate(${this.moveX /
scale}px, ${this.moveY / scale}px)`;
}
return style;
}
},
watch: {
value() {
this.active = this.startPosition;
},
startPosition(active) {
this.active = active;
},
closeOnPopstate: {
handler(val) {
this.handlePopstate(val);
},
immediate: true
}
},
methods: {
handlePopstate(bind) {
/* istanbul ignore if */
if (isServer) {
return;
}
if (this.bindStatus !== bind) {
this.bindStatus = bind;
const action = bind ? 'add' : 'remove';
window[`${action}EventListener`]('popstate', this.close);
}
},
onWrapperTouchStart() {
this.touchStartTime = new Date();
},
onWrapperTouchEnd(event) {
preventDefault(event);
const deltaTime = new Date() - this.touchStartTime;
const { offsetX = 0, offsetY = 0 } = this.$refs.swipe || {};
// prevent long tap to close component
if (deltaTime < 300 && offsetX < 10 && offsetY < 10) {
const index = this.active;
this.resetScale();
this.$emit('close', {
index,
url: this.images[index]
});
if (!this.asyncClose) {
this.$emit('input', false);
}
}
},
startMove(event) {
const image = event.currentTarget;
const rect = image.getBoundingClientRect();
const winWidth = window.innerWidth;
const winHeight = window.innerHeight;
this.touchStart(event);
this.moving = true;
this.startMoveX = this.moveX;
this.startMoveY = this.moveY;
this.maxMoveX = Math.max(0, (rect.width - winWidth) / 2);
this.maxMoveY = Math.max(0, (rect.height - winHeight) / 2);
},
startZoom(event) {
this.moving = false;
this.zooming = true;
this.startScale = this.scale;
this.startDistance = getDistance(event.touches);
},
onImageTouchStart(event) {
const { touches } = event;
const { offsetX = 0 } = this.$refs.swipe || {};
if (touches.length === 1 && this.scale !== 1) {
this.startMove(event);
} /* istanbul ignore else */ else if (touches.length === 2 && !offsetX) {
this.startZoom(event);
}
},
onImageTouchMove(event) {
const { touches } = event;
if (this.moving || this.zooming) {
preventDefault(event, true);
}
if (this.moving) {
this.touchMove(event);
const moveX = this.deltaX + this.startMoveX;
const moveY = this.deltaY + this.startMoveY;
this.moveX = range(moveX, -this.maxMoveX, this.maxMoveX);
this.moveY = range(moveY, -this.maxMoveY, this.maxMoveY);
}
if (this.zooming && touches.length === 2) {
const distance = getDistance(touches);
const scale = (this.startScale * distance) / this.startDistance;
this.scale = range(scale, this.minZoom, this.maxZoom);
}
},
onImageTouchEnd(event) {
/* istanbul ignore else */
if (this.moving || this.zooming) {
let stopPropagation = true;
if (
this.moving &&
this.startMoveX === this.moveX &&
this.startMoveY === this.moveY
) {
stopPropagation = false;
}
if (!event.touches.length) {
this.moving = false;
this.zooming = false;
this.startMoveX = 0;
this.startMoveY = 0;
this.startScale = 1;
if (this.scale < 1) {
this.resetScale();
}
}
if (stopPropagation) {
preventDefault(event, true);
}
}
},
onSwipeChange(active) {
this.resetScale();
this.active = active;
this.$emit('change', active);
},
resetScale() {
this.scale = 1;
this.moveX = 0;
this.moveY = 0;
}
},
render(h) {
if (!this.value) {
return;
}
const { active, images } = this;
const Index = this.showIndex && (
<div class={bem('index')}>
{this.slots('index') || `${active + 1}/${images.length}`}
</div>
);
const Images = (
<Swipe
ref="swipe"
loop={this.loop}
indicatorColor="white"
initialSwipe={this.startPosition}
showIndicators={this.showIndicators}
onChange={this.onSwipeChange}
>
{images.map((image, index) => {
const props = {
class: bem('image'),
style: index === active ? this.imageStyle : null,
on: {
touchstart: this.onImageTouchStart,
touchmove: this.onImageTouchMove,
touchend: this.onImageTouchEnd,
touchcancel: this.onImageTouchEnd
}
};
return (
<SwipeItem>
{this.lazyLoad ? (
<img vLazy={image} {...props} />
) : (
<img src={image} {...props} />
)}
</SwipeItem>
);
})}
</Swipe>
);
return (
<transition name="van-fade">
<div
class={[bem(), this.className]}
onTouchstart={this.onWrapperTouchStart}
onTouchMove={preventDefault}
onTouchend={this.onWrapperTouchEnd}
onTouchcancel={this.onWrapperTouchEnd}
>
{Index}
{Images}
</div>
</transition>
);
}
});