hoverzoom-js
Version:
A lightweight, high-performance hover-to-zoom image magnifier plugin with zero dependencies
1 lines • 9.4 kB
JavaScript
class HoverZoom{constructor(e={}){this.iteration=0;const t={classNames:{container:"hoverzoom",image:"hoverzoom-image",zoomedImage:"hoverzoom-zoom",magnifier:"hoverzoom-magnifier",magnifierRound:"hoverzoom-magnifier--round",magnifierImage:"hoverzoom-magnifier--image"},position:"right",type:"outside",largeImage:"",blur:!1,grayscale:!1,throttleDelay:16};this.options={...t,...e,classNames:{...t.classNames,...e.classNames||{}}},this.domCache=new Map,this.rafId=null,this.lastCall=0,this.imageCache=new Map}throttle(e,t){return(...i)=>{const s=Date.now();s-this.lastCall>=t&&(this.lastCall=s,e.apply(this,i))}}cacheElement(e,t){return this.domCache.has(e)||this.domCache.set(e,t),this.domCache.get(e)}preloadImage(e){if(this.imageCache.has(e)){const t=this.imageCache.get(e);if(t)return Promise.resolve(t)}return new Promise((t,i)=>{const s=new Image;s.onload=()=>{this.imageCache.set(e,s),t(s)},s.onerror=i,s.src=e})}init(){const e=document.getElementsByClassName(this.options.classNames.container);for(let t=0;t<e.length;t++)this.iteration=t,this.currentContainer=e[t],this.applyHoverZoom()}applyHoverZoom(){const{image:e}=this.options.classNames,t=this.currentContainer.querySelector(`.${e}`);if(!t)return;this.currentImageEl=t,this.currentImageEl.setAttribute("id",`${e}-${this.iteration}`),this.currentImageEl.setAttribute("role","img"),this.currentImageEl.setAttribute("aria-label",`Zoomable image ${this.iteration+1}`),this.currentImageEl.setAttribute("tabindex","0"),this.cacheElement(`image-${this.iteration}`,this.currentImageEl),this.options.largeImage=this.currentImageEl.dataset.largeImage?this.currentImageEl.dataset.largeImage:this.currentImageEl.src,this.preloadImage(this.options.largeImage).catch(()=>{console.warn("Failed to preload image:",this.options.largeImage)});"outside"===(this.currentImageEl.dataset.type||this.options.type)?this.outsideZoom():this.insideZoom(),this.addMouseListener(),this.addKeyboardListener()}outsideZoom(){const{zoomedImage:e,magnifier:t,magnifierImage:i}=this.options.classNames;this.zoomedElement=document.createElement("DIV"),this.zoomedElement.classList.add(e),this.zoomedElement.setAttribute("id",`${e}-${this.iteration}`),this.zoomedElement.setAttribute("role","region"),this.zoomedElement.setAttribute("aria-label","Zoomed image preview"),this.zoomedElement.setAttribute("aria-live","polite"),this.zoomedElement.style.setProperty("background-image",`url('${this.options.largeImage}')`);const s=this.currentImageEl.offsetWidth,a=this.currentImageEl.offsetHeight;this.zoomedElement.style.setProperty("background-size",`${4*s}px ${4*a}px`);const r=this.currentImageEl.dataset.position||this.options.position;this.currentContainer.style.setProperty("flex-direction","right"===r?"row":"column"),this.attachZoomedImage(),this.magnifierElement=document.createElement("DIV"),this.magnifierElement.classList.add(t),this.magnifierElement.setAttribute("id",`${t}-${this.iteration}`),this.magnifierElement.setAttribute("role","tooltip"),this.magnifierElement.setAttribute("aria-label","Magnifying glass lens"),this.magnifierElement.setAttribute("aria-hidden","true"),this.magnifierImageElement=document.createElement("IMG"),this.magnifierImageElement.classList.add(i),this.magnifierImageElement.setAttribute("id",`${i}-${this.iteration}`),this.magnifierImageElement.setAttribute("src",this.options.largeImage),this.magnifierImageElement.setAttribute("alt","Magnified view"),this.magnifierImageElement.setAttribute("role","presentation"),this.magnifierImageElement.style.setProperty("height",`${a}px`),this.magnifierImageElement.style.setProperty("width",`${s}px`),this.magnifierElement.appendChild(this.magnifierImageElement),this.currentContainer.appendChild(this.magnifierElement),this.cacheElement(`zoomed-${this.iteration}`,this.zoomedElement),this.cacheElement(`magnifier-${this.iteration}`,this.magnifierElement),this.cacheElement(`magnifier-image-${this.iteration}`,this.magnifierImageElement);const o=this.magnifierElement.offsetHeight*s/a;this.magnifierElement.style.setProperty("width",`${o}px`)}attachZoomedImage(){this.zoomedElement.style.setProperty("height",`${this.currentImageEl.offsetHeight}px`),this.zoomedElement.style.setProperty("width",`${this.currentImageEl.offsetWidth}px`);"right"===(this.currentImageEl.dataset.position||this.options.position)?this.zoomedElement.style.setProperty("margin-left","6px"):this.zoomedElement.style.setProperty("margin-top","6px"),this.currentContainer.appendChild(this.zoomedElement)}insideZoom(){const{magnifier:e,magnifierImage:t,magnifierRound:i}=this.options.classNames;this.magnifierElement=document.createElement("DIV"),this.magnifierElement.classList.add(e),this.magnifierElement.classList.add(i),this.magnifierElement.setAttribute("id",`${e}-${this.iteration}`),this.magnifierElement.setAttribute("role","tooltip"),this.magnifierElement.setAttribute("aria-label","Magnifying glass lens"),this.magnifierElement.setAttribute("aria-hidden","true"),this.magnifierImageElement=document.createElement("DIV"),this.magnifierImageElement.classList.add(t),this.magnifierImageElement.setAttribute("id",`${t}-${this.iteration}`),this.magnifierImageElement.style.setProperty("background-image",`url('${this.options.largeImage}')`);const s=this.currentImageEl.offsetWidth,a=this.currentImageEl.offsetHeight;this.magnifierImageElement.style.setProperty("background-size",`${4*s}px ${4*a}px`),this.magnifierImageElement.style.setProperty("height",`${a}px`),this.magnifierImageElement.style.setProperty("width",`${s}px`),this.magnifierElement.appendChild(this.magnifierImageElement),this.currentContainer.appendChild(this.magnifierElement),this.cacheElement(`magnifier-${this.iteration}`,this.magnifierElement),this.cacheElement(`magnifier-image-${this.iteration}`,this.magnifierImageElement)}addMouseListener(){const{image:e,magnifier:t,magnifierImage:i,zoomedImage:s}=this.options.classNames,a=this.domCache.get(`magnifier-${this.iteration}`)||document.getElementById(`${t}-${this.iteration}`),r=this.domCache.get(`zoomed-${this.iteration}`)||document.getElementById(`${s}-${this.iteration}`),o=this.domCache.get(`image-${this.iteration}`)||document.getElementById(`${e}-${this.iteration}`),n=this.domCache.get(`magnifier-image-${this.iteration}`)||document.getElementById(`${i}-${this.iteration}`);if(!a||!o)return;const{offsetHeight:m,offsetWidth:l}=a,h=o.dataset.type||this.options.type,g=.5*l,d="outside"===h?-.52*m:-.51*m,c=l/2-1,p=m/2;let f="opacity(0.8)";(o.dataset.blur||this.options.blur)&&(f+=" blur(2px)"),(o.dataset.grayscale||this.options.grayscale)&&(f+=" grayscale(100%)");const y=this.throttle(e=>{this.rafId&&cancelAnimationFrame(this.rafId),this.rafId=requestAnimationFrame(()=>{if(!o||!a)return;o.style.setProperty("filter",f),a.style.setProperty("opacity","1"),"outside"===h&&r&&r.style.setProperty("opacity","1");const t=e.offsetX?e.offsetX:e.pageX-o.offsetLeft,i=e.offsetY?e.offsetY:e.pageY-o.offsetTop;"outside"===h&&r&&r.style.setProperty("background-position",`${3*-t}px ${3*-i}px`),"outside"!==h&&n&&n.style.setProperty("background-position",`${3*-t}px ${3*-i}px`),a.style.setProperty("transform",`translate3d(${e.offsetX-g}px, ${e.offsetY+d}px, 0)`),n&&n.style.setProperty("transform",`translate3d(${-e.offsetX+c}px, ${-e.offsetY+p}px, 0)`)})},this.options.throttleDelay);this.currentImageEl.addEventListener("mousemove",y),this.currentImageEl.addEventListener("mouseout",()=>{this.rafId&&(cancelAnimationFrame(this.rafId),this.rafId=null),o&&a&&(o.style.setProperty("filter","unset"),a.style.setProperty("opacity","0"),"outside"===h&&r&&r.style.setProperty("opacity","0"))})}addKeyboardListener(){const{image:e,magnifier:t,zoomedImage:i}=this.options.classNames,s=this.domCache.get(`image-${this.iteration}`)||document.getElementById(`${e}-${this.iteration}`),a=this.domCache.get(`magnifier-${this.iteration}`)||document.getElementById(`${t}-${this.iteration}`),r=this.domCache.get(`zoomed-${this.iteration}`)||document.getElementById(`${i}-${this.iteration}`);if(!s||!a)return;const o=s.dataset.type||this.options.type;let n=!1,m="opacity(0.8)";(s.dataset.blur||this.options.blur)&&(m+=" blur(2px)"),(s.dataset.grayscale||this.options.grayscale)&&(m+=" grayscale(100%)"),s.addEventListener("keydown",e=>{if("Enter"===e.key||" "===e.key)if(e.preventDefault(),n=!n,n){s.style.setProperty("filter",m),a.style.setProperty("opacity","1"),"outside"===o&&r&&r.style.setProperty("opacity","1");const e=s.getBoundingClientRect(),t=e.width/2,i=e.height/2,n=new MouseEvent("mousemove",{bubbles:!0,cancelable:!0});Object.defineProperty(n,"offsetX",{value:t}),Object.defineProperty(n,"offsetY",{value:i}),s.dispatchEvent(n)}else s.style.setProperty("filter","unset"),a.style.setProperty("opacity","0"),"outside"===o&&r&&r.style.setProperty("opacity","0");else"Escape"===e.key&&(e.preventDefault(),n=!1,s.style.setProperty("filter","unset"),a.style.setProperty("opacity","0"),"outside"===o&&r&&r.style.setProperty("opacity","0"))}),s.addEventListener("focus",()=>{s.style.setProperty("outline","2px solid #4A90E2"),s.style.setProperty("outline-offset","2px")}),s.addEventListener("blur",()=>{s.style.setProperty("outline","none"),n&&(n=!1,s.style.setProperty("filter","unset"),a.style.setProperty("opacity","0"),"outside"===o&&r&&r.style.setProperty("opacity","0"))})}destroy(){this.rafId&&cancelAnimationFrame(this.rafId),this.domCache.clear(),this.imageCache.clear()}}export{HoverZoom as default};