Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/medium-zoom.css
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,7 @@
cursor: zoom-out;
will-change: transform;
}

.medium-zoom-image--change {
transition: none !important;
}
244 changes: 244 additions & 0 deletions src/medium-zoom.js
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,249 @@ const mediumZoom = (selector, options = {}) => {
return zoom
}

const change = ({ target } = {}) =>
new Promise(resolve => {
if (target && images.indexOf(target) === -1) {
resolve(zoom)
return
}

if (isAnimating || !active.original) {
resolve(zoom)
return
}

const _closeWrapper = () => {
active.zoomed.classList.remove('medium-zoom-image--change')
if (active.zoomedHd) {
active.zoomedHd.classList.remove('medium-zoom-image--change')
}
close()
}

isAnimating = true

active.zoomed.style.transform = ''

if (active.zoomedHd) {
active.zoomedHd.style.transform = ''
}

active.original.classList.remove('medium-zoom-image--hidden')
document.body.removeChild(active.zoomed)
if (active.zoomedHd) {
document.body.removeChild(active.zoomedHd)
}
active.zoomed.classList.remove('medium-zoom-image--opened')

active.original = null
active.zoomed = null
active.zoomedHd = null

const _animate = () => {
let container = {
width: document.documentElement.clientWidth,
height: document.documentElement.clientHeight,
left: 0,
top: 0,
right: 0,
bottom: 0,
}
let viewportWidth
let viewportHeight

if (zoomOptions.container) {
if (zoomOptions.container instanceof Object) {
// The container is given as an object with properties like width, height, left, top
container = {
...container,
...zoomOptions.container,
}

// We need to adjust custom options like container.right or container.bottom
viewportWidth =
container.width -
container.left -
container.right -
zoomOptions.margin * 2
viewportHeight =
container.height -
container.top -
container.bottom -
zoomOptions.margin * 2
} else {
// The container is given as an element
const zoomContainer = isNode(zoomOptions.container)
? zoomOptions.container
: document.querySelector(zoomOptions.container)

const {
width,
height,
left,
top,
} = zoomContainer.getBoundingClientRect()

container = {
...container,
width,
height,
left,
top,
}
}
}

viewportWidth =
viewportWidth || container.width - zoomOptions.margin * 2
viewportHeight =
viewportHeight || container.height - zoomOptions.margin * 2

const zoomTarget = active.zoomedHd || active.original
const naturalWidth = isSvg(zoomTarget)
? viewportWidth
: zoomTarget.naturalWidth || viewportWidth
const naturalHeight = isSvg(zoomTarget)
? viewportHeight
: zoomTarget.naturalHeight || viewportHeight
const { top, left, width, height } = zoomTarget.getBoundingClientRect()

const scaleX =
Math.min(Math.max(width, naturalWidth), viewportWidth) / width
const scaleY =
Math.min(Math.max(height, naturalHeight), viewportHeight) / height
const scale = Math.min(scaleX, scaleY)
const translateX =
(-left +
(viewportWidth - width) / 2 +
zoomOptions.margin +
container.left) /
scale
const translateY =
(-top +
(viewportHeight - height) / 2 +
zoomOptions.margin +
container.top) /
scale
const transform = `scale(${scale}) translate3d(${translateX}px, ${translateY}px, 0)`

active.zoomed.style.transform = transform

if (active.zoomedHd) {
active.zoomedHd.style.transform = transform
}
}

if (target) {
// The zoom was triggered manually via a click
active.original = target
} else if (images.length > 0) {
// The zoom was triggered programmatically, select the first image in the list
;[active.original] = images
} else {
resolve(zoom)
return
}

scrollTop =
window.pageYOffset ||
document.documentElement.scrollTop ||
document.body.scrollTop ||
0
active.zoomed = cloneTarget(active.original)

// If the selected <img> tag is inside a <picture> tag, set the
// currently-applied source as the cloned `src=` attribute.
// (as these might differ, or src= might be unset in some cases)
if (
active.original.parentElement &&
active.original.parentElement.tagName === 'PICTURE' &&
active.original.currentSrc
) {
active.zoomed.src = active.original.currentSrc
}

document.body.appendChild(active.zoomed)

active.original.classList.add('medium-zoom-image--hidden')
active.zoomed.classList.add(
'medium-zoom-image--opened',
'medium-zoom-image--change'
)

active.zoomed.addEventListener('click', _closeWrapper)

if (active.original.getAttribute('data-zoom-src')) {
active.zoomedHd = active.zoomed.cloneNode()

// Reset the `scrset` property or the HD image won't load.
active.zoomedHd.removeAttribute('srcset')
active.zoomedHd.removeAttribute('sizes')
// Remove loading attribute so the browser can load the image normally
active.zoomedHd.removeAttribute('loading')

active.zoomedHd.src = active.zoomed.getAttribute('data-zoom-src')

active.zoomedHd.onerror = () => {
clearInterval(getZoomTargetSize)
console.warn(
`Unable to reach the zoom image target ${active.zoomedHd.src}`
)
active.zoomedHd = null
_animate()
}

// We need to access the natural size of the full HD
// target as fast as possible to compute the animation.
const getZoomTargetSize = setInterval(() => {
if (__TEST__ ? true : active.zoomedHd.complete) {
clearInterval(getZoomTargetSize)
active.zoomedHd.classList.add(
'medium-zoom-image--opened',
'medium-zoom-image--change'
)
active.zoomedHd.addEventListener('click', _closeWrapper)
document.body.appendChild(active.zoomedHd)
_animate()
}
}, 10)
} else if (active.original.hasAttribute('srcset')) {
// If an image has a `srcset` attribuet, we don't know the dimensions of the
// zoomed (HD) image (like when `data-zoom-src` is specified).
// Therefore the approach is quite similar.
active.zoomedHd = active.zoomed.cloneNode()

// Resetting the sizes attribute tells the browser to load the
// image best fitting the current viewport size, respecting the `srcset`.
active.zoomedHd.removeAttribute('sizes')

// In Firefox, the `loading` attribute needs to be set to `eager` (default
// value) for the load event to be fired.
active.zoomedHd.removeAttribute('loading')

// Wait for the load event of the hd image. This will fire if the image
// is already cached.
const loadEventListener = active.zoomedHd.addEventListener(
'load',
() => {
active.zoomedHd.removeEventListener('load', loadEventListener)
active.zoomedHd.classList.add(
'medium-zoom-image--opened',
'medium-zoom-image--change'
)
active.zoomedHd.addEventListener('click', _closeWrapper)
document.body.appendChild(active.zoomedHd)
_animate()
}
)
} else {
_animate()
}

isAnimating = false
resolve(zoom)
})

const open = ({ target } = {}) => {
const _animate = () => {
let container = {
Expand Down Expand Up @@ -553,6 +796,7 @@ const mediumZoom = (selector, options = {}) => {
getOptions,
getImages,
getZoomedImage,
change,
}

return zoom
Expand Down
108 changes: 108 additions & 0 deletions stories/change.stories.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { storiesOf } from '@storybook/html'

storiesOf('change()', module).add(
'default',
() =>
`
<template id="template">
<div class="template-wrapper">
<header class="template-header">
<button data-zoom-previous>PREVIOUS</button>
<button data-zoom-next>NEXT</button>
</header>
<div class="template-container" data-zoom-container data-zoom-close></div>
</div>
</template>

<div style="display: flex; align-items: center;">
<div style="flex: 1;">
<img srcset="
image-1x300.jpg 300w,
image-1x600.jpg 600w,
image-1x800.jpg 800w,
image-1x1000.jpg 1000w,
image-1x1200.jpg 1200w
">
</div>

<div style="flex: 1;">
<img id="image-2" src="image-2.jpg">
</div>

<div style="flex: 1;">
<img id="image-3" src="image-3.thumbnail.jpg" data-zoom-src="image-3.jpg">
</div>
</div>

<script>
const zoom = mediumZoom('img', {
template: '#template',
container: '[data-zoom-container]',
});

zoom.on('opened', () => {
const closeButton = document.querySelector('[data-zoom-close]');
closeButton.addEventListener('click', () => zoom.close());

const nextButton = document.querySelector('[data-zoom-next]');
const previousButton = document.querySelector('[data-zoom-previous]');
nextButton.addEventListener('click', () => {
const images = zoom.getImages();
const imageIndex = images.indexOf(zoom.getZoomedImage());
const newIndex = images.length === imageIndex ? 0 : imageIndex + 1;
zoom.change({ target: images[newIndex] });
});
previousButton.addEventListener('click', () => {
const images = zoom.getImages();
const imageIndex = images.indexOf(zoom.getZoomedImage());
const newIndex = imageIndex === 0 ? images.length - 1 : imageIndex - 1;
zoom.change({ target: images[newIndex] });
});
});
</script>

<style>

img {
max-width: 100%;
height: auto;
}

.container {
width: 100%;
max-width: 768px;
margin: 48px auto;
padding: 16px;
}

.template-wrapper {
position: fixed;
display: flex;
flex-direction: column;
top: 0;
left: 0;
width: 100%;
height: 100vh;
}

.template-container {
width: 100%;
height: calc(100% - 64px);
margin: 0 auto;
}

.template-header {
display: flex;
align-items: center;
height: 64px;
padding: 16px;
justify-content: center;
}
</style>
`,
{
notes: {
markdown: `Change the zoomed image with custom template buttons`,
},
}
)