diff --git a/src/image-preview/ImagePreviewItem.js b/src/image-preview/ImagePreviewItem.js index 30f9b3cf8..9937eafa9 100644 --- a/src/image-preview/ImagePreviewItem.js +++ b/src/image-preview/ImagePreviewItem.js @@ -11,8 +11,6 @@ import Image from '../image'; import Loading from '../loading'; import SwipeItem from '../swipe-item'; -const DOUBLE_CLICK_INTERVAL = 250; - function getDistance(touches) { return Math.sqrt( (touches[0].clientX - touches[1].clientX) ** 2 + @@ -40,6 +38,8 @@ export default { moveY: 0, moving: false, zooming: false, + displayWidth: 0, + displayHeight: 0, }; }, @@ -58,34 +58,33 @@ export default { return style; }, + + maxMoveX() { + if (this.displayWidth) { + return Math.max( + 0, + (this.scale * this.displayWidth - this.windowWidth) / 2 + ); + } + return 0; + }, + + maxMoveY() { + if (this.displayHeight) { + return Math.max( + 0, + (this.scale * this.displayHeight - this.windowHeight) / 2 + ); + } + return 0; + }, + }, + + mounted() { + this.bindTouchEvent(this.$el); }, methods: { - startMove() { - this.setMaxMove(); - this.moving = true; - this.startMoveX = this.moveX; - this.startMoveY = this.moveY; - }, - - setMaxMove() { - const { - scale, - windowWidth, - windowHeight, - displayWidth, - displayHeight, - } = this; - - if (this.displayWidth && this.displayHeight) { - this.maxMoveX = Math.max(0, (displayWidth * scale - windowWidth) / 2); - this.maxMoveY = Math.max(0, (displayHeight * scale - windowHeight) / 2); - } else { - this.maxMoveX = 0; - this.maxMoveY = 0; - } - }, - resetScale() { this.setScale(1); this.moveX = 0; @@ -115,10 +114,15 @@ export default { this.touchStart(event); this.touchStartTime = new Date(); - if (touches.length === 1 && this.scale !== 1) { - this.startMove(); - } else if (touches.length === 2 && !offsetX) { - this.startZoom(event); + this.startMoveX = this.moveX; + this.startMoveY = this.moveY; + + this.moving = touches.length === 1 && this.scale !== 1; + this.zooming = touches.length === 2 && !offsetX; + + if (this.zooming) { + this.startScale = this.scale; + this.startDistance = getDistance(event.touches); } }, @@ -147,9 +151,12 @@ export default { }, onTouchEnd(event) { + let stopPropagation = false; + /* istanbul ignore else */ if (this.moving || this.zooming) { - let stopPropagation = true; + + stopPropagation = true; if ( this.moving && @@ -160,8 +167,13 @@ export default { } if (!event.touches.length) { + if (this.zooming) { + this.moveX = range(this.moveX, -this.maxMoveX, this.maxMoveX); + this.moveY = range(this.moveY, -this.maxMoveY, this.maxMoveY); + this.zooming = false; + } + this.moving = false; - this.zooming = false; this.startMoveX = 0; this.startMoveY = 0; this.startScale = 1; @@ -170,38 +182,36 @@ export default { this.resetScale(); } } - - if (stopPropagation) { - preventDefault(event, true); - } } - const deltaTime = new Date() - this.touchStartTime; - const { offsetX = 0, offsetY = 0 } = this; - - // prevent long tap to close component - if (deltaTime < DOUBLE_CLICK_INTERVAL && offsetX < 10 && offsetY < 10) { - if (!this.doubleClickTimer) { - this.doubleClickTimer = setTimeout(() => { - this.$emit('close'); - - this.doubleClickTimer = null; - }, DOUBLE_CLICK_INTERVAL); - } else { - clearTimeout(this.doubleClickTimer); - this.doubleClickTimer = null; - this.toggleScale(); - } - } + preventDefault(event, stopPropagation); + this.checkTap(); this.resetTouchStatus(); }, - startZoom(event) { - this.moving = false; - this.zooming = true; - this.startScale = this.scale; - this.startDistance = getDistance(event.touches); + checkTap() { + const { offsetX = 0, offsetY = 0 } = this; + const deltaTime = new Date() - this.touchStartTime; + const TAP_TIME = 250; + const TAP_OFFSET = 10; + + if ( + offsetX < TAP_OFFSET && + offsetY < TAP_OFFSET && + deltaTime < TAP_TIME + ) { + if (this.doubleTapTimer) { + clearTimeout(this.doubleTapTimer); + this.doubleTapTimer = null; + this.toggleScale(); + } else { + this.doubleTapTimer = setTimeout(() => { + this.$emit('close'); + this.doubleTapTimer = null; + }, TAP_TIME); + } + } }, onLoad(event) { @@ -226,12 +236,7 @@ export default { }; return ( - + `; -exports[`zoom 1`] = ` +exports[`zoom in and drag image to move 1`] = `
`; + +exports[`zoom in and drag image to move 2`] = `
`; diff --git a/src/image-preview/test/index.spec.js b/src/image-preview/test/index.spec.js index bd8e2fdc0..b3485c74e 100644 --- a/src/image-preview/test/index.spec.js +++ b/src/image-preview/test/index.spec.js @@ -1,14 +1,27 @@ import Vue from 'vue'; import ImagePreview from '..'; import ImagePreviewVue from '../ImagePreview'; -import { mount, trigger, triggerDrag, later } from '../../../test'; +import { mount, trigger, triggerDrag, later, mockGetBoundingClientRect } from '../../../test'; -function triggerZoom(el, x, y) { - trigger(el, 'touchstart', 0, 0, { x, y }); - trigger(el, 'touchmove', -x / 4, -y / 4, { x, y }); - trigger(el, 'touchmove', -x / 3, -y / 3, { x, y }); - trigger(el, 'touchmove', -x / 2, -y / 2, { x, y }); +function triggerTwoFingerTouchmove(el, x, y) { trigger(el, 'touchmove', -x, -y, { x, y }); +} + +function triggerZoom(el, x, y, direction = 'in') { + trigger(el, 'touchstart', 0, 0, { x, y }); + + if (direction === 'in') { + triggerTwoFingerTouchmove(el, x / 4, y / 4); + triggerTwoFingerTouchmove(el, x / 3, y / 3); + triggerTwoFingerTouchmove(el, x / 2, y / 2); + triggerTwoFingerTouchmove(el, x, y); + } else if (direction === 'out') { + triggerTwoFingerTouchmove(el, x, y); + triggerTwoFingerTouchmove(el, x / 2, y / 2); + triggerTwoFingerTouchmove(el, x / 3, y / 3); + triggerTwoFingerTouchmove(el, x / 4, y / 4); + } + trigger(el, 'touchend', 0, 0, { touchList: [] }); } @@ -172,9 +185,7 @@ test('onChange option', async (done) => { }); test('onScale option', async (done) => { - const { getBoundingClientRect } = Element.prototype; - Element.prototype.getBoundingClientRect = jest.fn(() => ({ width: 100 })); - + const restore = mockGetBoundingClientRect({ width: 100 }); const instance = ImagePreview({ images, startPosition: 0, @@ -188,7 +199,7 @@ test('onScale option', async (done) => { await later(); const image = instance.$el.querySelector('img'); triggerZoom(image, 300, 300); - Element.prototype.getBoundingClientRect = getBoundingClientRect; + restore(); }); test('register component', () => { @@ -196,21 +207,59 @@ test('register component', () => { expect(Vue.component(ImagePreviewVue.name)).toBeTruthy(); }); -test('zoom', async () => { - const { getBoundingClientRect } = Element.prototype; - Element.prototype.getBoundingClientRect = jest.fn(() => ({ width: 100 })); +test('zoom in and drag image to move', async () => { + const restore = mockGetBoundingClientRect({ width: 100 }); + const originWindowWidth = window.innerWidth; + const originWindowHeight = window.innerHeight; + + window.innerWidth = 100; + window.innerHeight = 100; const wrapper = mount(ImagePreviewVue, { propsData: { images, value: true }, }); await later(); - const image = wrapper.find('.van-image'); + const image = wrapper.find('img'); triggerZoom(image, 300, 300); - triggerDrag(image, 300, 300); - expect(image).toMatchSnapshot(); - Element.prototype.getBoundingClientRect = getBoundingClientRect; + // mock image size + ['naturalWidth', 'naturalHeight'].forEach((key) => { + Object.defineProperty(image.element, key, { value: 300 }); + }); + + // drag image before load + triggerDrag(image, 300, 300); + expect(wrapper.find('.van-image')).toMatchSnapshot(); + + // drag image after load + image.trigger('load'); + triggerDrag(image, 300, 300); + expect(wrapper.find('.van-image')).toMatchSnapshot(); + + window.innerWidth = originWindowWidth; + window.innerHeight = originWindowHeight; + restore(); +}); + +test('zoom out', async () => { + const restore = mockGetBoundingClientRect({ width: 100 }); + + const onScale = jest.fn(); + const wrapper = mount(ImagePreviewVue, { + propsData: { images, value: true }, + listeners: { + scale: onScale, + }, + }); + + await later(); + const image = wrapper.find('.van-image'); + triggerZoom(image, 300, 300, 'out'); + + expect(onScale).toHaveBeenLastCalledWith({ index: 0, scale: 1 }); + + restore(); }); test('set show-index prop to false', () => {