@zebra-ui/swiper
Version:
专为多端设计的高性能swiper轮播组件库,支持多种复杂的 3D swiper轮播效果。
827 lines (754 loc) • 23.2 kB
JavaScript
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
})
}