UNPKG

inner-image-zoom

Version:

A lightweight Vanilla JS package for magnifying an image within its parent container 🔎

374 lines (326 loc) • 12.6 kB
import { getBounds, getFullscreenStatus, getImgPropsDefaults, getOffsets, getRatios, getScaledDimensions } from './utils/images'; import { getDragMovePositions, getEventCoords, getInitialDragCoords, getIsValidDrag, getMouseMovePositions } from './utils/events'; const InnerImageZoom = (() => { function innerImageZoom(selector = '.iiz', options = {}) { if (options.$el) { return this.init(options.$el, options); } const $els = document.querySelectorAll(selector); if (!$els.length) { return; } if ($els.length > 1) { return Array.from($els).map(($el) => { return new InnerImageZoom(selector, { ...options, $el }); }); } return this.init($els[0], options); } innerImageZoom.prototype = { init: function ($el, options) { if ($el.tagName !== 'IMG' && !$el.querySelector('img')) { return; } this.setData($el, options); this.setSelectors($el); this.setListeners(); }, reinit: function (options) { this.uninit(); this.init(this.$el, options); }, uninit: function () { this.$container.parentNode && this.$container.parentNode.replaceChild(this.$el, this.$container); this.$portal && document.body.removeChild(this.$portal); }, setData: function ($el, options) { this.options = { moveType: 'pan', zoomType: 'click', zoomScale: 1, fadeDuration: 150, mobileBreakpoint: 640, ...options, ...$el.dataset }; if (!this.options.zoomSrc) { this.options.zoomSrc = $el.src || $el.querySelector('img').src; } this.isTouch = false; this.isZoomed = false; this.isValidDrag = false; this.isClosing = false; this.currentMoveType = this.options.moveType; this.imgProps = getImgPropsDefaults(); }, setSelectors: function ($el) { this.$el = $el.cloneNode(true); this.$container = this.createFigure($el, this.options); this.$img = this.createImg(this.$container); this.$zoomImg = this.createZoomImg(this.$container, this.options); this.$hint = this.createHint(this.$container, this.options); this.$closeButton = this.createCloseButton(this.$container, this.options); this.$portal = this.createPortal(this.options, this.$zoomImg, this.$closeButton); }, setListeners: function () { this.$container.addEventListener('touchstart', () => this.onTouchStart(), { passive: true }); this.$container.addEventListener('mouseenter', (e) => this.onMouseEnter(e)); this.$container.addEventListener('click', (e) => this.onClick(e)); this.$container.addEventListener('mousemove', (e) => this.onMouseMove(e), { passive: true }); this.$container.addEventListener('mouseleave', (e) => this.onMouseLeave(e)); this.$zoomImg.addEventListener('touchstart', (e) => this.onDragStart(e), { passive: true }); this.$zoomImg.addEventListener('touchend', (e) => this.onDragEnd(e), { passive: true }); this.$zoomImg.addEventListener('mousedown', (e) => this.onDragStart(e)); this.$zoomImg.addEventListener('mouseup', (e) => this.onDragEnd(e)); this.$zoomImg.onload = () => this.onLoad(); this.$zoomImg.addEventListener('transitionend', (e) => this.onTransitionEnd(e)); this.$closeButton && this.$closeButton.addEventListener('click', (e) => this.onClose(e)); this.dragMove = (e) => this.onDragMove(e); }, createFigure: function ($el, options) { let $container = $el; if ($el.tagName === 'IMG' || $el.tagName === 'PICTURE') { $container = document.createElement('figure'); $el.parentNode.insertBefore($container, $el); $el.classList.remove('iiz'); $container.appendChild($el); } $container.classList.add(...['iiz', ...(options.moveType === 'drag' ? ['iiz--drag'] : [])]); return $container; }, createImg: function ($container) { const $img = $container.querySelector('img'); $img.classList.add('iiz__img'); $img.style.transition = 'opacity 0ms linear, visibility 0ms linear'; return $img; }, createZoomImg: function ($container, options) { const $zoomImg = document.createElement('img'); $zoomImg.classList.add('iiz__zoom-img'); $zoomImg.alt = ''; $zoomImg.draggable = false; $zoomImg.style.left = '0px'; $zoomImg.style.top = '0px'; $zoomImg.style.transition = `opacity ${options.fadeDuration}ms linear, visibility ${options.fadeDuration}ms linear`; if (options.zoomPreload) { $zoomImg.setAttribute('src', options.zoomSrc); $container.appendChild($zoomImg); } return $zoomImg; }, createHint: function ($container, options) { if (options.hideHint) { return null; } const $hint = document.createElement('span'); $hint.classList.add('iiz__btn', 'iiz__hint'); $container.appendChild($hint); return $hint; }, createCloseButton: function ($container, options) { if (options.hideCloseButton) { return null; } const $closeButton = document.createElement('button'); $closeButton.classList.add('iiz__btn', 'iiz__close'); $closeButton.setAttribute('aria-label', 'Zoom Out'); $closeButton.style.transition = `opacity ${options.fadeDuration}ms linear, visibility ${options.fadeDuration}ms linear`; return $closeButton; }, createPortal: function (options, $zoomImg, $closeButton) { if (!options.fullscreenOnMobile) { return null; } const $portal = document.createElement('div'); $portal.classList.add('iiz__zoom-portal'); $portal.appendChild($zoomImg).setAttribute('src', this.options.zoomSrc); $closeButton && $portal.appendChild($closeButton); if ($closeButton) { $portal.appendChild($closeButton); } else { $zoomImg.addEventListener('click', (e) => this.onClick(e)); } return $portal; }, onTouchStart: function () { if (this.isZoomed) { return; } this.isTouch = true; this.isFullscreen = getFullscreenStatus(this.options.fullscreenOnMobile, this.options.mobileBreakpoint); this.currentMoveType = 'drag'; }, onMouseEnter: function (e) { if (this.isTouch) { return; } this.isClosing = false; this.$container.appendChild(this.$zoomImg); this.$zoomImg.src !== this.options.zoomSrc && this.$zoomImg.setAttribute('src', this.options.zoomSrc); this.$closeButton && this.$container.appendChild(this.$closeButton); this.options.zoomType === 'hover' && !this.isZoomed && this.onClick(e); }, onClick: function (e) { if (this.isZoomed) { if (this.isTouch) { !this.$closeButton && this.onClose(e); } else { !this.isValidDrag && this.zoomOut(); } return; } if (this.isTouch) { if (this.isFullscreen) { document.body.appendChild(this.$portal); } else { this.$container.appendChild(this.$zoomImg).setAttribute('src', this.options.zoomSrc); this.$closeButton && this.$container.appendChild(this.$closeButton); } } if (this.$zoomImg.complete) { this.onLoad(); this.zoomIn(e); } else { this.imgProps.onLoadCallback = this.zoomIn.bind(this, e); } }, onLoad: function () { const scaledDimensions = getScaledDimensions(this.$zoomImg, this.options.zoomScale); this.$zoomImg.setAttribute('width', scaledDimensions.width); this.$zoomImg.setAttribute('height', scaledDimensions.height); this.imgProps.scaledDimensions = scaledDimensions; this.imgProps.bounds = getBounds(this.$img, false); this.imgProps.ratios = getRatios(this.imgProps.bounds, scaledDimensions); if (this.imgProps.onLoadCallback) { this.imgProps.onLoadCallback(); this.imgProps.onLoadCallback = null; } }, onMouseMove: function (e) { if (this.currentMoveType === 'drag' || !this.isZoomed) { return; } const positions = getMouseMovePositions(e, this.imgProps); this.$zoomImg.style.left = `${positions.left}px`; this.$zoomImg.style.top = `${positions.top}px`; }, onMouseLeave: function (e) { if (this.isTouch) { return; } this.currentMoveType === 'drag' && this.isZoomed ? this.onDragEnd(e) : this.onClose(e); }, onDragStart: function (e) { if (this.currentMoveType !== 'drag') { return; } const coords = getEventCoords(e); this.imgProps.offsets = getOffsets(coords.x, coords.y, this.$zoomImg.offsetLeft, this.$zoomImg.offsetTop); this.$zoomImg.addEventListener(this.isTouch ? 'touchmove' : 'mousemove', this.dragMove, { passive: true }); if (!this.isTouch) { this.imgProps.eventPosition = coords; } }, onDragMove: function (e) { e.stopPropagation(); if (this.currentMoveType !== 'drag' || !this.isZoomed) { return; } const positions = getDragMovePositions(e, this.imgProps); this.$zoomImg.style.left = `${positions.left}px`; this.$zoomImg.style.top = `${positions.top}px`; }, onDragEnd: function (e) { if (this.currentMoveType !== 'drag') { return; } this.$zoomImg.removeEventListener(this.isTouch ? 'touchmove' : 'mousemove', this.dragMove); if (!this.isTouch) { this.isValidDrag = getIsValidDrag(e, this.imgProps); } }, onClose: function (e) { e.stopPropagation(); if (!(e.target.classList.contains('iiz__close') && !this.isTouch)) { if (!this.isZoomed || this.isFullscreen || !this.options.fadeDuration) { this.cleanup(); } else { this.isClosing = true; } } this.isZoomed && this.zoomOut(); }, onTransitionEnd: function (e) { this.isClosing && e.propertyName === 'opacity' && e.target === this.$zoomImg && this.cleanup(); }, initialMove: function (e) { this.imgProps.offsets = getOffsets( window.pageXOffset, window.pageYOffset, -this.imgProps.bounds.left, -this.imgProps.bounds.top ); this.onMouseMove(e); }, initialDrag: function (e) { const initialDragCoords = getInitialDragCoords(e, this.imgProps, this.isFullscreen); this.imgProps.bounds = getBounds(this.$img, this.isFullscreen); this.imgProps.offsets = getOffsets(0, 0, 0, 0); this.onDragMove({ ...initialDragCoords, stopPropagation: () => {} }); }, zoomIn: function (e) { this.isZoomed = true; this.$zoomImg.classList.add('iiz__zoom-img--visible'); this.$zoomImg.style.transitionDuration = `${this.isFullscreen ? 0 : this.options.fadeDuration}ms`; this.$img.classList.add('iiz__img--hidden'); this.$img.style.transitionDelay = `${this.isFullscreen ? 0 : this.options.fadeDuration}ms`; if (this.$closeButton && this.currentMoveType === 'drag') { this.$closeButton.classList.add('iiz__close--visible'); this.$closeButton.style.transitionDuration = `${this.isFullscreen ? 0 : this.options.fadeDuration}ms`; } this.currentMoveType === 'drag' ? this.initialDrag(e) : this.initialMove(e); this.options.afterZoomIn && this.options.afterZoomIn(); }, zoomOut: function () { this.isZoomed = false; this.$zoomImg.classList.remove('iiz__zoom-img--visible'); this.$img.classList.remove('iiz__img--hidden'); this.$img.style.transitionDelay = '0ms'; this.$closeButton && this.$closeButton.classList.remove('iiz__close--visible'); this.options.afterZoomOut && this.options.afterZoomOut(); }, cleanup: function () { if ((this.options.zoomPreload && this.isTouch) || !this.options.zoomPreload) { if (this.isFullscreen) { document.body.removeChild(this.$portal); } else { this.$container.removeChild(this.$zoomImg); this.$closeButton && this.$container.removeChild(this.$closeButton); } this.imgProps.current = getImgPropsDefaults(); } this.isTouch = false; this.isFullscreen = false; this.currentMoveType = this.options.moveType; this.isClosing = false; } }; return innerImageZoom; })(); export default InnerImageZoom;