UNPKG

@zebra-ui/swiper

Version:

专为多端设计的高性能swiper轮播组件库,支持多种复杂的 3D swiper轮播效果。

827 lines (754 loc) 23.2 kB
import { elementChildren, elementOffset, elementParents, getTranslate } from '../../components/shared/utils' export default function Zoom({ swiper, extendParams, on, emit }) { extendParams({ zoom: { enabled: false, limitToOriginalSize: false, maxRatio: 3, minRatio: 1, toggle: true, containerClass: 'swiper-zoom-container', zoomedSlideClass: 'swiper-slide-zoomed' } }) swiper.zoom = { enabled: false } let currentScale = 1 let isScaling = false let fakeGestureTouched let fakeGestureMoved const evCache = [] const gesture = { originX: 0, originY: 0, slideEl: undefined, slideWidth: undefined, slideHeight: undefined, imageEl: undefined, imageWrapEl: undefined, maxRatio: 3 } const image = { isTouched: undefined, isMoved: undefined, currentX: undefined, currentY: undefined, minX: undefined, minY: undefined, maxX: undefined, maxY: undefined, width: undefined, height: undefined, startX: undefined, startY: undefined, touchesStart: {}, touchesCurrent: {} } const velocity = { x: undefined, y: undefined, prevPositionX: undefined, prevPositionY: undefined, prevTime: undefined } let scale = 1 Object.defineProperty(swiper.zoom, 'scale', { get() { return scale }, set(value) { if (scale !== value) { const { imageEl } = gesture const { slideEl } = gesture emit('zoomChange', value, imageEl, slideEl) } scale = value } }) function getDistanceBetweenTouches() { if (evCache.length < 2) return 1 const x1 = evCache[0].pageX const y1 = evCache[0].pageY const x2 = evCache[1].pageX const y2 = evCache[1].pageY const distance = Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2) return distance } function getMaxRatio() { const params = swiper.params.zoom const maxRatio = gesture.imageWrapEl.getAttribute('data-swiper-zoom') || params.maxRatio if ( params.limitToOriginalSize && gesture.imageEl && gesture.imageEl.naturalWidth ) { const imageMaxRatio = gesture.imageEl.naturalWidth / gesture.imageEl.offsetWidth return Math.min(imageMaxRatio, maxRatio) } return maxRatio } function getScaleOrigin() { if (evCache.length < 2) return { x: null, y: null } const box = gesture.imageEl.getBoundingClientRect() return [ (evCache[0].pageX + (evCache[1].pageX - evCache[0].pageX) / 2 - box.x - window.scrollX) / currentScale, (evCache[0].pageY + (evCache[1].pageY - evCache[0].pageY) / 2 - box.y - window.scrollY) / currentScale ] } function getSlideSelector() { return swiper.isElement ? `swiper-slide` : `.${swiper.params.slideClass}` } function eventWithinSlide(e) { const slideSelector = getSlideSelector() if (e.target.matches(slideSelector)) return true if ( swiper.slides.filter((slideEl) => slideEl.contains(e.target)).length > 0 ) return true return false } function eventWithinZoomContainer(e) { const selector = `.${swiper.params.zoom.containerClass}` if (e.target.matches(selector)) return true if ( [...swiper.hostEl.querySelectorAll(selector)].filter((containerEl) => containerEl.contains(e.target) ).length > 0 ) return true return false } // Events function onGestureStart(e) { if (e.pointerType === 'mouse') { evCache.splice(0, evCache.length) } if (!eventWithinSlide(e)) return const params = swiper.params.zoom fakeGestureTouched = false fakeGestureMoved = false evCache.push(e) if (evCache.length < 2) { return } fakeGestureTouched = true gesture.scaleStart = getDistanceBetweenTouches() if (!gesture.slideEl) { gesture.slideEl = e.target.closest( `.${swiper.params.slideClass}, swiper-slide` ) if (!gesture.slideEl) gesture.slideEl = swiper.slides[swiper.activeIndex] let imageEl = gesture.slideEl.querySelector(`.${params.containerClass}`) if (imageEl) { imageEl = imageEl.querySelectorAll( 'picture, img, svg, canvas, .swiper-zoom-target' )[0] } gesture.imageEl = imageEl if (imageEl) { gesture.imageWrapEl = elementParents( gesture.imageEl, `.${params.containerClass}` )[0] } else { gesture.imageWrapEl = undefined } if (!gesture.imageWrapEl) { gesture.imageEl = undefined return } gesture.maxRatio = getMaxRatio() } if (gesture.imageEl) { const [originX, originY] = getScaleOrigin() gesture.originX = originX gesture.originY = originY gesture.imageEl.style.transitionDuration = '0ms' } isScaling = true } function onGestureChange(e) { if (!eventWithinSlide(e)) return const params = swiper.params.zoom const { zoom } = swiper const pointerIndex = evCache.findIndex( (cachedEv) => cachedEv.pointerId === e.pointerId ) if (pointerIndex >= 0) evCache[pointerIndex] = e if (evCache.length < 2) { return } fakeGestureMoved = true gesture.scaleMove = getDistanceBetweenTouches() if (!gesture.imageEl) { return } zoom.scale = (gesture.scaleMove / gesture.scaleStart) * currentScale if (zoom.scale > gesture.maxRatio) { zoom.scale = gesture.maxRatio - 1 + (zoom.scale - gesture.maxRatio + 1) ** 0.5 } if (zoom.scale < params.minRatio) { zoom.scale = params.minRatio + 1 - (params.minRatio - zoom.scale + 1) ** 0.5 } gesture.imageEl.style.transform = `translate3d(0,0,0) scale(${zoom.scale})` } function onGestureEnd(e) { if (!eventWithinSlide(e)) return if (e.pointerType === 'mouse' && e.type === 'pointerout') return const params = swiper.params.zoom const { zoom } = swiper const pointerIndex = evCache.findIndex( (cachedEv) => cachedEv.pointerId === e.pointerId ) if (pointerIndex >= 0) evCache.splice(pointerIndex, 1) if (!fakeGestureTouched || !fakeGestureMoved) { return } fakeGestureTouched = false fakeGestureMoved = false if (!gesture.imageEl) return zoom.scale = Math.max( Math.min(zoom.scale, gesture.maxRatio), params.minRatio ) gesture.imageEl.style.transitionDuration = `${swiper.params.speed}ms` gesture.imageEl.style.transform = `translate3d(0,0,0) scale(${zoom.scale})` currentScale = zoom.scale isScaling = false if (zoom.scale > 1 && gesture.slideEl) { gesture.slideEl.classList.add(`${params.zoomedSlideClass}`) } else if (zoom.scale <= 1 && gesture.slideEl) { gesture.slideEl.classList.remove(`${params.zoomedSlideClass}`) } if (zoom.scale === 1) { gesture.originX = 0 gesture.originY = 0 gesture.slideEl = undefined } } let allowTouchMoveTimeout function allowTouchMove() { swiper.touchEventsData.preventTouchMoveFromPointerMove = false } function preventTouchMove() { clearTimeout(allowTouchMoveTimeout) swiper.touchEventsData.preventTouchMoveFromPointerMove = true allowTouchMoveTimeout = setTimeout(() => { if (swiper.destroyed) return allowTouchMove() }) } function onTouchStart(e) { const { device } = swiper if (!gesture.imageEl) return if (image.isTouched) return if (device.android && e.cancelable) e.preventDefault() image.isTouched = true const event = evCache.length > 0 ? evCache[0] : e image.touchesStart.x = event.pageX image.touchesStart.y = event.pageY } function onTouchMove(e) { if (!eventWithinSlide(e) || !eventWithinZoomContainer(e)) { return } const { zoom } = swiper if (!gesture.imageEl) { return } if (!image.isTouched || !gesture.slideEl) { return } if (!image.isMoved) { image.width = gesture.imageEl.offsetWidth || gesture.imageEl.clientWidth image.height = gesture.imageEl.offsetHeight || gesture.imageEl.clientHeight image.startX = getTranslate(gesture.imageWrapEl, 'x') || 0 image.startY = getTranslate(gesture.imageWrapEl, 'y') || 0 gesture.slideWidth = gesture.slideEl.offsetWidth gesture.slideHeight = gesture.slideEl.offsetHeight gesture.imageWrapEl.style.transitionDuration = '0ms' } // Define if we need image drag const scaledWidth = image.width * zoom.scale const scaledHeight = image.height * zoom.scale image.minX = Math.min(gesture.slideWidth / 2 - scaledWidth / 2, 0) image.maxX = -image.minX image.minY = Math.min(gesture.slideHeight / 2 - scaledHeight / 2, 0) image.maxY = -image.minY image.touchesCurrent.x = evCache.length > 0 ? evCache[0].pageX : e.pageX image.touchesCurrent.y = evCache.length > 0 ? evCache[0].pageY : e.pageY const touchesDiff = Math.max( Math.abs(image.touchesCurrent.x - image.touchesStart.x), Math.abs(image.touchesCurrent.y - image.touchesStart.y) ) if (touchesDiff > 5) { swiper.allowClick = false } if (!image.isMoved && !isScaling) { if ( swiper.isHorizontal() && ((Math.floor(image.minX) === Math.floor(image.startX) && image.touchesCurrent.x < image.touchesStart.x) || (Math.floor(image.maxX) === Math.floor(image.startX) && image.touchesCurrent.x > image.touchesStart.x)) ) { image.isTouched = false allowTouchMove() return } if ( !swiper.isHorizontal() && ((Math.floor(image.minY) === Math.floor(image.startY) && image.touchesCurrent.y < image.touchesStart.y) || (Math.floor(image.maxY) === Math.floor(image.startY) && image.touchesCurrent.y > image.touchesStart.y)) ) { image.isTouched = false allowTouchMove() return } } if (e.cancelable) { e.preventDefault() } e.stopPropagation() preventTouchMove() image.isMoved = true const scaleRatio = (zoom.scale - currentScale) / (gesture.maxRatio - swiper.params.zoom.minRatio) const { originX, originY } = gesture image.currentX = image.touchesCurrent.x - image.touchesStart.x + image.startX + scaleRatio * (image.width - originX * 2) image.currentY = image.touchesCurrent.y - image.touchesStart.y + image.startY + scaleRatio * (image.height - originY * 2) if (image.currentX < image.minX) { image.currentX = image.minX + 1 - (image.minX - image.currentX + 1) ** 0.8 } if (image.currentX > image.maxX) { image.currentX = image.maxX - 1 + (image.currentX - image.maxX + 1) ** 0.8 } if (image.currentY < image.minY) { image.currentY = image.minY + 1 - (image.minY - image.currentY + 1) ** 0.8 } if (image.currentY > image.maxY) { image.currentY = image.maxY - 1 + (image.currentY - image.maxY + 1) ** 0.8 } // Velocity if (!velocity.prevPositionX) velocity.prevPositionX = image.touchesCurrent.x if (!velocity.prevPositionY) velocity.prevPositionY = image.touchesCurrent.y if (!velocity.prevTime) velocity.prevTime = Date.now() velocity.x = (image.touchesCurrent.x - velocity.prevPositionX) / (Date.now() - velocity.prevTime) / 2 velocity.y = (image.touchesCurrent.y - velocity.prevPositionY) / (Date.now() - velocity.prevTime) / 2 if (Math.abs(image.touchesCurrent.x - velocity.prevPositionX) < 2) velocity.x = 0 if (Math.abs(image.touchesCurrent.y - velocity.prevPositionY) < 2) velocity.y = 0 velocity.prevPositionX = image.touchesCurrent.x velocity.prevPositionY = image.touchesCurrent.y velocity.prevTime = Date.now() gesture.imageWrapEl.style.transform = `translate3d(${image.currentX}px, ${image.currentY}px,0)` } function onTouchEnd() { const { zoom } = swiper if (!gesture.imageEl) return if (!image.isTouched || !image.isMoved) { image.isTouched = false image.isMoved = false return } image.isTouched = false image.isMoved = false let momentumDurationX = 300 let momentumDurationY = 300 const momentumDistanceX = velocity.x * momentumDurationX const newPositionX = image.currentX + momentumDistanceX const momentumDistanceY = velocity.y * momentumDurationY const newPositionY = image.currentY + momentumDistanceY // Fix duration if (velocity.x !== 0) momentumDurationX = Math.abs((newPositionX - image.currentX) / velocity.x) if (velocity.y !== 0) momentumDurationY = Math.abs((newPositionY - image.currentY) / velocity.y) const momentumDuration = Math.max(momentumDurationX, momentumDurationY) image.currentX = newPositionX image.currentY = newPositionY // Define if we need image drag const scaledWidth = image.width * zoom.scale const scaledHeight = image.height * zoom.scale image.minX = Math.min(gesture.slideWidth / 2 - scaledWidth / 2, 0) image.maxX = -image.minX image.minY = Math.min(gesture.slideHeight / 2 - scaledHeight / 2, 0) image.maxY = -image.minY image.currentX = Math.max(Math.min(image.currentX, image.maxX), image.minX) image.currentY = Math.max(Math.min(image.currentY, image.maxY), image.minY) gesture.imageWrapEl.style.transitionDuration = `${momentumDuration}ms` gesture.imageWrapEl.style.transform = `translate3d(${image.currentX}px, ${image.currentY}px,0)` } function onTransitionEnd() { const { zoom } = swiper if ( gesture.slideEl && swiper.activeIndex !== swiper.slides.indexOf(gesture.slideEl) ) { if (gesture.imageEl) { gesture.imageEl.style.transform = 'translate3d(0,0,0) scale(1)' } if (gesture.imageWrapEl) { gesture.imageWrapEl.style.transform = 'translate3d(0,0,0)' } gesture.slideEl.classList.remove(`${swiper.params.zoom.zoomedSlideClass}`) zoom.scale = 1 currentScale = 1 gesture.slideEl = undefined gesture.imageEl = undefined gesture.imageWrapEl = undefined gesture.originX = 0 gesture.originY = 0 } } function zoomIn(e) { const { zoom } = swiper const params = swiper.params.zoom if (!gesture.slideEl) { if (e && e.target) { gesture.slideEl = e.target.closest( `.${swiper.params.slideClass}, swiper-slide` ) } if (!gesture.slideEl) { if ( swiper.params.virtual && swiper.params.virtual.enabled && swiper.virtual ) { gesture.slideEl = elementChildren( swiper.slidesEl, `.${swiper.params.slideActiveClass}` )[0] } else { gesture.slideEl = swiper.slides[swiper.activeIndex] } } let imageEl = gesture.slideEl.querySelector(`.${params.containerClass}`) if (imageEl) { imageEl = imageEl.querySelectorAll( 'picture, img, svg, canvas, .swiper-zoom-target' )[0] } gesture.imageEl = imageEl if (imageEl) { gesture.imageWrapEl = elementParents( gesture.imageEl, `.${params.containerClass}` )[0] } else { gesture.imageWrapEl = undefined } } if (!gesture.imageEl || !gesture.imageWrapEl) return if (swiper.params.cssMode) { swiper.wrapperEl.style.overflow = 'hidden' swiper.wrapperEl.style.touchAction = 'none' } gesture.slideEl.classList.add(`${params.zoomedSlideClass}`) let touchX let touchY let offsetX let offsetY let diffX let diffY let translateX let translateY let imageWidth let imageHeight let scaledWidth let scaledHeight let translateMinX let translateMinY let translateMaxX let translateMaxY let slideWidth let slideHeight if (typeof image.touchesStart.x === 'undefined' && e) { touchX = e.pageX touchY = e.pageY } else { touchX = image.touchesStart.x touchY = image.touchesStart.y } const forceZoomRatio = typeof e === 'number' ? e : null if (currentScale === 1 && forceZoomRatio) { touchX = undefined touchY = undefined image.touchesStart.x = undefined image.touchesStart.y = undefined } const maxRatio = getMaxRatio() zoom.scale = forceZoomRatio || maxRatio currentScale = forceZoomRatio || maxRatio if (e && !(currentScale === 1 && forceZoomRatio)) { slideWidth = gesture.slideEl.offsetWidth slideHeight = gesture.slideEl.offsetHeight offsetX = elementOffset(gesture.slideEl).left + window.scrollX offsetY = elementOffset(gesture.slideEl).top + window.scrollY diffX = offsetX + slideWidth / 2 - touchX diffY = offsetY + slideHeight / 2 - touchY imageWidth = gesture.imageEl.offsetWidth || gesture.imageEl.clientWidth imageHeight = gesture.imageEl.offsetHeight || gesture.imageEl.clientHeight scaledWidth = imageWidth * zoom.scale scaledHeight = imageHeight * zoom.scale translateMinX = Math.min(slideWidth / 2 - scaledWidth / 2, 0) translateMinY = Math.min(slideHeight / 2 - scaledHeight / 2, 0) translateMaxX = -translateMinX translateMaxY = -translateMinY translateX = diffX * zoom.scale translateY = diffY * zoom.scale if (translateX < translateMinX) { translateX = translateMinX } if (translateX > translateMaxX) { translateX = translateMaxX } if (translateY < translateMinY) { translateY = translateMinY } if (translateY > translateMaxY) { translateY = translateMaxY } } else { translateX = 0 translateY = 0 } if (forceZoomRatio && zoom.scale === 1) { gesture.originX = 0 gesture.originY = 0 } gesture.imageWrapEl.style.transitionDuration = '300ms' gesture.imageWrapEl.style.transform = `translate3d(${translateX}px, ${translateY}px,0)` gesture.imageEl.style.transitionDuration = '300ms' gesture.imageEl.style.transform = `translate3d(0,0,0) scale(${zoom.scale})` } function zoomOut() { const { zoom } = swiper const params = swiper.params.zoom if (!gesture.slideEl) { if ( swiper.params.virtual && swiper.params.virtual.enabled && swiper.virtual ) { gesture.slideEl = elementChildren( swiper.slidesEl, `.${swiper.params.slideActiveClass}` )[0] } else { gesture.slideEl = swiper.slides[swiper.activeIndex] } let imageEl = gesture.slideEl.querySelector(`.${params.containerClass}`) if (imageEl) { imageEl = imageEl.querySelectorAll( 'picture, img, svg, canvas, .swiper-zoom-target' )[0] } gesture.imageEl = imageEl if (imageEl) { gesture.imageWrapEl = elementParents( gesture.imageEl, `.${params.containerClass}` )[0] } else { gesture.imageWrapEl = undefined } } if (!gesture.imageEl || !gesture.imageWrapEl) return if (swiper.params.cssMode) { swiper.wrapperEl.style.overflow = '' swiper.wrapperEl.style.touchAction = '' } zoom.scale = 1 currentScale = 1 image.touchesStart.x = undefined image.touchesStart.y = undefined gesture.imageWrapEl.style.transitionDuration = '300ms' gesture.imageWrapEl.style.transform = 'translate3d(0,0,0)' gesture.imageEl.style.transitionDuration = '300ms' gesture.imageEl.style.transform = 'translate3d(0,0,0) scale(1)' gesture.slideEl.classList.remove(`${params.zoomedSlideClass}`) gesture.slideEl = undefined gesture.originX = 0 gesture.originY = 0 } // Toggle Zoom function zoomToggle(e) { const { zoom } = swiper if (zoom.scale && zoom.scale !== 1) { // Zoom Out zoomOut() } else { // Zoom In zoomIn(e) } } function getListeners() { const passiveListener = swiper.params.passiveListeners ? { passive: true, capture: false } : false const activeListenerWithCapture = swiper.params.passiveListeners ? { passive: false, capture: true } : true return { passiveListener, activeListenerWithCapture } } // Attach/Detach Events function enable() { const { zoom } = swiper if (zoom.enabled) return zoom.enabled = true const { passiveListener, activeListenerWithCapture } = getListeners() // Scale image swiper.wrapperEl.addEventListener( 'pointerdown', onGestureStart, passiveListener ) swiper.wrapperEl.addEventListener( 'pointermove', onGestureChange, activeListenerWithCapture ) ;['pointerup', 'pointercancel', 'pointerout'].forEach((eventName) => { swiper.wrapperEl.addEventListener( eventName, onGestureEnd, passiveListener ) }) // Move image swiper.wrapperEl.addEventListener( 'pointermove', onTouchMove, activeListenerWithCapture ) } function disable() { const { zoom } = swiper if (!zoom.enabled) return zoom.enabled = false const { passiveListener, activeListenerWithCapture } = getListeners() // Scale image swiper.wrapperEl.removeEventListener( 'pointerdown', onGestureStart, passiveListener ) swiper.wrapperEl.removeEventListener( 'pointermove', onGestureChange, activeListenerWithCapture ) ;['pointerup', 'pointercancel', 'pointerout'].forEach((eventName) => { swiper.wrapperEl.removeEventListener( eventName, onGestureEnd, passiveListener ) }) // Move image swiper.wrapperEl.removeEventListener( 'pointermove', onTouchMove, activeListenerWithCapture ) } on('init', () => { if (swiper.params.zoom.enabled) { enable() } }) on('destroy', () => { disable() }) on('touchStart', (_s, e) => { if (!swiper.zoom.enabled) return onTouchStart(e) }) on('touchEnd', (_s, e) => { if (!swiper.zoom.enabled) return onTouchEnd(e) }) on('doubleTap', (_s, e) => { if ( !swiper.animating && swiper.params.zoom.enabled && swiper.zoom.enabled && swiper.params.zoom.toggle ) { zoomToggle(e) } }) on('transitionEnd', () => { if (swiper.zoom.enabled && swiper.params.zoom.enabled) { onTransitionEnd() } }) on('slideChange', () => { if ( swiper.zoom.enabled && swiper.params.zoom.enabled && swiper.params.cssMode ) { onTransitionEnd() } }) Object.assign(swiper.zoom, { enable, disable, in: zoomIn, out: zoomOut, toggle: zoomToggle }) }