diff --git a/dist/js/parvus.esm.js b/dist/js/parvus.esm.js index 7202a22..4789963 100644 --- a/dist/js/parvus.esm.js +++ b/dist/js/parvus.esm.js @@ -69,11 +69,13 @@ var en = { slideLabel: 'Image' }; +/** + * Parvus Lightbox + * + * @param {Object} userOptions - User configuration options + * @returns {Object} Parvus instance + */ function Parvus(userOptions) { - /** - * Global variables - * - */ const BROWSER_WINDOW = window; const GROUP_ATTRIBUTES = { triggerElements: [], @@ -82,6 +84,7 @@ function Parvus(userOptions) { contentElements: [] }; const GROUPS = {}; + const activePointers = new Map(); let groupIdCounter = 0; let newGroup = null; let activeGroup = null; @@ -102,6 +105,11 @@ function Parvus(userOptions) { let isDraggingX = false; let isDraggingY = false; let pointerDown = false; + let pinchStartDistance = 0; + let currentScale = 1; + let isPinching = false; + let lastScale = 1; + let baseScale = 1; let offset = null; let offsetTmp = null; let resizeTicking = false; @@ -1059,9 +1067,6 @@ function Parvus(userOptions) { const pointerdownHandler = event => { event.preventDefault(); event.stopPropagation(); - if (event.pointerType === 'mouse' && !config.simulateTouch) { - return; - } isDraggingX = false; isDraggingY = false; pointerDown = true; @@ -1069,6 +1074,7 @@ function Parvus(userOptions) { pageX, pageY } = event; + activePointers.set(event.pointerId, event); drag.startX = pageX; drag.startY = pageY; const { @@ -1089,15 +1095,39 @@ function Parvus(userOptions) { */ const pointermoveHandler = event => { event.preventDefault(); - if (pointerDown) { - const { - pageX, - pageY - } = event; - drag.endX = pageX; - drag.endY = pageY; - doSwipe(); + if (!pointerDown) { + return; + } + const currentImg = GROUPS[activeGroup].sliderElements[currentIndex]; + + // Update pointer position + activePointers.set(event.pointerId, event); + + // Zoom + if (activePointers.size === 2) { + const points = Array.from(activePointers.values()); + const distance = Math.hypot(points[1].clientX - points[0].clientX, points[1].clientY - points[0].clientY); + if (!isPinching) { + pinchStartDistance = distance; + isPinching = true; + baseScale = lastScale; + } + currentScale = baseScale * (distance / pinchStartDistance); + currentScale = Math.min(Math.max(1, currentScale), 3); + currentImg.style.transform = `scale(${currentScale})`; + lastScale = currentScale; + return; } + if (currentScale > 1) { + return; + } + const { + pageX, + pageY + } = event; + drag.endX = pageX; + drag.endY = pageY; + doSwipe(); }; /** @@ -1111,6 +1141,7 @@ function Parvus(userOptions) { const pointerupHandler = event => { event.stopPropagation(); pointerDown = false; + isPinching = false; const { slider } = GROUPS[activeGroup]; @@ -1120,6 +1151,12 @@ function Parvus(userOptions) { updateAfterDrag(); } clearDrag(); + activePointers.delete(event.pointerId); + if (currentScale > 1) { + baseScale = lastScale; + } else { + pinchStartDistance = 0; + } }; /** diff --git a/dist/js/parvus.esm.min.js b/dist/js/parvus.esm.min.js index 82434b6..d01c91d 100644 --- a/dist/js/parvus.esm.min.js +++ b/dist/js/parvus.esm.min.js @@ -8,4 +8,4 @@ * MIT license */ -const e=['a:not([inert]):not([tabindex^="-"])','button:not([inert]):not([tabindex^="-"]):not(:disabled)','[tabindex]:not([inert]):not([tabindex^="-"])'],t=window,r=()=>t.innerWidth-document.documentElement.clientWidth;var i={lightboxLabel:"This is a dialog window that overlays the main content of the page. The modal displays the enlarged image. Pressing the Escape key will close the modal and bring you back to where you were on the page.",lightboxLoadingIndicatorLabel:"Image loading",lightboxLoadingError:"The requested image cannot be loaded.",controlsLabel:"Controls",previousButtonLabel:"Previous image",nextButtonLabel:"Next image",closeButtonLabel:"Close dialog window",sliderLabel:"Images",slideLabel:"Image"};function n(t){const n=window,s={triggerElements:[],slider:null,sliderElements:[],contentElements:[]},a={};let l=0,o=null,d=null,u=0,c={},p=null,m=null,g=1,h=null,b=null,v=null,f=null,E=null,A=null,y=null,w=null,L={},_=!1,x=!1,C=!1,k=null,T=null,I=!1,N=!0;const M=n.matchMedia("(prefers-reduced-motion)"),S=()=>{N=!!M.matches},$=e=>{const t=e.dataset.group||`default-${l}`;return++l,e.hasAttribute("data-group")||e.setAttribute("data-group",t),t},B=e=>{if(p||z(),!("A"===e.tagName&&e.hasAttribute("href")||"BUTTON"===e.tagName&&e.hasAttribute("data-target")))throw new Error("Use a link with the 'href' attribute or a button with the 'data-target' attribute. Both attributes must contain a path to the image file.");if(o=$(e),a[o]||(a[o]=structuredClone(s)),a[o].triggerElements.includes(e))throw new Error("Ups, element already added.");if(a[o].triggerElements.push(e),c.zoomIndicator&&((e,t)=>{if(e.querySelector("img")&&null===e.querySelector(".parvus-zoom__indicator")){const r=document.createElement("div");r.className="parvus-zoom__indicator",r.innerHTML=t.lightboxIndicatorIcon,e.appendChild(r)}})(e,c),e.classList.add("parvus-trigger"),e.addEventListener("click",te),ue()&&o===d){const t=a[o].triggerElements.indexOf(e);H(t),P(e,t,(()=>{F(t)})),Q(),R(),K()}},q=e=>{if(!e||!e.hasAttribute("data-group"))return;const t=$(e);if(!a[t]||!a[t].triggerElements.includes(e))return;const r=a[t].triggerElements.indexOf(e);a[t].triggerElements.splice(r,1),a[t].sliderElements.splice(r,1),c.zoomIndicator&&(e=>{if(e.querySelector("img")&&null!==e.querySelector(".parvus-zoom__indicator")){const t=e.querySelector(".parvus-zoom__indicator");e.removeChild(t)}})(e),ue()&&t===d&&(Q(),R(),K()),e.removeEventListener("click",te),e.classList.remove("parvus-trigger")},z=()=>{p=document.createElement("dialog"),p.setAttribute("role","dialog"),p.setAttribute("aria-modal","true"),p.setAttribute("aria-label",c.l10n.lightboxLabel),p.classList.add("parvus"),m=document.createElement("div"),m.classList.add("parvus__overlay"),p.appendChild(m),h=document.createElement("div"),h.className="parvus__toolbar",b=document.createElement("div"),v=document.createElement("div"),f=document.createElement("div"),f.className="parvus__controls",f.setAttribute("role","group"),f.setAttribute("aria-label",c.l10n.controlsLabel),v.appendChild(f),y=document.createElement("button"),y.className="parvus__btn parvus__btn--close",y.setAttribute("type","button"),y.setAttribute("aria-label",c.l10n.closeButtonLabel),y.innerHTML=c.closeButtonIcon,f.appendChild(y),E=document.createElement("button"),E.className="parvus__btn parvus__btn--previous",E.setAttribute("type","button"),E.setAttribute("aria-label",c.l10n.previousButtonLabel),E.innerHTML=c.previousButtonIcon,f.appendChild(E),A=document.createElement("button"),A.className="parvus__btn parvus__btn--next",A.setAttribute("type","button"),A.setAttribute("aria-label",c.l10n.nextButtonLabel),A.innerHTML=c.nextButtonIcon,f.appendChild(A),w=document.createElement("div"),w.className="parvus__counter",b.appendChild(w),h.appendChild(b),h.appendChild(v),p.appendChild(h),document.body.appendChild(p)},H=e=>{if(void 0!==a[d].sliderElements[e])return;const t=document.createElement("div"),r=document.createElement("div"),i=a[d].triggerElements.length;if(t.className="parvus__slide",t.style.position="absolute",t.style.left=100*e+"%",t.setAttribute("aria-hidden","true"),t.appendChild(r),i>1&&(t.setAttribute("role","group"),t.setAttribute("aria-label",`${c.l10n.slideLabel} ${e+1}/${i}`)),a[d].sliderElements[e]=t,e>=u){const r=(e=>{const t=a[d].sliderElements,r=t.length;for(let i=e+1;i{const t=a[d].sliderElements;for(let r=e-1;r>=0;r--)if(void 0!==t[r])return r;return-1})(e);-1!==r?a[d].sliderElements[r].after(t):a[d].slider.prepend(t)}},X=e=>{if(p&&e&&e.classList.contains("parvus-trigger")&&!ue()){if(d=$(e),!a[d].triggerElements.includes(e))throw new Error("Ups, I can't find the element.");u=a[d].triggerElements.indexOf(e),history.pushState({parvus:"close"},"Image",window.location.href),oe(),c.hideScrollbar&&(document.body.style.marginInlineEnd=`${r()}px`,document.body.style.overflow="hidden"),p.classList.add("parvus--is-opening"),p.showModal(),(()=>{const e=document.createElement("div");e.className="parvus__slider",a[d].slider=e,p.appendChild(e)})(),H(u),G(),Q(),R(),K(),D(u),P(e,u,(()=>{F(u,!0),p.classList.remove("parvus--is-opening"),a[d].slider.classList.add("parvus__slider--animate")})),O(u+1),O(u-1),ce("open")}},Y=()=>{if(!ue())throw new Error("Ups, I'm already closed.");const e=a[d].contentElements[u],t=a[d].triggerElements[u];de(),J(),"close"===history.state?.parvus&&history.back(),p.classList.add("parvus--is-closing");const r=()=>{W(u),p.close(),p.classList.remove("parvus--is-closing"),p.classList.remove("parvus--is-vertical-closing"),a[d].slider.remove(),a[d].slider=null,a[d].sliderElements=[],a[d].contentElements=[],w.removeAttribute("aria-hidden"),E.removeAttribute("aria-hidden"),E.removeAttribute("aria-disabled"),A.removeAttribute("aria-hidden"),A.removeAttribute("aria-disabled"),c.hideScrollbar&&(document.body.style.marginInlineEnd="",document.body.style.overflow="")};if(e&&"IMG"===e.tagName)if(document.startViewTransition){e.style.viewTransitionName="lightboximage";document.startViewTransition((()=>{e.style.opacity="0",e.style.viewTransitionName=null,t.style.viewTransitionName="lightboximage"})).finished.finally((()=>{r(),t.style.viewTransitionName=null}))}else e.style.opacity="0",r();else r()},O=e=>{e<0||e>=a[d].triggerElements.length||void 0!==a[d].sliderElements[e]||(H(e),P(a[d].triggerElements[e],e,(()=>{F(e)})))},D=e=>{a[d].sliderElements[e].setAttribute("aria-hidden","false")},P=(e,t,r)=>{const{contentElements:i,sliderElements:n}=a[d];if(void 0!==i[t])return void(r&&"function"==typeof r&&r());const s=n[t].querySelector("div"),l=new Image,o=document.createElement("div"),u=e.querySelector("img"),p=document.createElement("div");o.className="parvus__content",p.className="parvus__loader",p.setAttribute("role","progressbar"),p.setAttribute("aria-label",c.l10n.lightboxLoadingIndicatorLabel),s.appendChild(p);new Promise(((e,t)=>{l.onload=()=>e(l),l.onerror=e=>t(e)})).then((r=>{r.style.opacity=0,o.appendChild(r),s.appendChild(o),c.captions&&((e,t,r,i)=>{const n=document.createElement("div");let s=null;if(n.className="parvus__caption","self"===c.captionsSelector)r.hasAttribute(c.captionsAttribute)&&""!==r.getAttribute(c.captionsAttribute)&&(s=r.getAttribute(c.captionsAttribute));else{const e=r.querySelector(c.captionsSelector);null!==e&&(s=e.hasAttribute(c.captionsAttribute)&&""!==e.getAttribute(c.captionsAttribute)?e.getAttribute(c.captionsAttribute):e.innerHTML)}if(null!==s){const r=`parvus__caption-${i}`;n.id=r,n.innerHTML=`

${s}

`,e.appendChild(n),t.setAttribute("aria-describedby",r)}})(s,l,e,t),i[t]=r,r.setAttribute("width",r.naturalWidth),r.setAttribute("height",r.naturalHeight),ee(n[t],r)})).catch((()=>{const e=document.createElement("div");e.classList.add("parvus__content"),e.classList.add("parvus__content--error"),e.innerHTML=`${c.l10n.lightboxLoadingError}`,s.appendChild(e),i[t]=e})).finally((()=>{s.removeChild(p),r&&"function"==typeof r&&r()})),e.hasAttribute("data-sizes")&&""!==e.getAttribute("data-sizes")&&l.setAttribute("sizes",e.getAttribute("data-sizes")),e.hasAttribute("data-srcset")&&""!==e.getAttribute("data-srcset")&&l.setAttribute("srcset",e.getAttribute("data-srcset")),"A"===e.tagName?l.setAttribute("src",e.href):l.setAttribute("src",e.getAttribute("data-target")),u&&u.hasAttribute("alt")&&""!==u.getAttribute("alt")?l.alt=u.alt:e.hasAttribute("data-alt")&&""!==e.getAttribute("data-alt")?l.alt=e.getAttribute("data-alt"):l.alt=""},F=(e,t)=>{const r=a[d].contentElements[e];if(r&&"IMG"===r.tagName){const i=a[d].triggerElements[e];if(t&&document.startViewTransition){i.style.viewTransitionName="lightboximage";document.startViewTransition((()=>{r.style.opacity="",i.style.viewTransitionName=null,r.style.viewTransitionName="lightboximage"})).finished.finally((()=>{r.style.viewTransitionName=null}))}else r.style.opacity=""}else r.style.opacity=""},j=e=>{const t=u;if(!ue())throw new Error("Oops, I'm closed.");{if("number"!=typeof e||isNaN(e))throw new Error("Oops, no slide specified.");const t=a[d].triggerElements;if(e===u)throw new Error(`Oops, slide ${e} is already selected.`);if(e<-1||e>=t.length)throw new Error(`Oops, I can't find slide ${e}.`)}void 0!==a[d].sliderElements[e]||(H(e),P(a[d].triggerElements[e],e,(()=>{F(e)}))),D(e),u=e,G(),et&&(R(),O(e+1)),W(t),K(),ce("select")},U=()=>{u>0&&j(u-1)},V=()=>{const{triggerElements:e}=a[d];u{void 0!==a[d].sliderElements[e]&&a[d].sliderElements[e].setAttribute("aria-hidden","true")},G=()=>{d=null!==d?d:o,k=-u*p.offsetWidth,a[d].slider.style.transform=`translate3d(${k}px, 0, 0)`,T=k},R=()=>{const{triggerElements:e}=a[d],t=e.length,r=u===t-1;t>1&&(0===u?(E.setAttribute("aria-disabled","true"),A.removeAttribute("aria-disabled")):r?(E.removeAttribute("aria-disabled"),A.setAttribute("aria-disabled","true")):(E.removeAttribute("aria-disabled"),A.removeAttribute("aria-disabled")))},K=()=>{w.textContent=`${u+1}/${a[d].triggerElements.length}`},J=()=>{L={startX:0,endX:0,startY:0,endY:0}},Q=()=>{const e=a[d].triggerElements.length,t=a[d].slider,r=a[d].sliderElements,i=t.classList.contains("parvus__slider--is-draggable");c.simulateTouch&&c.swipeClose&&!i||c.simulateTouch&&e>1&&!i?t.classList.add("parvus__slider--is-draggable"):t.classList.remove("parvus__slider--is-draggable"),e>1?(t.setAttribute("role","region"),t.setAttribute("aria-roledescription","carousel"),t.setAttribute("aria-label",c.l10n.sliderLabel),r.forEach(((t,r)=>{t.setAttribute("role","group"),t.setAttribute("aria-label",`${c.l10n.slideLabel} ${r+1}/${e}`)}))):(t.removeAttribute("role"),t.removeAttribute("aria-roledescription"),t.removeAttribute("aria-label"),r.forEach((e=>{e.removeAttribute("role"),e.removeAttribute("aria-label")}))),1===e?(w.setAttribute("aria-hidden","true"),E.setAttribute("aria-hidden","true"),A.setAttribute("aria-hidden","true")):(w.removeAttribute("aria-hidden"),E.removeAttribute("aria-hidden"),A.removeAttribute("aria-hidden"))},Z=()=>{I||(I=!0,n.requestAnimationFrame((()=>{a[d].sliderElements.forEach(((e,t)=>{ee(e,a[d].contentElements[t])})),G(),I=!1})))},ee=(e,t)=>{if("IMG"!==t.tagName)return;const r=getComputedStyle(e),i=e.querySelector(".parvus__caption"),n=i?i.getBoundingClientRect().height:0,s=t.getAttribute("height"),a=t.getAttribute("width");let l=e.offsetHeight,o=e.offsetWidth;l-=parseFloat(r.paddingTop)+parseFloat(r.paddingBottom)+parseFloat(n),o-=parseFloat(r.paddingLeft)+parseFloat(r.paddingRight);const d=Math.min(o/a||0,l/s),u=a*d||0,c=s*d||0;s>c&&su&&a{const{target:t}=e;t===E?U():t===A?V():(t===y||c.docClose&&!x&&!_&&t.classList.contains("parvus__slide"))&&Y(),e.stopPropagation()},ie=t=>{const r=(i=p,Array.from(i.querySelectorAll(e.join(", "))).filter((e=>null!==e.offsetParent)));var i;const n=r.indexOf(document.activeElement),s=r.length-1;switch(t.code){case"Tab":t.shiftKey?0===n&&(r[s].focus(),t.preventDefault()):n===s&&(r[0].focus(),t.preventDefault());break;case"Escape":Y(),t.preventDefault();break;case"ArrowLeft":U(),t.preventDefault();break;case"ArrowRight":V(),t.preventDefault()}},ne=e=>{if(e.preventDefault(),e.stopPropagation(),"mouse"===e.pointerType&&!c.simulateTouch)return;_=!1,x=!1,C=!0;const{pageX:t,pageY:r}=e;L.startX=t,L.startY=r;const{slider:i}=a[d];i.classList.add("parvus__slider--is-dragging"),i.style.willChange="transform",g=getComputedStyle(m).opacity},se=e=>{if(e.preventDefault(),C){const{pageX:t,pageY:r}=e;L.endX=t,L.endY=r,le()}},ae=e=>{e.stopPropagation(),C=!1;const{slider:t}=a[d];t.classList.remove("parvus__slider--is-dragging"),t.style.willChange="",(L.endX||L.endY)&&(()=>{const{startX:e,startY:t,endX:r,endY:i}=L,n=r-e,s=i-t,l=Math.abs(n),o=Math.abs(s),{triggerElements:g}=a[d],h=g.length;_?n>2&&l>=c.threshold&&u>0?U():n<2&&l>=c.threshold&&u!==h-1?V():G():x?(o>2&&c.swipeClose&&o>=c.threshold?Y():(p.classList.remove("parvus--is-vertical-closing"),G()),m.style.opacity=""):G()})(),J()},le=()=>{const{startX:e,endX:t,startY:r,endY:i}=L,n=e-t,s=i-r,l=Math.abs(s);Math.abs(n)>2&&!x&&a[d].triggerElements.length>1?(a[d].slider.style.transform=`translate3d(${T-Math.round(n)}px, 0, 0)`,_=!0,x=!1):Math.abs(s)>2&&!_&&c.swipeClose&&(!N&&l<=100&&(m.style.opacity=g-l/100),p.classList.add("parvus--is-vertical-closing"),a[d].slider.style.transform=`translate3d(${T}px, ${Math.round(s)}px, 0)`,_=!1,x=!0)},oe=()=>{n.addEventListener("keydown",ie),n.addEventListener("resize",Z),n.addEventListener("popstate",Y),M.addEventListener("change",S),p.addEventListener("click",re),p.addEventListener("pointerdown",ne),p.addEventListener("pointerup",ae),p.addEventListener("pointermove",se)},de=()=>{n.removeEventListener("keydown",ie),n.removeEventListener("resize",Z),n.removeEventListener("popstate",Y),M.removeEventListener("change",S),p.removeEventListener("click",re),p.removeEventListener("pointerdown",ne),p.removeEventListener("pointerup",ae),p.removeEventListener("pointermove",se)},ue=()=>p.hasAttribute("open"),ce=e=>{const t=new CustomEvent(e,{cancelable:!0});p.dispatchEvent(t)},pe=()=>{if(c=(e=>{const t={selector:".lightbox",gallerySelector:null,zoomIndicator:!0,captions:!0,captionsSelector:"self",captionsAttribute:"data-caption",docClose:!0,swipeClose:!0,simulateTouch:!0,threshold:50,hideScrollbar:!0,lightboxIndicatorIcon:'',previousButtonIcon:'',nextButtonIcon:'',closeButtonIcon:'',l10n:i},r={...t,...e};return e&&e.l10n&&(r.l10n={...t.l10n,...e.l10n}),r})(t),S(),null!==c.gallerySelector){document.querySelectorAll(c.gallerySelector).forEach(((e,t)=>{const r=t;e.querySelectorAll(c.selector).forEach((e=>{e.setAttribute("data-group",`parvus-gallery-${r}`),B(e)}))}))}document.querySelectorAll(`${c.selector}:not(.parvus-trigger)`).forEach(B)};return pe(),{init:pe,open:X,close:Y,select:j,previous:U,next:V,currentIndex:()=>u,add:B,remove:q,destroy:()=>{if(!p)return;ue()&&Y(),p.remove();document.querySelectorAll(".parvus-trigger").forEach(q),ce("destroy")},isOpen:ue,on:(e,t)=>{p&&p.addEventListener(e,t)},off:(e,t)=>{p&&p.removeEventListener(e,t)}}}export{n as default}; +const e=['a:not([inert]):not([tabindex^="-"])','button:not([inert]):not([tabindex^="-"]):not(:disabled)','[tabindex]:not([inert]):not([tabindex^="-"])'],t=window,r=()=>t.innerWidth-document.documentElement.clientWidth;var i={lightboxLabel:"This is a dialog window that overlays the main content of the page. The modal displays the enlarged image. Pressing the Escape key will close the modal and bring you back to where you were on the page.",lightboxLoadingIndicatorLabel:"Image loading",lightboxLoadingError:"The requested image cannot be loaded.",controlsLabel:"Controls",previousButtonLabel:"Previous image",nextButtonLabel:"Next image",closeButtonLabel:"Close dialog window",sliderLabel:"Images",slideLabel:"Image"};function n(t){const n=window,s={triggerElements:[],slider:null,sliderElements:[],contentElements:[]},a={},l=new Map;let o=0,d=null,u=null,c=0,p={},m=null,g=null,h=1,b=null,v=null,f=null,E=null,y=null,A=null,w=null,L=null,_={},x=!1,C=!1,I=!1,k=0,M=1,N=!1,T=1,S=1,$=null,B=null,q=!1,z=!0;const X=n.matchMedia("(prefers-reduced-motion)"),Y=()=>{z=!!X.matches},H=e=>{const t=e.dataset.group||`default-${o}`;return++o,e.hasAttribute("data-group")||e.setAttribute("data-group",t),t},O=e=>{if(m||P(),!("A"===e.tagName&&e.hasAttribute("href")||"BUTTON"===e.tagName&&e.hasAttribute("data-target")))throw new Error("Use a link with the 'href' attribute or a button with the 'data-target' attribute. Both attributes must contain a path to the image file.");if(d=H(e),a[d]||(a[d]=structuredClone(s)),a[d].triggerElements.includes(e))throw new Error("Ups, element already added.");if(a[d].triggerElements.push(e),p.zoomIndicator&&((e,t)=>{if(e.querySelector("img")&&null===e.querySelector(".parvus-zoom__indicator")){const r=document.createElement("div");r.className="parvus-zoom__indicator",r.innerHTML=t.lightboxIndicatorIcon,e.appendChild(r)}})(e,p),e.classList.add("parvus-trigger"),e.addEventListener("click",le),be()&&d===u){const t=a[d].triggerElements.indexOf(e);F(t),G(e,t,(()=>{R(t)})),ne(),te(),re()}},D=e=>{if(!e||!e.hasAttribute("data-group"))return;const t=H(e);if(!a[t]||!a[t].triggerElements.includes(e))return;const r=a[t].triggerElements.indexOf(e);a[t].triggerElements.splice(r,1),a[t].sliderElements.splice(r,1),p.zoomIndicator&&(e=>{if(e.querySelector("img")&&null!==e.querySelector(".parvus-zoom__indicator")){const t=e.querySelector(".parvus-zoom__indicator");e.removeChild(t)}})(e),be()&&t===u&&(ne(),te(),re()),e.removeEventListener("click",le),e.classList.remove("parvus-trigger")},P=()=>{m=document.createElement("dialog"),m.setAttribute("role","dialog"),m.setAttribute("aria-modal","true"),m.setAttribute("aria-label",p.l10n.lightboxLabel),m.classList.add("parvus"),g=document.createElement("div"),g.classList.add("parvus__overlay"),m.appendChild(g),b=document.createElement("div"),b.className="parvus__toolbar",v=document.createElement("div"),f=document.createElement("div"),E=document.createElement("div"),E.className="parvus__controls",E.setAttribute("role","group"),E.setAttribute("aria-label",p.l10n.controlsLabel),f.appendChild(E),w=document.createElement("button"),w.className="parvus__btn parvus__btn--close",w.setAttribute("type","button"),w.setAttribute("aria-label",p.l10n.closeButtonLabel),w.innerHTML=p.closeButtonIcon,E.appendChild(w),y=document.createElement("button"),y.className="parvus__btn parvus__btn--previous",y.setAttribute("type","button"),y.setAttribute("aria-label",p.l10n.previousButtonLabel),y.innerHTML=p.previousButtonIcon,E.appendChild(y),A=document.createElement("button"),A.className="parvus__btn parvus__btn--next",A.setAttribute("type","button"),A.setAttribute("aria-label",p.l10n.nextButtonLabel),A.innerHTML=p.nextButtonIcon,E.appendChild(A),L=document.createElement("div"),L.className="parvus__counter",v.appendChild(L),b.appendChild(v),b.appendChild(f),m.appendChild(b),document.body.appendChild(m)},F=e=>{if(void 0!==a[u].sliderElements[e])return;const t=document.createElement("div"),r=document.createElement("div"),i=a[u].triggerElements.length;if(t.className="parvus__slide",t.style.position="absolute",t.style.left=100*e+"%",t.setAttribute("aria-hidden","true"),t.appendChild(r),i>1&&(t.setAttribute("role","group"),t.setAttribute("aria-label",`${p.l10n.slideLabel} ${e+1}/${i}`)),a[u].sliderElements[e]=t,e>=c){const r=(e=>{const t=a[u].sliderElements,r=t.length;for(let i=e+1;i{const t=a[u].sliderElements;for(let r=e-1;r>=0;r--)if(void 0!==t[r])return r;return-1})(e);-1!==r?a[u].sliderElements[r].after(t):a[u].slider.prepend(t)}},j=e=>{if(m&&e&&e.classList.contains("parvus-trigger")&&!be()){if(u=H(e),!a[u].triggerElements.includes(e))throw new Error("Ups, I can't find the element.");c=a[u].triggerElements.indexOf(e),history.pushState({parvus:"close"},"Image",window.location.href),ge(),p.hideScrollbar&&(document.body.style.marginInlineEnd=`${r()}px`,document.body.style.overflow="hidden"),m.classList.add("parvus--is-opening"),m.showModal(),(()=>{const e=document.createElement("div");e.className="parvus__slider",a[u].slider=e,m.appendChild(e)})(),F(c),ee(),ne(),te(),re(),W(c),G(e,c,(()=>{R(c,!0),m.classList.remove("parvus--is-opening"),a[u].slider.classList.add("parvus__slider--animate")})),V(c+1),V(c-1),ve("open")}},U=()=>{if(!be())throw new Error("Ups, I'm already closed.");const e=a[u].contentElements[c],t=a[u].triggerElements[c];he(),ie(),"close"===history.state?.parvus&&history.back(),m.classList.add("parvus--is-closing");const r=()=>{Z(c),m.close(),m.classList.remove("parvus--is-closing"),m.classList.remove("parvus--is-vertical-closing"),a[u].slider.remove(),a[u].slider=null,a[u].sliderElements=[],a[u].contentElements=[],L.removeAttribute("aria-hidden"),y.removeAttribute("aria-hidden"),y.removeAttribute("aria-disabled"),A.removeAttribute("aria-hidden"),A.removeAttribute("aria-disabled"),p.hideScrollbar&&(document.body.style.marginInlineEnd="",document.body.style.overflow="")};if(e&&"IMG"===e.tagName)if(document.startViewTransition){e.style.viewTransitionName="lightboximage";document.startViewTransition((()=>{e.style.opacity="0",e.style.viewTransitionName=null,t.style.viewTransitionName="lightboximage"})).finished.finally((()=>{r(),t.style.viewTransitionName=null}))}else e.style.opacity="0",r();else r()},V=e=>{e<0||e>=a[u].triggerElements.length||void 0!==a[u].sliderElements[e]||(F(e),G(a[u].triggerElements[e],e,(()=>{R(e)})))},W=e=>{a[u].sliderElements[e].setAttribute("aria-hidden","false")},G=(e,t,r)=>{const{contentElements:i,sliderElements:n}=a[u];if(void 0!==i[t])return void(r&&"function"==typeof r&&r());const s=n[t].querySelector("div"),l=new Image,o=document.createElement("div"),d=e.querySelector("img"),c=document.createElement("div");o.className="parvus__content",c.className="parvus__loader",c.setAttribute("role","progressbar"),c.setAttribute("aria-label",p.l10n.lightboxLoadingIndicatorLabel),s.appendChild(c);new Promise(((e,t)=>{l.onload=()=>e(l),l.onerror=e=>t(e)})).then((r=>{r.style.opacity=0,o.appendChild(r),s.appendChild(o),p.captions&&((e,t,r,i)=>{const n=document.createElement("div");let s=null;if(n.className="parvus__caption","self"===p.captionsSelector)r.hasAttribute(p.captionsAttribute)&&""!==r.getAttribute(p.captionsAttribute)&&(s=r.getAttribute(p.captionsAttribute));else{const e=r.querySelector(p.captionsSelector);null!==e&&(s=e.hasAttribute(p.captionsAttribute)&&""!==e.getAttribute(p.captionsAttribute)?e.getAttribute(p.captionsAttribute):e.innerHTML)}if(null!==s){const r=`parvus__caption-${i}`;n.id=r,n.innerHTML=`

${s}

`,e.appendChild(n),t.setAttribute("aria-describedby",r)}})(s,l,e,t),i[t]=r,r.setAttribute("width",r.naturalWidth),r.setAttribute("height",r.naturalHeight),ae(n[t],r)})).catch((()=>{const e=document.createElement("div");e.classList.add("parvus__content"),e.classList.add("parvus__content--error"),e.innerHTML=`${p.l10n.lightboxLoadingError}`,s.appendChild(e),i[t]=e})).finally((()=>{s.removeChild(c),r&&"function"==typeof r&&r()})),e.hasAttribute("data-sizes")&&""!==e.getAttribute("data-sizes")&&l.setAttribute("sizes",e.getAttribute("data-sizes")),e.hasAttribute("data-srcset")&&""!==e.getAttribute("data-srcset")&&l.setAttribute("srcset",e.getAttribute("data-srcset")),"A"===e.tagName?l.setAttribute("src",e.href):l.setAttribute("src",e.getAttribute("data-target")),d&&d.hasAttribute("alt")&&""!==d.getAttribute("alt")?l.alt=d.alt:e.hasAttribute("data-alt")&&""!==e.getAttribute("data-alt")?l.alt=e.getAttribute("data-alt"):l.alt=""},R=(e,t)=>{const r=a[u].contentElements[e];if(r&&"IMG"===r.tagName){const i=a[u].triggerElements[e];if(t&&document.startViewTransition){i.style.viewTransitionName="lightboximage";document.startViewTransition((()=>{r.style.opacity="",i.style.viewTransitionName=null,r.style.viewTransitionName="lightboximage"})).finished.finally((()=>{r.style.viewTransitionName=null}))}else r.style.opacity=""}else r.style.opacity=""},K=e=>{const t=c;if(!be())throw new Error("Oops, I'm closed.");{if("number"!=typeof e||isNaN(e))throw new Error("Oops, no slide specified.");const t=a[u].triggerElements;if(e===c)throw new Error(`Oops, slide ${e} is already selected.`);if(e<-1||e>=t.length)throw new Error(`Oops, I can't find slide ${e}.`)}void 0!==a[u].sliderElements[e]||(F(e),G(a[u].triggerElements[e],e,(()=>{R(e)}))),W(e),c=e,ee(),et&&(te(),V(e+1)),Z(t),re(),ve("select")},J=()=>{c>0&&K(c-1)},Q=()=>{const{triggerElements:e}=a[u];c{void 0!==a[u].sliderElements[e]&&a[u].sliderElements[e].setAttribute("aria-hidden","true")},ee=()=>{u=null!==u?u:d,$=-c*m.offsetWidth,a[u].slider.style.transform=`translate3d(${$}px, 0, 0)`,B=$},te=()=>{const{triggerElements:e}=a[u],t=e.length,r=c===t-1;t>1&&(0===c?(y.setAttribute("aria-disabled","true"),A.removeAttribute("aria-disabled")):r?(y.removeAttribute("aria-disabled"),A.setAttribute("aria-disabled","true")):(y.removeAttribute("aria-disabled"),A.removeAttribute("aria-disabled")))},re=()=>{L.textContent=`${c+1}/${a[u].triggerElements.length}`},ie=()=>{_={startX:0,endX:0,startY:0,endY:0}},ne=()=>{const e=a[u].triggerElements.length,t=a[u].slider,r=a[u].sliderElements,i=t.classList.contains("parvus__slider--is-draggable");p.simulateTouch&&p.swipeClose&&!i||p.simulateTouch&&e>1&&!i?t.classList.add("parvus__slider--is-draggable"):t.classList.remove("parvus__slider--is-draggable"),e>1?(t.setAttribute("role","region"),t.setAttribute("aria-roledescription","carousel"),t.setAttribute("aria-label",p.l10n.sliderLabel),r.forEach(((t,r)=>{t.setAttribute("role","group"),t.setAttribute("aria-label",`${p.l10n.slideLabel} ${r+1}/${e}`)}))):(t.removeAttribute("role"),t.removeAttribute("aria-roledescription"),t.removeAttribute("aria-label"),r.forEach((e=>{e.removeAttribute("role"),e.removeAttribute("aria-label")}))),1===e?(L.setAttribute("aria-hidden","true"),y.setAttribute("aria-hidden","true"),A.setAttribute("aria-hidden","true")):(L.removeAttribute("aria-hidden"),y.removeAttribute("aria-hidden"),A.removeAttribute("aria-hidden"))},se=()=>{q||(q=!0,n.requestAnimationFrame((()=>{a[u].sliderElements.forEach(((e,t)=>{ae(e,a[u].contentElements[t])})),ee(),q=!1})))},ae=(e,t)=>{if("IMG"!==t.tagName)return;const r=getComputedStyle(e),i=e.querySelector(".parvus__caption"),n=i?i.getBoundingClientRect().height:0,s=t.getAttribute("height"),a=t.getAttribute("width");let l=e.offsetHeight,o=e.offsetWidth;l-=parseFloat(r.paddingTop)+parseFloat(r.paddingBottom)+parseFloat(n),o-=parseFloat(r.paddingLeft)+parseFloat(r.paddingRight);const d=Math.min(o/a||0,l/s),u=a*d||0,c=s*d||0;s>c&&su&&a{const{target:t}=e;t===y?J():t===A?Q():(t===w||p.docClose&&!C&&!x&&t.classList.contains("parvus__slide"))&&U(),e.stopPropagation()},de=t=>{const r=(i=m,Array.from(i.querySelectorAll(e.join(", "))).filter((e=>null!==e.offsetParent)));var i;const n=r.indexOf(document.activeElement),s=r.length-1;switch(t.code){case"Tab":t.shiftKey?0===n&&(r[s].focus(),t.preventDefault()):n===s&&(r[0].focus(),t.preventDefault());break;case"Escape":U(),t.preventDefault();break;case"ArrowLeft":J(),t.preventDefault();break;case"ArrowRight":Q(),t.preventDefault()}},ue=e=>{e.preventDefault(),e.stopPropagation(),x=!1,C=!1,I=!0;const{pageX:t,pageY:r}=e;l.set(e.pointerId,e),_.startX=t,_.startY=r;const{slider:i}=a[u];i.classList.add("parvus__slider--is-dragging"),i.style.willChange="transform",h=getComputedStyle(g).opacity},ce=e=>{if(e.preventDefault(),!I)return;const t=a[u].sliderElements[c];if(l.set(e.pointerId,e),2===l.size){const e=Array.from(l.values()),r=Math.hypot(e[1].clientX-e[0].clientX,e[1].clientY-e[0].clientY);return N||(k=r,N=!0,S=T),M=S*(r/k),M=Math.min(Math.max(1,M),3),t.style.transform=`scale(${M})`,void(T=M)}if(M>1)return;const{pageX:r,pageY:i}=e;_.endX=r,_.endY=i,me()},pe=e=>{e.stopPropagation(),I=!1,N=!1;const{slider:t}=a[u];t.classList.remove("parvus__slider--is-dragging"),t.style.willChange="",(_.endX||_.endY)&&(()=>{const{startX:e,startY:t,endX:r,endY:i}=_,n=r-e,s=i-t,l=Math.abs(n),o=Math.abs(s),{triggerElements:d}=a[u],h=d.length;x?n>2&&l>=p.threshold&&c>0?J():n<2&&l>=p.threshold&&c!==h-1?Q():ee():C?(o>2&&p.swipeClose&&o>=p.threshold?U():(m.classList.remove("parvus--is-vertical-closing"),ee()),g.style.opacity=""):ee()})(),ie(),l.delete(e.pointerId),M>1?S=T:k=0},me=()=>{const{startX:e,endX:t,startY:r,endY:i}=_,n=e-t,s=i-r,l=Math.abs(s);Math.abs(n)>2&&!C&&a[u].triggerElements.length>1?(a[u].slider.style.transform=`translate3d(${B-Math.round(n)}px, 0, 0)`,x=!0,C=!1):Math.abs(s)>2&&!x&&p.swipeClose&&(!z&&l<=100&&(g.style.opacity=h-l/100),m.classList.add("parvus--is-vertical-closing"),a[u].slider.style.transform=`translate3d(${B}px, ${Math.round(s)}px, 0)`,x=!1,C=!0)},ge=()=>{n.addEventListener("keydown",de),n.addEventListener("resize",se),n.addEventListener("popstate",U),X.addEventListener("change",Y),m.addEventListener("click",oe),m.addEventListener("pointerdown",ue),m.addEventListener("pointerup",pe),m.addEventListener("pointermove",ce)},he=()=>{n.removeEventListener("keydown",de),n.removeEventListener("resize",se),n.removeEventListener("popstate",U),X.removeEventListener("change",Y),m.removeEventListener("click",oe),m.removeEventListener("pointerdown",ue),m.removeEventListener("pointerup",pe),m.removeEventListener("pointermove",ce)},be=()=>m.hasAttribute("open"),ve=e=>{const t=new CustomEvent(e,{cancelable:!0});m.dispatchEvent(t)},fe=()=>{if(p=(e=>{const t={selector:".lightbox",gallerySelector:null,zoomIndicator:!0,captions:!0,captionsSelector:"self",captionsAttribute:"data-caption",docClose:!0,swipeClose:!0,simulateTouch:!0,threshold:50,hideScrollbar:!0,lightboxIndicatorIcon:'',previousButtonIcon:'',nextButtonIcon:'',closeButtonIcon:'',l10n:i},r={...t,...e};return e&&e.l10n&&(r.l10n={...t.l10n,...e.l10n}),r})(t),Y(),null!==p.gallerySelector){document.querySelectorAll(p.gallerySelector).forEach(((e,t)=>{const r=t;e.querySelectorAll(p.selector).forEach((e=>{e.setAttribute("data-group",`parvus-gallery-${r}`),O(e)}))}))}document.querySelectorAll(`${p.selector}:not(.parvus-trigger)`).forEach(O)};return fe(),{init:fe,open:j,close:U,select:K,previous:J,next:Q,currentIndex:()=>c,add:O,remove:D,destroy:()=>{if(!m)return;be()&&U(),m.remove();document.querySelectorAll(".parvus-trigger").forEach(D),ve("destroy")},isOpen:be,on:(e,t)=>{m&&m.addEventListener(e,t)},off:(e,t)=>{m&&m.removeEventListener(e,t)}}}export{n as default}; diff --git a/dist/js/parvus.js b/dist/js/parvus.js index a84726c..9fa525c 100644 --- a/dist/js/parvus.js +++ b/dist/js/parvus.js @@ -75,11 +75,13 @@ slideLabel: 'Image' }; + /** + * Parvus Lightbox + * + * @param {Object} userOptions - User configuration options + * @returns {Object} Parvus instance + */ function Parvus(userOptions) { - /** - * Global variables - * - */ const BROWSER_WINDOW = window; const GROUP_ATTRIBUTES = { triggerElements: [], @@ -88,6 +90,7 @@ contentElements: [] }; const GROUPS = {}; + const activePointers = new Map(); let groupIdCounter = 0; let newGroup = null; let activeGroup = null; @@ -108,6 +111,11 @@ let isDraggingX = false; let isDraggingY = false; let pointerDown = false; + let pinchStartDistance = 0; + let currentScale = 1; + let isPinching = false; + let lastScale = 1; + let baseScale = 1; let offset = null; let offsetTmp = null; let resizeTicking = false; @@ -1065,9 +1073,6 @@ const pointerdownHandler = event => { event.preventDefault(); event.stopPropagation(); - if (event.pointerType === 'mouse' && !config.simulateTouch) { - return; - } isDraggingX = false; isDraggingY = false; pointerDown = true; @@ -1075,6 +1080,7 @@ pageX, pageY } = event; + activePointers.set(event.pointerId, event); drag.startX = pageX; drag.startY = pageY; const { @@ -1095,15 +1101,39 @@ */ const pointermoveHandler = event => { event.preventDefault(); - if (pointerDown) { - const { - pageX, - pageY - } = event; - drag.endX = pageX; - drag.endY = pageY; - doSwipe(); + if (!pointerDown) { + return; + } + const currentImg = GROUPS[activeGroup].sliderElements[currentIndex]; + + // Update pointer position + activePointers.set(event.pointerId, event); + + // Zoom + if (activePointers.size === 2) { + const points = Array.from(activePointers.values()); + const distance = Math.hypot(points[1].clientX - points[0].clientX, points[1].clientY - points[0].clientY); + if (!isPinching) { + pinchStartDistance = distance; + isPinching = true; + baseScale = lastScale; + } + currentScale = baseScale * (distance / pinchStartDistance); + currentScale = Math.min(Math.max(1, currentScale), 3); + currentImg.style.transform = `scale(${currentScale})`; + lastScale = currentScale; + return; } + if (currentScale > 1) { + return; + } + const { + pageX, + pageY + } = event; + drag.endX = pageX; + drag.endY = pageY; + doSwipe(); }; /** @@ -1117,6 +1147,7 @@ const pointerupHandler = event => { event.stopPropagation(); pointerDown = false; + isPinching = false; const { slider } = GROUPS[activeGroup]; @@ -1126,6 +1157,12 @@ updateAfterDrag(); } clearDrag(); + activePointers.delete(event.pointerId); + if (currentScale > 1) { + baseScale = lastScale; + } else { + pinchStartDistance = 0; + } }; /** diff --git a/dist/js/parvus.min.js b/dist/js/parvus.min.js index ca1f92e..69cf1ba 100644 --- a/dist/js/parvus.min.js +++ b/dist/js/parvus.min.js @@ -8,4 +8,4 @@ * MIT license */ -!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).Parvus=t()}(this,(function(){"use strict";const e=['a:not([inert]):not([tabindex^="-"])','button:not([inert]):not([tabindex^="-"]):not(:disabled)','[tabindex]:not([inert]):not([tabindex^="-"])'],t=window,r=()=>t.innerWidth-document.documentElement.clientWidth;var i={lightboxLabel:"This is a dialog window that overlays the main content of the page. The modal displays the enlarged image. Pressing the Escape key will close the modal and bring you back to where you were on the page.",lightboxLoadingIndicatorLabel:"Image loading",lightboxLoadingError:"The requested image cannot be loaded.",controlsLabel:"Controls",previousButtonLabel:"Previous image",nextButtonLabel:"Next image",closeButtonLabel:"Close dialog window",sliderLabel:"Images",slideLabel:"Image"};return function(t){const n=window,s={triggerElements:[],slider:null,sliderElements:[],contentElements:[]},a={};let l=0,o=null,d=null,u=0,c={},p=null,m=null,g=1,h=null,b=null,v=null,f=null,E=null,y=null,A=null,w=null,L={},_=!1,x=!1,C=!1,T=null,k=null,I=!1,N=!0;const M=n.matchMedia("(prefers-reduced-motion)"),S=()=>{N=!!M.matches},$=e=>{const t=e.dataset.group||`default-${l}`;return++l,e.hasAttribute("data-group")||e.setAttribute("data-group",t),t},B=e=>{if(p||z(),!("A"===e.tagName&&e.hasAttribute("href")||"BUTTON"===e.tagName&&e.hasAttribute("data-target")))throw new Error("Use a link with the 'href' attribute or a button with the 'data-target' attribute. Both attributes must contain a path to the image file.");if(o=$(e),a[o]||(a[o]=structuredClone(s)),a[o].triggerElements.includes(e))throw new Error("Ups, element already added.");if(a[o].triggerElements.push(e),c.zoomIndicator&&((e,t)=>{if(e.querySelector("img")&&null===e.querySelector(".parvus-zoom__indicator")){const r=document.createElement("div");r.className="parvus-zoom__indicator",r.innerHTML=t.lightboxIndicatorIcon,e.appendChild(r)}})(e,c),e.classList.add("parvus-trigger"),e.addEventListener("click",te),ue()&&o===d){const t=a[o].triggerElements.indexOf(e);H(t),P(e,t,(()=>{j(t)})),Q(),R(),K()}},q=e=>{if(!e||!e.hasAttribute("data-group"))return;const t=$(e);if(!a[t]||!a[t].triggerElements.includes(e))return;const r=a[t].triggerElements.indexOf(e);a[t].triggerElements.splice(r,1),a[t].sliderElements.splice(r,1),c.zoomIndicator&&(e=>{if(e.querySelector("img")&&null!==e.querySelector(".parvus-zoom__indicator")){const t=e.querySelector(".parvus-zoom__indicator");e.removeChild(t)}})(e),ue()&&t===d&&(Q(),R(),K()),e.removeEventListener("click",te),e.classList.remove("parvus-trigger")},z=()=>{p=document.createElement("dialog"),p.setAttribute("role","dialog"),p.setAttribute("aria-modal","true"),p.setAttribute("aria-label",c.l10n.lightboxLabel),p.classList.add("parvus"),m=document.createElement("div"),m.classList.add("parvus__overlay"),p.appendChild(m),h=document.createElement("div"),h.className="parvus__toolbar",b=document.createElement("div"),v=document.createElement("div"),f=document.createElement("div"),f.className="parvus__controls",f.setAttribute("role","group"),f.setAttribute("aria-label",c.l10n.controlsLabel),v.appendChild(f),A=document.createElement("button"),A.className="parvus__btn parvus__btn--close",A.setAttribute("type","button"),A.setAttribute("aria-label",c.l10n.closeButtonLabel),A.innerHTML=c.closeButtonIcon,f.appendChild(A),E=document.createElement("button"),E.className="parvus__btn parvus__btn--previous",E.setAttribute("type","button"),E.setAttribute("aria-label",c.l10n.previousButtonLabel),E.innerHTML=c.previousButtonIcon,f.appendChild(E),y=document.createElement("button"),y.className="parvus__btn parvus__btn--next",y.setAttribute("type","button"),y.setAttribute("aria-label",c.l10n.nextButtonLabel),y.innerHTML=c.nextButtonIcon,f.appendChild(y),w=document.createElement("div"),w.className="parvus__counter",b.appendChild(w),h.appendChild(b),h.appendChild(v),p.appendChild(h),document.body.appendChild(p)},H=e=>{if(void 0!==a[d].sliderElements[e])return;const t=document.createElement("div"),r=document.createElement("div"),i=a[d].triggerElements.length;if(t.className="parvus__slide",t.style.position="absolute",t.style.left=100*e+"%",t.setAttribute("aria-hidden","true"),t.appendChild(r),i>1&&(t.setAttribute("role","group"),t.setAttribute("aria-label",`${c.l10n.slideLabel} ${e+1}/${i}`)),a[d].sliderElements[e]=t,e>=u){const r=(e=>{const t=a[d].sliderElements,r=t.length;for(let i=e+1;i{const t=a[d].sliderElements;for(let r=e-1;r>=0;r--)if(void 0!==t[r])return r;return-1})(e);-1!==r?a[d].sliderElements[r].after(t):a[d].slider.prepend(t)}},X=e=>{if(p&&e&&e.classList.contains("parvus-trigger")&&!ue()){if(d=$(e),!a[d].triggerElements.includes(e))throw new Error("Ups, I can't find the element.");u=a[d].triggerElements.indexOf(e),history.pushState({parvus:"close"},"Image",window.location.href),oe(),c.hideScrollbar&&(document.body.style.marginInlineEnd=`${r()}px`,document.body.style.overflow="hidden"),p.classList.add("parvus--is-opening"),p.showModal(),(()=>{const e=document.createElement("div");e.className="parvus__slider",a[d].slider=e,p.appendChild(e)})(),H(u),G(),Q(),R(),K(),D(u),P(e,u,(()=>{j(u,!0),p.classList.remove("parvus--is-opening"),a[d].slider.classList.add("parvus__slider--animate")})),O(u+1),O(u-1),ce("open")}},Y=()=>{if(!ue())throw new Error("Ups, I'm already closed.");const e=a[d].contentElements[u],t=a[d].triggerElements[u];de(),J(),"close"===history.state?.parvus&&history.back(),p.classList.add("parvus--is-closing");const r=()=>{W(u),p.close(),p.classList.remove("parvus--is-closing"),p.classList.remove("parvus--is-vertical-closing"),a[d].slider.remove(),a[d].slider=null,a[d].sliderElements=[],a[d].contentElements=[],w.removeAttribute("aria-hidden"),E.removeAttribute("aria-hidden"),E.removeAttribute("aria-disabled"),y.removeAttribute("aria-hidden"),y.removeAttribute("aria-disabled"),c.hideScrollbar&&(document.body.style.marginInlineEnd="",document.body.style.overflow="")};if(e&&"IMG"===e.tagName)if(document.startViewTransition){e.style.viewTransitionName="lightboximage";document.startViewTransition((()=>{e.style.opacity="0",e.style.viewTransitionName=null,t.style.viewTransitionName="lightboximage"})).finished.finally((()=>{r(),t.style.viewTransitionName=null}))}else e.style.opacity="0",r();else r()},O=e=>{e<0||e>=a[d].triggerElements.length||void 0!==a[d].sliderElements[e]||(H(e),P(a[d].triggerElements[e],e,(()=>{j(e)})))},D=e=>{a[d].sliderElements[e].setAttribute("aria-hidden","false")},P=(e,t,r)=>{const{contentElements:i,sliderElements:n}=a[d];if(void 0!==i[t])return void(r&&"function"==typeof r&&r());const s=n[t].querySelector("div"),l=new Image,o=document.createElement("div"),u=e.querySelector("img"),p=document.createElement("div");o.className="parvus__content",p.className="parvus__loader",p.setAttribute("role","progressbar"),p.setAttribute("aria-label",c.l10n.lightboxLoadingIndicatorLabel),s.appendChild(p);new Promise(((e,t)=>{l.onload=()=>e(l),l.onerror=e=>t(e)})).then((r=>{r.style.opacity=0,o.appendChild(r),s.appendChild(o),c.captions&&((e,t,r,i)=>{const n=document.createElement("div");let s=null;if(n.className="parvus__caption","self"===c.captionsSelector)r.hasAttribute(c.captionsAttribute)&&""!==r.getAttribute(c.captionsAttribute)&&(s=r.getAttribute(c.captionsAttribute));else{const e=r.querySelector(c.captionsSelector);null!==e&&(s=e.hasAttribute(c.captionsAttribute)&&""!==e.getAttribute(c.captionsAttribute)?e.getAttribute(c.captionsAttribute):e.innerHTML)}if(null!==s){const r=`parvus__caption-${i}`;n.id=r,n.innerHTML=`

${s}

`,e.appendChild(n),t.setAttribute("aria-describedby",r)}})(s,l,e,t),i[t]=r,r.setAttribute("width",r.naturalWidth),r.setAttribute("height",r.naturalHeight),ee(n[t],r)})).catch((()=>{const e=document.createElement("div");e.classList.add("parvus__content"),e.classList.add("parvus__content--error"),e.innerHTML=`${c.l10n.lightboxLoadingError}`,s.appendChild(e),i[t]=e})).finally((()=>{s.removeChild(p),r&&"function"==typeof r&&r()})),e.hasAttribute("data-sizes")&&""!==e.getAttribute("data-sizes")&&l.setAttribute("sizes",e.getAttribute("data-sizes")),e.hasAttribute("data-srcset")&&""!==e.getAttribute("data-srcset")&&l.setAttribute("srcset",e.getAttribute("data-srcset")),"A"===e.tagName?l.setAttribute("src",e.href):l.setAttribute("src",e.getAttribute("data-target")),u&&u.hasAttribute("alt")&&""!==u.getAttribute("alt")?l.alt=u.alt:e.hasAttribute("data-alt")&&""!==e.getAttribute("data-alt")?l.alt=e.getAttribute("data-alt"):l.alt=""},j=(e,t)=>{const r=a[d].contentElements[e];if(r&&"IMG"===r.tagName){const i=a[d].triggerElements[e];if(t&&document.startViewTransition){i.style.viewTransitionName="lightboximage";document.startViewTransition((()=>{r.style.opacity="",i.style.viewTransitionName=null,r.style.viewTransitionName="lightboximage"})).finished.finally((()=>{r.style.viewTransitionName=null}))}else r.style.opacity=""}else r.style.opacity=""},F=e=>{const t=u;if(!ue())throw new Error("Oops, I'm closed.");{if("number"!=typeof e||isNaN(e))throw new Error("Oops, no slide specified.");const t=a[d].triggerElements;if(e===u)throw new Error(`Oops, slide ${e} is already selected.`);if(e<-1||e>=t.length)throw new Error(`Oops, I can't find slide ${e}.`)}void 0!==a[d].sliderElements[e]||(H(e),P(a[d].triggerElements[e],e,(()=>{j(e)}))),D(e),u=e,G(),et&&(R(),O(e+1)),W(t),K(),ce("select")},U=()=>{u>0&&F(u-1)},V=()=>{const{triggerElements:e}=a[d];u{void 0!==a[d].sliderElements[e]&&a[d].sliderElements[e].setAttribute("aria-hidden","true")},G=()=>{d=null!==d?d:o,T=-u*p.offsetWidth,a[d].slider.style.transform=`translate3d(${T}px, 0, 0)`,k=T},R=()=>{const{triggerElements:e}=a[d],t=e.length,r=u===t-1;t>1&&(0===u?(E.setAttribute("aria-disabled","true"),y.removeAttribute("aria-disabled")):r?(E.removeAttribute("aria-disabled"),y.setAttribute("aria-disabled","true")):(E.removeAttribute("aria-disabled"),y.removeAttribute("aria-disabled")))},K=()=>{w.textContent=`${u+1}/${a[d].triggerElements.length}`},J=()=>{L={startX:0,endX:0,startY:0,endY:0}},Q=()=>{const e=a[d].triggerElements.length,t=a[d].slider,r=a[d].sliderElements,i=t.classList.contains("parvus__slider--is-draggable");c.simulateTouch&&c.swipeClose&&!i||c.simulateTouch&&e>1&&!i?t.classList.add("parvus__slider--is-draggable"):t.classList.remove("parvus__slider--is-draggable"),e>1?(t.setAttribute("role","region"),t.setAttribute("aria-roledescription","carousel"),t.setAttribute("aria-label",c.l10n.sliderLabel),r.forEach(((t,r)=>{t.setAttribute("role","group"),t.setAttribute("aria-label",`${c.l10n.slideLabel} ${r+1}/${e}`)}))):(t.removeAttribute("role"),t.removeAttribute("aria-roledescription"),t.removeAttribute("aria-label"),r.forEach((e=>{e.removeAttribute("role"),e.removeAttribute("aria-label")}))),1===e?(w.setAttribute("aria-hidden","true"),E.setAttribute("aria-hidden","true"),y.setAttribute("aria-hidden","true")):(w.removeAttribute("aria-hidden"),E.removeAttribute("aria-hidden"),y.removeAttribute("aria-hidden"))},Z=()=>{I||(I=!0,n.requestAnimationFrame((()=>{a[d].sliderElements.forEach(((e,t)=>{ee(e,a[d].contentElements[t])})),G(),I=!1})))},ee=(e,t)=>{if("IMG"!==t.tagName)return;const r=getComputedStyle(e),i=e.querySelector(".parvus__caption"),n=i?i.getBoundingClientRect().height:0,s=t.getAttribute("height"),a=t.getAttribute("width");let l=e.offsetHeight,o=e.offsetWidth;l-=parseFloat(r.paddingTop)+parseFloat(r.paddingBottom)+parseFloat(n),o-=parseFloat(r.paddingLeft)+parseFloat(r.paddingRight);const d=Math.min(o/a||0,l/s),u=a*d||0,c=s*d||0;s>c&&su&&a{const{target:t}=e;t===E?U():t===y?V():(t===A||c.docClose&&!x&&!_&&t.classList.contains("parvus__slide"))&&Y(),e.stopPropagation()},ie=t=>{const r=(i=p,Array.from(i.querySelectorAll(e.join(", "))).filter((e=>null!==e.offsetParent)));var i;const n=r.indexOf(document.activeElement),s=r.length-1;switch(t.code){case"Tab":t.shiftKey?0===n&&(r[s].focus(),t.preventDefault()):n===s&&(r[0].focus(),t.preventDefault());break;case"Escape":Y(),t.preventDefault();break;case"ArrowLeft":U(),t.preventDefault();break;case"ArrowRight":V(),t.preventDefault()}},ne=e=>{if(e.preventDefault(),e.stopPropagation(),"mouse"===e.pointerType&&!c.simulateTouch)return;_=!1,x=!1,C=!0;const{pageX:t,pageY:r}=e;L.startX=t,L.startY=r;const{slider:i}=a[d];i.classList.add("parvus__slider--is-dragging"),i.style.willChange="transform",g=getComputedStyle(m).opacity},se=e=>{if(e.preventDefault(),C){const{pageX:t,pageY:r}=e;L.endX=t,L.endY=r,le()}},ae=e=>{e.stopPropagation(),C=!1;const{slider:t}=a[d];t.classList.remove("parvus__slider--is-dragging"),t.style.willChange="",(L.endX||L.endY)&&(()=>{const{startX:e,startY:t,endX:r,endY:i}=L,n=r-e,s=i-t,l=Math.abs(n),o=Math.abs(s),{triggerElements:g}=a[d],h=g.length;_?n>2&&l>=c.threshold&&u>0?U():n<2&&l>=c.threshold&&u!==h-1?V():G():x?(o>2&&c.swipeClose&&o>=c.threshold?Y():(p.classList.remove("parvus--is-vertical-closing"),G()),m.style.opacity=""):G()})(),J()},le=()=>{const{startX:e,endX:t,startY:r,endY:i}=L,n=e-t,s=i-r,l=Math.abs(s);Math.abs(n)>2&&!x&&a[d].triggerElements.length>1?(a[d].slider.style.transform=`translate3d(${k-Math.round(n)}px, 0, 0)`,_=!0,x=!1):Math.abs(s)>2&&!_&&c.swipeClose&&(!N&&l<=100&&(m.style.opacity=g-l/100),p.classList.add("parvus--is-vertical-closing"),a[d].slider.style.transform=`translate3d(${k}px, ${Math.round(s)}px, 0)`,_=!1,x=!0)},oe=()=>{n.addEventListener("keydown",ie),n.addEventListener("resize",Z),n.addEventListener("popstate",Y),M.addEventListener("change",S),p.addEventListener("click",re),p.addEventListener("pointerdown",ne),p.addEventListener("pointerup",ae),p.addEventListener("pointermove",se)},de=()=>{n.removeEventListener("keydown",ie),n.removeEventListener("resize",Z),n.removeEventListener("popstate",Y),M.removeEventListener("change",S),p.removeEventListener("click",re),p.removeEventListener("pointerdown",ne),p.removeEventListener("pointerup",ae),p.removeEventListener("pointermove",se)},ue=()=>p.hasAttribute("open"),ce=e=>{const t=new CustomEvent(e,{cancelable:!0});p.dispatchEvent(t)},pe=()=>{if(c=(e=>{const t={selector:".lightbox",gallerySelector:null,zoomIndicator:!0,captions:!0,captionsSelector:"self",captionsAttribute:"data-caption",docClose:!0,swipeClose:!0,simulateTouch:!0,threshold:50,hideScrollbar:!0,lightboxIndicatorIcon:'',previousButtonIcon:'',nextButtonIcon:'',closeButtonIcon:'',l10n:i},r={...t,...e};return e&&e.l10n&&(r.l10n={...t.l10n,...e.l10n}),r})(t),S(),null!==c.gallerySelector){document.querySelectorAll(c.gallerySelector).forEach(((e,t)=>{const r=t;e.querySelectorAll(c.selector).forEach((e=>{e.setAttribute("data-group",`parvus-gallery-${r}`),B(e)}))}))}document.querySelectorAll(`${c.selector}:not(.parvus-trigger)`).forEach(B)};return pe(),{init:pe,open:X,close:Y,select:F,previous:U,next:V,currentIndex:()=>u,add:B,remove:q,destroy:()=>{if(!p)return;ue()&&Y(),p.remove();document.querySelectorAll(".parvus-trigger").forEach(q),ce("destroy")},isOpen:ue,on:(e,t)=>{p&&p.addEventListener(e,t)},off:(e,t)=>{p&&p.removeEventListener(e,t)}}}})); +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).Parvus=t()}(this,(function(){"use strict";const e=['a:not([inert]):not([tabindex^="-"])','button:not([inert]):not([tabindex^="-"]):not(:disabled)','[tabindex]:not([inert]):not([tabindex^="-"])'],t=window,r=()=>t.innerWidth-document.documentElement.clientWidth;var i={lightboxLabel:"This is a dialog window that overlays the main content of the page. The modal displays the enlarged image. Pressing the Escape key will close the modal and bring you back to where you were on the page.",lightboxLoadingIndicatorLabel:"Image loading",lightboxLoadingError:"The requested image cannot be loaded.",controlsLabel:"Controls",previousButtonLabel:"Previous image",nextButtonLabel:"Next image",closeButtonLabel:"Close dialog window",sliderLabel:"Images",slideLabel:"Image"};return function(t){const n=window,s={triggerElements:[],slider:null,sliderElements:[],contentElements:[]},a={},l=new Map;let o=0,d=null,u=null,c=0,p={},m=null,g=null,h=1,b=null,v=null,f=null,E=null,y=null,A=null,w=null,L=null,_={},x=!1,C=!1,I=!1,k=0,T=1,M=!1,N=1,S=1,$=null,B=null,q=!1,z=!0;const X=n.matchMedia("(prefers-reduced-motion)"),Y=()=>{z=!!X.matches},H=e=>{const t=e.dataset.group||`default-${o}`;return++o,e.hasAttribute("data-group")||e.setAttribute("data-group",t),t},O=e=>{if(m||P(),!("A"===e.tagName&&e.hasAttribute("href")||"BUTTON"===e.tagName&&e.hasAttribute("data-target")))throw new Error("Use a link with the 'href' attribute or a button with the 'data-target' attribute. Both attributes must contain a path to the image file.");if(d=H(e),a[d]||(a[d]=structuredClone(s)),a[d].triggerElements.includes(e))throw new Error("Ups, element already added.");if(a[d].triggerElements.push(e),p.zoomIndicator&&((e,t)=>{if(e.querySelector("img")&&null===e.querySelector(".parvus-zoom__indicator")){const r=document.createElement("div");r.className="parvus-zoom__indicator",r.innerHTML=t.lightboxIndicatorIcon,e.appendChild(r)}})(e,p),e.classList.add("parvus-trigger"),e.addEventListener("click",le),be()&&d===u){const t=a[d].triggerElements.indexOf(e);j(t),G(e,t,(()=>{R(t)})),ne(),te(),re()}},D=e=>{if(!e||!e.hasAttribute("data-group"))return;const t=H(e);if(!a[t]||!a[t].triggerElements.includes(e))return;const r=a[t].triggerElements.indexOf(e);a[t].triggerElements.splice(r,1),a[t].sliderElements.splice(r,1),p.zoomIndicator&&(e=>{if(e.querySelector("img")&&null!==e.querySelector(".parvus-zoom__indicator")){const t=e.querySelector(".parvus-zoom__indicator");e.removeChild(t)}})(e),be()&&t===u&&(ne(),te(),re()),e.removeEventListener("click",le),e.classList.remove("parvus-trigger")},P=()=>{m=document.createElement("dialog"),m.setAttribute("role","dialog"),m.setAttribute("aria-modal","true"),m.setAttribute("aria-label",p.l10n.lightboxLabel),m.classList.add("parvus"),g=document.createElement("div"),g.classList.add("parvus__overlay"),m.appendChild(g),b=document.createElement("div"),b.className="parvus__toolbar",v=document.createElement("div"),f=document.createElement("div"),E=document.createElement("div"),E.className="parvus__controls",E.setAttribute("role","group"),E.setAttribute("aria-label",p.l10n.controlsLabel),f.appendChild(E),w=document.createElement("button"),w.className="parvus__btn parvus__btn--close",w.setAttribute("type","button"),w.setAttribute("aria-label",p.l10n.closeButtonLabel),w.innerHTML=p.closeButtonIcon,E.appendChild(w),y=document.createElement("button"),y.className="parvus__btn parvus__btn--previous",y.setAttribute("type","button"),y.setAttribute("aria-label",p.l10n.previousButtonLabel),y.innerHTML=p.previousButtonIcon,E.appendChild(y),A=document.createElement("button"),A.className="parvus__btn parvus__btn--next",A.setAttribute("type","button"),A.setAttribute("aria-label",p.l10n.nextButtonLabel),A.innerHTML=p.nextButtonIcon,E.appendChild(A),L=document.createElement("div"),L.className="parvus__counter",v.appendChild(L),b.appendChild(v),b.appendChild(f),m.appendChild(b),document.body.appendChild(m)},j=e=>{if(void 0!==a[u].sliderElements[e])return;const t=document.createElement("div"),r=document.createElement("div"),i=a[u].triggerElements.length;if(t.className="parvus__slide",t.style.position="absolute",t.style.left=100*e+"%",t.setAttribute("aria-hidden","true"),t.appendChild(r),i>1&&(t.setAttribute("role","group"),t.setAttribute("aria-label",`${p.l10n.slideLabel} ${e+1}/${i}`)),a[u].sliderElements[e]=t,e>=c){const r=(e=>{const t=a[u].sliderElements,r=t.length;for(let i=e+1;i{const t=a[u].sliderElements;for(let r=e-1;r>=0;r--)if(void 0!==t[r])return r;return-1})(e);-1!==r?a[u].sliderElements[r].after(t):a[u].slider.prepend(t)}},F=e=>{if(m&&e&&e.classList.contains("parvus-trigger")&&!be()){if(u=H(e),!a[u].triggerElements.includes(e))throw new Error("Ups, I can't find the element.");c=a[u].triggerElements.indexOf(e),history.pushState({parvus:"close"},"Image",window.location.href),ge(),p.hideScrollbar&&(document.body.style.marginInlineEnd=`${r()}px`,document.body.style.overflow="hidden"),m.classList.add("parvus--is-opening"),m.showModal(),(()=>{const e=document.createElement("div");e.className="parvus__slider",a[u].slider=e,m.appendChild(e)})(),j(c),ee(),ne(),te(),re(),W(c),G(e,c,(()=>{R(c,!0),m.classList.remove("parvus--is-opening"),a[u].slider.classList.add("parvus__slider--animate")})),V(c+1),V(c-1),ve("open")}},U=()=>{if(!be())throw new Error("Ups, I'm already closed.");const e=a[u].contentElements[c],t=a[u].triggerElements[c];he(),ie(),"close"===history.state?.parvus&&history.back(),m.classList.add("parvus--is-closing");const r=()=>{Z(c),m.close(),m.classList.remove("parvus--is-closing"),m.classList.remove("parvus--is-vertical-closing"),a[u].slider.remove(),a[u].slider=null,a[u].sliderElements=[],a[u].contentElements=[],L.removeAttribute("aria-hidden"),y.removeAttribute("aria-hidden"),y.removeAttribute("aria-disabled"),A.removeAttribute("aria-hidden"),A.removeAttribute("aria-disabled"),p.hideScrollbar&&(document.body.style.marginInlineEnd="",document.body.style.overflow="")};if(e&&"IMG"===e.tagName)if(document.startViewTransition){e.style.viewTransitionName="lightboximage";document.startViewTransition((()=>{e.style.opacity="0",e.style.viewTransitionName=null,t.style.viewTransitionName="lightboximage"})).finished.finally((()=>{r(),t.style.viewTransitionName=null}))}else e.style.opacity="0",r();else r()},V=e=>{e<0||e>=a[u].triggerElements.length||void 0!==a[u].sliderElements[e]||(j(e),G(a[u].triggerElements[e],e,(()=>{R(e)})))},W=e=>{a[u].sliderElements[e].setAttribute("aria-hidden","false")},G=(e,t,r)=>{const{contentElements:i,sliderElements:n}=a[u];if(void 0!==i[t])return void(r&&"function"==typeof r&&r());const s=n[t].querySelector("div"),l=new Image,o=document.createElement("div"),d=e.querySelector("img"),c=document.createElement("div");o.className="parvus__content",c.className="parvus__loader",c.setAttribute("role","progressbar"),c.setAttribute("aria-label",p.l10n.lightboxLoadingIndicatorLabel),s.appendChild(c);new Promise(((e,t)=>{l.onload=()=>e(l),l.onerror=e=>t(e)})).then((r=>{r.style.opacity=0,o.appendChild(r),s.appendChild(o),p.captions&&((e,t,r,i)=>{const n=document.createElement("div");let s=null;if(n.className="parvus__caption","self"===p.captionsSelector)r.hasAttribute(p.captionsAttribute)&&""!==r.getAttribute(p.captionsAttribute)&&(s=r.getAttribute(p.captionsAttribute));else{const e=r.querySelector(p.captionsSelector);null!==e&&(s=e.hasAttribute(p.captionsAttribute)&&""!==e.getAttribute(p.captionsAttribute)?e.getAttribute(p.captionsAttribute):e.innerHTML)}if(null!==s){const r=`parvus__caption-${i}`;n.id=r,n.innerHTML=`

${s}

`,e.appendChild(n),t.setAttribute("aria-describedby",r)}})(s,l,e,t),i[t]=r,r.setAttribute("width",r.naturalWidth),r.setAttribute("height",r.naturalHeight),ae(n[t],r)})).catch((()=>{const e=document.createElement("div");e.classList.add("parvus__content"),e.classList.add("parvus__content--error"),e.innerHTML=`${p.l10n.lightboxLoadingError}`,s.appendChild(e),i[t]=e})).finally((()=>{s.removeChild(c),r&&"function"==typeof r&&r()})),e.hasAttribute("data-sizes")&&""!==e.getAttribute("data-sizes")&&l.setAttribute("sizes",e.getAttribute("data-sizes")),e.hasAttribute("data-srcset")&&""!==e.getAttribute("data-srcset")&&l.setAttribute("srcset",e.getAttribute("data-srcset")),"A"===e.tagName?l.setAttribute("src",e.href):l.setAttribute("src",e.getAttribute("data-target")),d&&d.hasAttribute("alt")&&""!==d.getAttribute("alt")?l.alt=d.alt:e.hasAttribute("data-alt")&&""!==e.getAttribute("data-alt")?l.alt=e.getAttribute("data-alt"):l.alt=""},R=(e,t)=>{const r=a[u].contentElements[e];if(r&&"IMG"===r.tagName){const i=a[u].triggerElements[e];if(t&&document.startViewTransition){i.style.viewTransitionName="lightboximage";document.startViewTransition((()=>{r.style.opacity="",i.style.viewTransitionName=null,r.style.viewTransitionName="lightboximage"})).finished.finally((()=>{r.style.viewTransitionName=null}))}else r.style.opacity=""}else r.style.opacity=""},K=e=>{const t=c;if(!be())throw new Error("Oops, I'm closed.");{if("number"!=typeof e||isNaN(e))throw new Error("Oops, no slide specified.");const t=a[u].triggerElements;if(e===c)throw new Error(`Oops, slide ${e} is already selected.`);if(e<-1||e>=t.length)throw new Error(`Oops, I can't find slide ${e}.`)}void 0!==a[u].sliderElements[e]||(j(e),G(a[u].triggerElements[e],e,(()=>{R(e)}))),W(e),c=e,ee(),et&&(te(),V(e+1)),Z(t),re(),ve("select")},J=()=>{c>0&&K(c-1)},Q=()=>{const{triggerElements:e}=a[u];c{void 0!==a[u].sliderElements[e]&&a[u].sliderElements[e].setAttribute("aria-hidden","true")},ee=()=>{u=null!==u?u:d,$=-c*m.offsetWidth,a[u].slider.style.transform=`translate3d(${$}px, 0, 0)`,B=$},te=()=>{const{triggerElements:e}=a[u],t=e.length,r=c===t-1;t>1&&(0===c?(y.setAttribute("aria-disabled","true"),A.removeAttribute("aria-disabled")):r?(y.removeAttribute("aria-disabled"),A.setAttribute("aria-disabled","true")):(y.removeAttribute("aria-disabled"),A.removeAttribute("aria-disabled")))},re=()=>{L.textContent=`${c+1}/${a[u].triggerElements.length}`},ie=()=>{_={startX:0,endX:0,startY:0,endY:0}},ne=()=>{const e=a[u].triggerElements.length,t=a[u].slider,r=a[u].sliderElements,i=t.classList.contains("parvus__slider--is-draggable");p.simulateTouch&&p.swipeClose&&!i||p.simulateTouch&&e>1&&!i?t.classList.add("parvus__slider--is-draggable"):t.classList.remove("parvus__slider--is-draggable"),e>1?(t.setAttribute("role","region"),t.setAttribute("aria-roledescription","carousel"),t.setAttribute("aria-label",p.l10n.sliderLabel),r.forEach(((t,r)=>{t.setAttribute("role","group"),t.setAttribute("aria-label",`${p.l10n.slideLabel} ${r+1}/${e}`)}))):(t.removeAttribute("role"),t.removeAttribute("aria-roledescription"),t.removeAttribute("aria-label"),r.forEach((e=>{e.removeAttribute("role"),e.removeAttribute("aria-label")}))),1===e?(L.setAttribute("aria-hidden","true"),y.setAttribute("aria-hidden","true"),A.setAttribute("aria-hidden","true")):(L.removeAttribute("aria-hidden"),y.removeAttribute("aria-hidden"),A.removeAttribute("aria-hidden"))},se=()=>{q||(q=!0,n.requestAnimationFrame((()=>{a[u].sliderElements.forEach(((e,t)=>{ae(e,a[u].contentElements[t])})),ee(),q=!1})))},ae=(e,t)=>{if("IMG"!==t.tagName)return;const r=getComputedStyle(e),i=e.querySelector(".parvus__caption"),n=i?i.getBoundingClientRect().height:0,s=t.getAttribute("height"),a=t.getAttribute("width");let l=e.offsetHeight,o=e.offsetWidth;l-=parseFloat(r.paddingTop)+parseFloat(r.paddingBottom)+parseFloat(n),o-=parseFloat(r.paddingLeft)+parseFloat(r.paddingRight);const d=Math.min(o/a||0,l/s),u=a*d||0,c=s*d||0;s>c&&su&&a{const{target:t}=e;t===y?J():t===A?Q():(t===w||p.docClose&&!C&&!x&&t.classList.contains("parvus__slide"))&&U(),e.stopPropagation()},de=t=>{const r=(i=m,Array.from(i.querySelectorAll(e.join(", "))).filter((e=>null!==e.offsetParent)));var i;const n=r.indexOf(document.activeElement),s=r.length-1;switch(t.code){case"Tab":t.shiftKey?0===n&&(r[s].focus(),t.preventDefault()):n===s&&(r[0].focus(),t.preventDefault());break;case"Escape":U(),t.preventDefault();break;case"ArrowLeft":J(),t.preventDefault();break;case"ArrowRight":Q(),t.preventDefault()}},ue=e=>{e.preventDefault(),e.stopPropagation(),x=!1,C=!1,I=!0;const{pageX:t,pageY:r}=e;l.set(e.pointerId,e),_.startX=t,_.startY=r;const{slider:i}=a[u];i.classList.add("parvus__slider--is-dragging"),i.style.willChange="transform",h=getComputedStyle(g).opacity},ce=e=>{if(e.preventDefault(),!I)return;const t=a[u].sliderElements[c];if(l.set(e.pointerId,e),2===l.size){const e=Array.from(l.values()),r=Math.hypot(e[1].clientX-e[0].clientX,e[1].clientY-e[0].clientY);return M||(k=r,M=!0,S=N),T=S*(r/k),T=Math.min(Math.max(1,T),3),t.style.transform=`scale(${T})`,void(N=T)}if(T>1)return;const{pageX:r,pageY:i}=e;_.endX=r,_.endY=i,me()},pe=e=>{e.stopPropagation(),I=!1,M=!1;const{slider:t}=a[u];t.classList.remove("parvus__slider--is-dragging"),t.style.willChange="",(_.endX||_.endY)&&(()=>{const{startX:e,startY:t,endX:r,endY:i}=_,n=r-e,s=i-t,l=Math.abs(n),o=Math.abs(s),{triggerElements:d}=a[u],h=d.length;x?n>2&&l>=p.threshold&&c>0?J():n<2&&l>=p.threshold&&c!==h-1?Q():ee():C?(o>2&&p.swipeClose&&o>=p.threshold?U():(m.classList.remove("parvus--is-vertical-closing"),ee()),g.style.opacity=""):ee()})(),ie(),l.delete(e.pointerId),T>1?S=N:k=0},me=()=>{const{startX:e,endX:t,startY:r,endY:i}=_,n=e-t,s=i-r,l=Math.abs(s);Math.abs(n)>2&&!C&&a[u].triggerElements.length>1?(a[u].slider.style.transform=`translate3d(${B-Math.round(n)}px, 0, 0)`,x=!0,C=!1):Math.abs(s)>2&&!x&&p.swipeClose&&(!z&&l<=100&&(g.style.opacity=h-l/100),m.classList.add("parvus--is-vertical-closing"),a[u].slider.style.transform=`translate3d(${B}px, ${Math.round(s)}px, 0)`,x=!1,C=!0)},ge=()=>{n.addEventListener("keydown",de),n.addEventListener("resize",se),n.addEventListener("popstate",U),X.addEventListener("change",Y),m.addEventListener("click",oe),m.addEventListener("pointerdown",ue),m.addEventListener("pointerup",pe),m.addEventListener("pointermove",ce)},he=()=>{n.removeEventListener("keydown",de),n.removeEventListener("resize",se),n.removeEventListener("popstate",U),X.removeEventListener("change",Y),m.removeEventListener("click",oe),m.removeEventListener("pointerdown",ue),m.removeEventListener("pointerup",pe),m.removeEventListener("pointermove",ce)},be=()=>m.hasAttribute("open"),ve=e=>{const t=new CustomEvent(e,{cancelable:!0});m.dispatchEvent(t)},fe=()=>{if(p=(e=>{const t={selector:".lightbox",gallerySelector:null,zoomIndicator:!0,captions:!0,captionsSelector:"self",captionsAttribute:"data-caption",docClose:!0,swipeClose:!0,simulateTouch:!0,threshold:50,hideScrollbar:!0,lightboxIndicatorIcon:'',previousButtonIcon:'',nextButtonIcon:'',closeButtonIcon:'',l10n:i},r={...t,...e};return e&&e.l10n&&(r.l10n={...t.l10n,...e.l10n}),r})(t),Y(),null!==p.gallerySelector){document.querySelectorAll(p.gallerySelector).forEach(((e,t)=>{const r=t;e.querySelectorAll(p.selector).forEach((e=>{e.setAttribute("data-group",`parvus-gallery-${r}`),O(e)}))}))}document.querySelectorAll(`${p.selector}:not(.parvus-trigger)`).forEach(O)};return fe(),{init:fe,open:F,close:U,select:K,previous:J,next:Q,currentIndex:()=>c,add:O,remove:D,destroy:()=>{if(!m)return;be()&&U(),m.remove();document.querySelectorAll(".parvus-trigger").forEach(D),ve("destroy")},isOpen:be,on:(e,t)=>{m&&m.addEventListener(e,t)},off:(e,t)=>{m&&m.removeEventListener(e,t)}}}})); diff --git a/src/js/parvus.js b/src/js/parvus.js index fc7ae4b..424c7e6 100644 --- a/src/js/parvus.js +++ b/src/js/parvus.js @@ -5,11 +5,13 @@ import { addZoomIndicator, removeZoomIndicator } from './zoom-indicator' // Default language import en from '../l10n/en.js' +/** + * Parvus Lightbox + * + * @param {Object} userOptions - User configuration options + * @returns {Object} Parvus instance + */ export default function Parvus (userOptions) { - /** - * Global variables - * - */ const BROWSER_WINDOW = window const GROUP_ATTRIBUTES = { triggerElements: [], @@ -18,6 +20,7 @@ export default function Parvus (userOptions) { contentElements: [] } const GROUPS = {} + const activePointers = new Map() let groupIdCounter = 0 let newGroup = null let activeGroup = null @@ -38,6 +41,11 @@ export default function Parvus (userOptions) { let isDraggingX = false let isDraggingY = false let pointerDown = false + let pinchStartDistance = 0 + let currentScale = 1 + let isPinching = false + let lastScale = 1 + let baseScale = 1 let offset = null let offsetTmp = null let resizeTicking = false @@ -1112,10 +1120,6 @@ export default function Parvus (userOptions) { event.preventDefault() event.stopPropagation() - if (event.pointerType === 'mouse' && !config.simulateTouch) { - return - } - isDraggingX = false isDraggingY = false @@ -1123,6 +1127,8 @@ export default function Parvus (userOptions) { const { pageX, pageY } = event + activePointers.set(event.pointerId, event) + drag.startX = pageX drag.startY = pageY @@ -1145,14 +1151,49 @@ export default function Parvus (userOptions) { const pointermoveHandler = (event) => { event.preventDefault() - if (pointerDown) { - const { pageX, pageY } = event + if (!pointerDown) { + return + } + + const currentImg = GROUPS[activeGroup].sliderElements[currentIndex] + + // Update pointer position + activePointers.set(event.pointerId, event) + + // Zoom + if (activePointers.size === 2) { + const points = Array.from(activePointers.values()) + const distance = Math.hypot( + points[1].clientX - points[0].clientX, + points[1].clientY - points[0].clientY + ) + + if (!isPinching) { + pinchStartDistance = distance + isPinching = true + baseScale = lastScale + } + + currentScale = baseScale * (distance / pinchStartDistance) + currentScale = Math.min(Math.max(1, currentScale), 3) + + currentImg.style.transform = `scale(${currentScale})` - drag.endX = pageX - drag.endY = pageY + lastScale = currentScale - doSwipe() + return } + + if (currentScale > 1) { + return + } + + const { pageX, pageY } = event + + drag.endX = pageX + drag.endY = pageY + + doSwipe() } /** @@ -1167,6 +1208,7 @@ export default function Parvus (userOptions) { event.stopPropagation() pointerDown = false + isPinching = false const { slider } = GROUPS[activeGroup] @@ -1178,6 +1220,14 @@ export default function Parvus (userOptions) { } clearDrag() + + activePointers.delete(event.pointerId) + + if (currentScale > 1) { + baseScale = lastScale + } else { + pinchStartDistance = 0 + } } /**