// Utils import { createNamespace } from '../utils'; import { range } from '../utils/format/number'; import { on, preventDefault } from '../utils/dom/event'; // Mixins import { PopupMixin } from '../mixins/popup'; import { TouchMixin } from '../mixins/touch'; // Components import Icon from '../icon'; import Image from '../image'; import Swipe from '../swipe'; import Loading from '../loading'; import SwipeItem from '../swipe-item'; const [createComponent, bem] = createNamespace('image-preview'); const DOUBLE_CLICK_INTERVAL = 250; function getDistance(touches) { return Math.sqrt( (touches[0].clientX - touches[1].clientX) ** 2 + (touches[0].clientY - touches[1].clientY) ** 2 ); } export default createComponent({ mixins: [ PopupMixin({ skipToggleEvent: true, }), TouchMixin, ], props: { className: null, asyncClose: Boolean, showIndicators: Boolean, images: { type: Array, default: () => [], }, loop: { type: Boolean, default: true, }, swipeDuration: { type: [Number, String], default: 500, }, overlay: { type: Boolean, default: true, }, showIndex: { type: Boolean, default: true, }, startPosition: { type: [Number, String], default: 0, }, minZoom: { type: [Number, String], default: 1 / 3, }, maxZoom: { type: [Number, String], default: 3, }, overlayClass: { type: String, default: bem('overlay'), }, closeable: Boolean, closeIcon: { type: String, default: 'clear', }, closeIconPosition: { type: String, default: 'top-right', }, }, data() { return { scale: 1, moveX: 0, moveY: 0, active: 0, moving: false, zooming: false, doubleClickTimer: null, }; }, computed: { imageStyle() { const { scale } = this; const style = { transitionDuration: this.zooming || this.moving ? '0s' : '.3s', }; if (scale !== 1) { style.transform = `scale3d(${scale}, ${scale}, 1) translate(${ this.moveX / scale }px, ${this.moveY / scale}px)`; } return style; }, }, watch: { startPosition: 'setActive', value(val) { if (val) { this.setActive(+this.startPosition); this.$nextTick(() => { this.$refs.swipe.swipeTo(+this.startPosition, { immediate: true }); }); } else { this.$emit('close', { index: this.active, url: this.images[this.active], }); } }, shouldRender: { handler(val) { if (val) { this.$nextTick(() => { const swipe = this.$refs.swipe.$el; on(swipe, 'touchstart', this.onWrapperTouchStart); on(swipe, 'touchmove', preventDefault); on(swipe, 'touchend', this.onWrapperTouchEnd); on(swipe, 'touchcancel', this.onWrapperTouchEnd); }); } }, immediate: true, }, }, methods: { emitClose() { if (!this.asyncClose) { this.$emit('input', false); } }, 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 < DOUBLE_CLICK_INTERVAL && offsetX < 10 && offsetY < 10) { if (!this.doubleClickTimer) { this.doubleClickTimer = setTimeout(() => { this.emitClose(); this.doubleClickTimer = null; }, DOUBLE_CLICK_INTERVAL); } else { clearTimeout(this.doubleClickTimer); this.doubleClickTimer = null; this.toggleScale(); } } }, 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.setScale(scale); } }, 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); } } }, setActive(active) { this.resetScale(); if (active !== this.active) { this.active = active; this.$emit('change', active); } }, setScale(scale) { const value = range(scale, +this.minZoom, +this.maxZoom); this.scale = value; this.$emit('scale', { index: this.active, scale: value }); }, resetScale() { this.setScale(1); this.moveX = 0; this.moveY = 0; }, toggleScale() { const scale = this.scale > 1 ? 1 : 2; this.setScale(scale); this.moveX = 0; this.moveY = 0; }, genIndex() { if (this.showIndex) { return (
{this.slots('index') || `${this.active + 1} / ${this.images.length}`}
); } }, genCover() { const cover = this.slots('cover'); if (cover) { return
{cover}
; } }, genImages() { const imageSlots = { loading: () => , }; return ( {this.images.map((image, index) => ( ))} ); }, genClose() { if (this.closeable) { return ( ); } }, onClosed() { this.$emit('closed'); }, }, render() { if (!this.shouldRender) { return; } return (
{this.genClose()} {this.genImages()} {this.genIndex()} {this.genCover()}
); }, });