Skip to content

Instantly share code, notes, and snippets.

@RazinTeqB
Last active July 7, 2023 10:16
Show Gist options
  • Save RazinTeqB/0e70862c6ef7a260c875a4efceeeef00 to your computer and use it in GitHub Desktop.
Save RazinTeqB/0e70862c6ef7a260c875a4efceeeef00 to your computer and use it in GitHub Desktop.
Image Zoom on mouse over vue component
<template>
<div
v-show="!zoom || $device.isMobile"
class="pointer-events-none sm:pointer-events-auto absolute inset-x-0 top-4 z-10 mx-auto flex max-w-max cursor-pointer gap-3 rounded-full bg-dark/75 px-4 py-2 text-white"
role="button"
@click="zoom = !zoom"
>
<nuxt-img src="/images/icons/icon-search-white.svg" width="20" />
<span>Click to zoom</span>
</div>
</template>
<script>
/**
* if selector have overlay elements like size or discount badge on product page image with position absolute
* then set pointer-events to none to avoid zoom out when mouse over overlay element
*/
export default {
name: 'VZoom',
props: {
selector: {
type: String,
required: true,
},
transitionDuration: {
type: Number,
default: 300,
},
},
setup(props) {
const { $bus, $device } = useNuxtApp()
const zoom = ref($device.isMobile)
watch(zoom, (value) => {
setZoomEvents(!value)
const elements = document.querySelectorAll(props.selector)
elements.forEach((elm) => {
if (value) {
elm.style.cursor = 'zoom-in'
} else {
elm.style.removeProperty('cursor')
}
})
})
onMounted(() => {
$bus.$on(`${props.selector}:setZoom`, (value) => {
zoom.value = value
})
$bus.$on(
`${props.selector}:updateZoom`,
() => (zoom.value = !zoom.value)
)
if (zoom.value) {
setZoomEvents()
}
})
onBeforeUnmount(() => {
setZoomEvents(true)
})
function setZoomEvents(detach = false) {
document.querySelectorAll('.image-zoom').forEach((elm) => {
;['mousemove', 'touchstart'].forEach((name) => {
if (detach) {
elm.removeEventListener(name, focusZoom)
} else {
elm.addEventListener(name, focusZoom, { passive: true })
}
})
;['mouseleave', 'touchend'].forEach((name) => {
if (detach) {
elm.removeEventListener(name, focusZoomOut)
} else {
elm.addEventListener(name, focusZoomOut, {
passive: true,
})
}
})
if (detach) {
focusZoomOut({ target: elm })
elm.removeEventListener('contextmenu', preventContextMenu)
if (
elm.tagName === 'IMG' &&
elm.parentElement.tagName === 'PICTURE'
) {
setTimeout(() => {
elm.parentElement.style.removeProperty('display')
elm.parentElement.style.removeProperty('overflow')
elm.style.removeProperty('transition')
}, props.transitionDuration)
}
} else {
elm.addEventListener('contextmenu', preventContextMenu)
elm.style.transition = `scale ${props.transitionDuration}ms ease-in-out`
if (
elm.tagName === 'IMG' &&
elm.parentElement.tagName === 'PICTURE'
) {
elm.parentElement.style.display = 'block'
elm.parentElement.style.overflow = 'hidden'
}
}
})
}
function preventContextMenu(e) {
e.preventDefault()
}
function focusZoom(e) {
const img = e.target
const imgRect = img.getBoundingClientRect()
let pageX = e.pageX
let pageY = e.pageY
if (e.constructor.name === 'TouchEvent') {
pageX = e.changedTouches[0].pageX
pageY = e.changedTouches[0].pageY
}
const offsetX = ((pageX - imgRect.left) / imgRect.width) * 100
const offsetY = ((pageY - imgRect.top) / imgRect.height) * 100
img.style.scale = 1.6
img.style.transformOrigin = `${offsetX}% ${offsetY}%`
}
function focusZoomOut(e) {
const img = e.target
img.style.removeProperty('scale')
setTimeout(() => {
img.style.removeProperty('transform-origin')
}, props.transitionDuration)
}
return {
zoom,
}
},
}
</script>
<template>
<div
v-show="!zoom || $device.isMobile"
class="pointer-events-none absolute inset-x-0 top-4 z-10 mx-auto max-w-max cursor-pointer rounded-full bg-dark/75 text-white min-h-[30px]"
role="button"
:class="zoomedIn ? 'px-2 py-2' : 'px-4 py-2'"
>
<div
v-if="($device.isMobile && !zoomedIn) || $device.isDesktop"
class="flex gap-3"
>
<nuxt-img src="/images/icons/icon-search-white.svg" width="20" />
<span>{{ $n('general.click-to-zoom') }}</span>
</div>
<nuxt-img
v-else
src="/images/icons/icon-cross-black.svg"
class="filter invert"
width="20"
/>
</div>
</template>
<script>
/**
* if selector have overlay elements like size or discount badge on product page image with position absolute
* then set pointer-events to none to avoid zoom out when mouse over overlay element
*/
export default {
name: 'VZoom',
props: {
selector: {
type: String,
required: true,
},
transitionDuration: {
type: Number,
default: 300,
},
},
setup(props) {
const { $bus, $device } = useNuxtApp()
const zoom = ref(false)
const zoomedIn = ref(false)
watch(zoom, (value) => {
setZoomEvents(!value)
const elements = document.querySelectorAll(props.selector)
elements.forEach((elm) => {
if (value) {
elm.style.cursor = 'zoom-in'
} else {
elm.style.removeProperty('cursor')
}
})
value ? zoomIn() : zoomOut()
})
watch(zoomedIn, (value, old) => {
if(value !== old && $device.isMobile){
const event = value ? 'zoomIn' : 'zoomOut';
$bus.$emit(`${props.selector}:${event}`)
value ? zoomIn() : zoomOut()
}
})
onMounted(() => {
$bus.$on(`${props.selector}:setZoom`, (value) => zoom.value = value)
$bus.$on(`${props.selector}:updateZoom`, () => zoom.value = !zoom.value)
})
onBeforeUnmount(() => {
setZoomEvents(true)
})
function zoomIn() {
const elements = document.querySelectorAll(props.selector)
elements.forEach(elm => {
// default style first to avoid jerk on first click on mobile
elm.style.transition = `scale ${props.transitionDuration}ms ease-in-out`;
focusZoom({target: elm}, true)
})
}
function zoomOut() {
const elements = document.querySelectorAll(props.selector)
elements.forEach(elm => {
focusZoomOut({target: elm},)
})
}
function enableZoom(value) {
zoom.value = value
value ? zoomIn() : zoomOut()
}
function setZoomEvents(detach = false) {
document.querySelectorAll('.image-zoom').forEach((elm) => {
;['touchstart'].forEach((name) => {
if (detach) {
elm.removeEventListener(name, enableZoom(!zoom.value))
} else {
elm.addEventListener(name, enableZoom(true), { passive: true })
}
})
;['mousemove','touchmove'].forEach((name) => {
if (detach) {
elm.removeEventListener(name, focusZoom)
} else {
elm.addEventListener(name, focusZoom, { passive: true })
}
})
;['mouseleave'].forEach((name) => {
if (detach) {
elm.removeEventListener(name, focusZoomOut)
} else {
elm.addEventListener(name, focusZoomOut, {
passive: true,
})
}
})
if (detach) {
focusZoomOut({ target: elm })
elm.removeEventListener('contextmenu', preventContextMenu)
if (
elm.tagName === 'IMG' &&
elm.parentElement.tagName === 'PICTURE'
) {
setTimeout(() => {
elm.parentElement.style.removeProperty('display')
elm.parentElement.style.removeProperty('overflow')
elm.style.removeProperty('transition')
}, props.transitionDuration)
}
} else {
elm.addEventListener('contextmenu', preventContextMenu)
elm.style.transition = `scale ${props.transitionDuration}ms ease-in-out`
if (
elm.tagName === 'IMG' &&
elm.parentElement.tagName === 'PICTURE'
) {
elm.parentElement.style.display = 'block'
elm.parentElement.style.overflow = 'hidden'
}
}
})
}
function preventContextMenu(e) {
e.preventDefault()
}
function inBoundaries(bounds,x,y) {
const l = bounds.left;
const t = bounds.top;
const h = bounds.height;
const w = bounds.width;
const maxX = l + w;
const maxY = t + h;
return (y <= maxY && y >= t) && (x <= maxX && x >= l);
}
function focusZoom(e, force = false) {
const img = e.target
const imgRect = img.getBoundingClientRect()
let pageX = e.pageX
let pageY = e.pageY
if (e.constructor.name === 'TouchEvent') {
pageX = e.changedTouches[0].pageX
pageY = e.changedTouches[0].pageY
}
// prevent image move when cursor is out of bound
if(!force && !inBoundaries(imgRect, pageX, pageY)) return;
const offsetX = ((pageX - imgRect.left) / imgRect.width) * 100
const offsetY = ((pageY - imgRect.top) / imgRect.height) * 100
img.style.scale = 1.6
img.style.transformOrigin = `${offsetX}% ${offsetY}%`
zoomedIn.value = true
}
function focusZoomOut(e) {
const img = e.target
img.style.removeProperty('scale')
setTimeout(() => {
img.style.removeProperty('transform-origin')
}, props.transitionDuration)
zoomedIn.value = false
}
return {
zoom,
zoomedIn,
}
},
}
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment