popupable 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,203 @@
1
+ # popupable
2
+
3
+ A lightweight, zero-dependency lightbox library using modern JavaScript and CSS.
4
+ Just add `data-popupable` to any image!
5
+
6
+ [![npm version](https://badge.fury.io/js/popupable.svg)](https://www.npmjs.com/package/popupable)
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
8
+
9
+ [**Live Demo**](https://popupable.ewanhowell.com/)
10
+
11
+ ## Features
12
+
13
+ * No dependencies
14
+ * Animates open from the element's original position
15
+ * Works with mouse, touch, and keyboard
16
+ * Gallery groups with swipe, scroll, keyboard, and button navigation
17
+ * Thumbnail strip and image counter
18
+ * Pinch-to-zoom on touch, with optional click/tap-to-zoom support
19
+ * Load a hi-res image on open
20
+ * Checkerboard background for transparent images
21
+ * Customizable via CSS variables
22
+
23
+ ## Quick Start
24
+
25
+ ### Install via npm
26
+ ```bash
27
+ npm install popupable
28
+ ```
29
+
30
+ ### Or use via CDN
31
+ https://www.jsdelivr.com/package/npm/popupable
32
+
33
+ ### Import
34
+
35
+ ```js
36
+ import "popupable/styles.css"
37
+ import "popupable"
38
+ ```
39
+
40
+ ### Add popups to your images
41
+
42
+ ```html
43
+ <img src="photo.jpg" data-popupable>
44
+ <img src="thumbnail.jpg" data-popupable data-popupable-src="full-res.jpg">
45
+ <img src="photo.jpg" data-popupable data-popupable-title="Sunset" data-popupable-description="Taken in July 2024">
46
+ ```
47
+
48
+ Close by clicking again, or pressing Escape, Backspace, or Delete.
49
+
50
+ ## Settings
51
+
52
+ | Attribute | Description |
53
+ |---|---|
54
+ | `data-popupable` | Required. Marks an image as openable. Optionally set a value to give the popup a CSS `id`. |
55
+ | `data-popupable-src="url"` | A different image to display when the popup is open, e.g. a higher resolution version. |
56
+ | `data-popupable-title="text"` | Title text shown alongside the image. |
57
+ | `data-popupable-description="text"` | Description text shown alongside the image. |
58
+ | `data-popupable-transparent` | Shows a checkerboard background behind transparent images. |
59
+ | `data-popupable-maintain-aspect` | Uses the element's rendered aspect ratio instead of the image's natural dimensions. |
60
+ | `data-popupable-no-upscale` | Prevents the popup from scaling the image beyond its native resolution. |
61
+ | `data-popupable-zoomable` | Enables click/tap-to-zoom and scroll wheel zoom. |
62
+ | `data-popupable-group="name"` | Groups images into a navigable gallery. |
63
+ | `data-popupable-counter` | Shows a "1 / N" counter when in a group. Read from the clicked image. |
64
+ | `data-popupable-thumbnails` | Shows a thumbnail strip when in a group. Read from the clicked image. |
65
+ | `data-popupable-order="..."` | Controls the order of UI elements. See [Custom UI order](#custom-ui-order). |
66
+
67
+ ## Advanced Usage
68
+
69
+ ### Hi-res source
70
+
71
+ Use `data-popupable-src` to display a different (e.g. higher resolution) image when the popup is open:
72
+
73
+ ```html
74
+ <img src="thumbnail.jpg" data-popupable data-popupable-src="full-res.jpg">
75
+ ```
76
+
77
+ ### Transparent images
78
+
79
+ Add `data-popupable-transparent` to show a checkerboard background behind transparent images:
80
+
81
+ ```html
82
+ <img src="icon.png" data-popupable data-popupable-transparent>
83
+ ```
84
+
85
+ ### Maintain aspect ratio
86
+
87
+ By default, the popup size is calculated from the image's natural dimensions. Add `data-popupable-maintain-aspect` to use the element's rendered aspect ratio instead, which is useful when the image is cropped or stretched via CSS:
88
+
89
+ ```html
90
+ <img src="photo.jpg" data-popupable data-popupable-maintain-aspect>
91
+ ```
92
+
93
+ ### Prevent upscaling
94
+
95
+ Add `data-popupable-no-upscale` to prevent the popup from scaling the image beyond its native resolution:
96
+
97
+ ```html
98
+ <img src="pixel-art.png" data-popupable data-popupable-no-upscale>
99
+ ```
100
+
101
+ ### Zoom
102
+
103
+ All images support pinch-to-zoom. Add `data-popupable-zoomable` to also enable tap/click-to-zoom and scroll wheel zoom:
104
+
105
+ ```html
106
+ <img src="map.jpg" data-popupable data-popupable-zoomable>
107
+ ```
108
+
109
+ When zoomed in, pan by dragging and zoom in/out with the scroll wheel. Click the image or background, or pinch back to scale 1, to unzoom.
110
+
111
+ ### Groups / Galleries
112
+
113
+ Group multiple images together with `data-popupable-group`. Users can navigate with arrow buttons, swipe gestures, scroll wheel, or keyboard shortcuts.
114
+
115
+ ```html
116
+ <img src="photo1.jpg" data-popupable data-popupable-group="holiday">
117
+ <img src="photo2.jpg" data-popupable data-popupable-group="holiday">
118
+ <img src="photo3.jpg" data-popupable data-popupable-group="holiday">
119
+ ```
120
+
121
+ #### Counter and thumbnails
122
+
123
+ Add `data-popupable-counter` and/or `data-popupable-thumbnails` to individual group members. The attributes are read from whichever image is clicked to open the gallery, so add them to every image that should show these elements:
124
+
125
+ ```html
126
+ <img src="photo1.jpg" data-popupable data-popupable-group="holiday" data-popupable-counter data-popupable-thumbnails>
127
+ <img src="photo2.jpg" data-popupable data-popupable-group="holiday" data-popupable-counter data-popupable-thumbnails>
128
+ <img src="photo3.jpg" data-popupable data-popupable-group="holiday" data-popupable-counter data-popupable-thumbnails>
129
+ ```
130
+
131
+ #### Custom UI order
132
+
133
+ Control the order of UI elements around the image with `data-popupable-order`. The `image` token marks where the image sits; everything before it goes in the header, everything after goes in the footer.
134
+
135
+ ```html
136
+ <!-- Counter above image, thumbnails and content below -->
137
+ <img data-popupable data-popupable-order="counter,image,thumbnails,content">
138
+ ```
139
+
140
+ Default order: `counter,image,content,thumbnails`
141
+
142
+ #### Keyboard navigation
143
+
144
+ | Keys | Action |
145
+ |---|---|
146
+ | `→` `↓` `Page Down` `D` `S` | Next image |
147
+ | `←` `↑` `Page Up` `A` `W` | Previous image |
148
+ | `Home` | First image |
149
+ | `End` | Last image |
150
+ | `1`–`9` | Jump to image by number |
151
+
152
+ ### Custom IDs
153
+
154
+ Set the value of `data-popupable` to give the popup a CSS `id`, useful for per-popup styling. In a gallery, the `id` updates as you navigate, so each image can have its own style:
155
+
156
+ ```html
157
+ <img src="red.jpg" data-popupable="theme-red" data-popupable-group="themed">
158
+ <img src="green.jpg" data-popupable="theme-green" data-popupable-group="themed">
159
+ <img src="blue.jpg" data-popupable="theme-blue" data-popupable-group="themed">
160
+ ```
161
+
162
+ ```css
163
+ #theme-red { --popupable-background: #3a0000cc; }
164
+ #theme-green { --popupable-background: #003a00cc; }
165
+ #theme-blue { --popupable-background: #00003acc; }
166
+ ```
167
+
168
+ ## Customization
169
+
170
+ Popups can be styled using CSS variables:
171
+
172
+ ```css
173
+ :root {
174
+ /* Overlay */
175
+ --popupable-background: #000B; /* Backdrop color */
176
+ --popupable-blur: 6px; /* Backdrop blur amount */
177
+
178
+ /* UI elements (header, footer, buttons) */
179
+ --popupable-ui-background: #0008; /* Header/footer/button background */
180
+
181
+ /* Spacing */
182
+ --popupable-screen-padding: 40px; /* Minimum gap between image and viewport edge */
183
+
184
+ /* Animation */
185
+ --popupable-open-duration: .25s; /* Open/close transition duration */
186
+ --popupable-switch-duration: .25s; /* Gallery navigation transition duration */
187
+ }
188
+ ```
189
+
190
+ ## How it works
191
+
192
+ popupable uses a clone-and-expand technique:
193
+
194
+ 1. **Clones the image** at its exact on-screen position and size
195
+ 2. **Hides the original** and appends the clone as a fixed overlay
196
+ 3. **Animates the clone** to fill the viewport, respecting aspect ratio and padding
197
+ 4. **On close**, reverses the animation back to the original position, then removes the overlay
198
+
199
+ This gives a seamless visual transition with no layout shifts.
200
+
201
+ ## License
202
+
203
+ MIT © [Ewan Howell](https://github.com/ewanhowell5195)
@@ -0,0 +1,7 @@
1
+ /*!
2
+ * popupable
3
+ * Version : 1.0.0
4
+ * License : MIT
5
+ * Copyright: 2025 Ewan Howell
6
+ */
7
+ @property --popupable-screen-padding{syntax:"<length>";inherits:true;initial-value:40px}@property --popupable-background{syntax:"<color>";inherits:true;initial-value:#000B}@property --popupable-ui-background{syntax:"<color>";inherits:true;initial-value:#0008}@property --popupable-blur{syntax:"<length>";inherits:true;initial-value:6px}@property --popupable-open-duration{syntax:"<time>";inherits:true;initial-value:.25s}@property --popupable-switch-duration{syntax:"<time>";inherits:true;initial-value:.25s}[data-popupable],[data-popupable] *{cursor:pointer!important}.popupable-hide{visibility:hidden!important}.popupable-container{position:fixed;inset:0;transition:background var(--popupable-open-duration),backdrop-filter var(--popupable-open-duration);z-index:2147483646;user-select:none;pointer-events:none}.popupable-container.popupable-active{z-index:2147483647;background:var(--popupable-background);backdrop-filter:blur(var(--popupable-blur))}.popupable-container>*{pointer-events:none}.popupable-viewport{position:fixed;left:var(--popupable-vv-left,0);top:var(--popupable-vv-top,0);width:calc(var(--popupable-vv-width,100vw) * var(--popupable-vv-scale,1));height:calc(var(--popupable-vv-height,100vh) * var(--popupable-vv-scale,1));transform-origin:top left;transform:scale(var(--popupable-vv-ui-scale,1));pointer-events:none!important}.popupable-container *{box-sizing:border-box}.popupable-container.popupable-open,.popupable-container.popupable-open>*{pointer-events:initial}.popupable-clones{transition:transform var(--popupable-switch-duration)}.popupable-clone-container{position:fixed;transition:all var(--popupable-open-duration);pointer-events:initial;overflow:hidden;transform:translateX(calc(var(--popupable-view-width) * var(--popupable-offset-multiplier)));transform-origin:0 0}.popupable-clone-container.popupable-transparent::before{content:"";position:absolute;inset:0;background-image:conic-gradient(#313131 .25turn,#1e1e1e .25turn .5turn,#313131 .5turn .75turn,#1e1e1e .75turn);background-size:32px 32px;background-attachment:fixed;z-index:-1;opacity:0;transition:opacity var(--popupable-open-duration);image-rendering:pixelated}.popupable-container.popupable-active .popupable-clone-container{border-radius:0!important}.popupable-container.popupable-active .popupable-clone-container.popupable-transparent::before{opacity:1}.popupable-container.popupable-open .popupable-clone-container{transition:transform var(--popupable-switch-duration),translate var(--popupable-switch-duration)}.popupable-clone,.popupable-clone-layer{width:100%;height:100%;position:absolute;inset:0;transition:opacity var(--popupable-open-duration)}.popupable-clone-layer{opacity:0;object-fit:cover}.popupable-container.popupable-open .popupable-clone:not(:last-child){opacity:0}.popupable-container.popupable-active .popupable-clone-layer{opacity:1}.popupable-button{width:40px;height:40px;display:flex;align-items:center;justify-content:center;transition:opacity var(--popupable-open-duration),transform var(--popupable-open-duration);cursor:pointer;position:relative}.popupable-button::before{content:"";position:absolute;inset:0;opacity:.5;background:var(--popupable-ui-background);transition:opacity .25s;border-radius:50%}.popupable-button svg{width:24px;height:24px;z-index:1}.popupable-container.popupable-open .popupable-button{transition:background .25s,opacity var(--popupable-open-duration),transform .25s}.popupable-button:hover::before,.popupable-next-container:hover .popupable-button::before,.popupable-prev-container:hover .popupable-button::before{opacity:1}.popupable-next-container,.popupable-prev-container{position:absolute;top:50%;transform:translateY(-50%);padding:40px;cursor:pointer;z-index:1;pointer-events:auto}.popupable-next-container{right:calc(var(--popupable-screen-padding)/ 2)}.popupable-prev-container{left:calc(var(--popupable-screen-padding)/ 2)}.popupable-nav-button{opacity:0}.popupable-next{transform:translateX(40px)}.popupable-prev{transform:translateX(-40px)}.popupable-container.popupable-active .popupable-nav-button{opacity:1;transform:translateX(0)}.popupable-next-container:hover .popupable-button{transform:translateX(4px)}.popupable-prev-container:hover .popupable-button{transform:translateX(-4px)}.popupable-locked .popupable-next-container,.popupable-locked .popupable-prev-container,.popupable-next-container.popupable-disabled,.popupable-next-container.popupable-nav-inactive,.popupable-prev-container.popupable-disabled,.popupable-prev-container.popupable-nav-inactive{pointer-events:none}.popupable-locked .popupable-next-container .popupable-next,.popupable-next-container.popupable-disabled .popupable-next,.popupable-next-container.popupable-nav-inactive .popupable-next{transform:translateX(40px);opacity:0}.popupable-locked .popupable-prev-container .popupable-prev,.popupable-prev-container.popupable-disabled .popupable-prev,.popupable-prev-container.popupable-nav-inactive .popupable-prev{transform:translateX(-40px);opacity:0}.popupable-container:not(.popupable-active) .popupable-clone-container:not(.popupable-clone-extra){transform:initial}.popupable-container:not(.popupable-active) .popupable-clone-extra{opacity:0;scale:0.75}.popupable-footer,.popupable-header{position:absolute;top:0;left:0;right:0;opacity:0;transition:opacity var(--popupable-open-duration),transform var(--popupable-open-duration);transform:translateY(-40px);color:#fff;background:var(--popupable-ui-background);pointer-events:auto}.popupable-footer{top:initial;bottom:0;transform:translateY(40px)}.popupable-container.popupable-active:not(.popupable-locked) .popupable-footer,.popupable-container.popupable-active:not(.popupable-locked) .popupable-header{opacity:1;transform:initial}.popupable-counter{padding:10px;text-align:center}.popupable-footer>*+*,.popupable-header>*+*{border-top:1px solid #fff3}.popupable-content-container{display:grid;align-items:flex-end;transition:height var(--popupable-switch-duration);overflow:hidden;position:relative}.popupable-thumbnails{display:flex;gap:10px;padding:10px;justify-content:safe center;overflow:hidden;scrollbar-width:none;touch-action:pan-x}.popupable-thumbnails.popupable-thumbnails-dragging{cursor:grabbing}.popupable-thumbnail{width:64px;height:64px;object-fit:cover;opacity:.65;cursor:pointer;transition:opacity .2s,outline-color .2s;outline:2px solid #0000;outline-offset:-2px;flex:0 0 auto}.popupable-thumbnail:hover{opacity:1}.popupable-thumbnail.popupable-thumbnail-active{opacity:1;outline-color:#fffa}.popupable-content{grid-column:1;grid-row:1;text-align:center;padding:16px;color:#fff;display:flex;flex-direction:column;gap:8px;user-select:text;transition:opacity var(--popupable-switch-duration),transform var(--popupable-switch-duration);position:absolute;bottom:0;left:50%;transform:translateX(-50%);max-width:100%;width:max-content}.popupable-title{font-size:32px;font-weight:600}.popupable-content-before{pointer-events:none;opacity:0;transform:translateX(calc(-50% - 80px))}.popupable-content-after{pointer-events:none;opacity:0;transform:translateX(calc(-50% + 80px))}.popupable-zoomable{cursor:zoom-in}.popupable-zoomed{cursor:zoom-out;touch-action:none}@media (max-width:768px){:root{--popupable-screen-padding:14px}.popupable-button{width:42px;height:42px}.popupable-next-container,.popupable-prev-container{padding:36px}.popupable-button svg{width:25px;height:25px}.popupable-counter{padding:8px}.popupable-content{padding:14px;gap:6px}.popupable-title{font-size:28px}.popupable-thumbnail{width:48px;height:48px}}
@@ -0,0 +1,7 @@
1
+ /*!
2
+ * popupable
3
+ * Version : 1.0.0
4
+ * License : MIT
5
+ * Copyright: 2025 Ewan Howell
6
+ */
7
+ {let e,t,n,o=0;function disableScroll(){window.addEventListener("wheel",prevent,{passive:!1}),window.addEventListener("touchmove",prevent,{passive:!1}),window.addEventListener("keydown",blockKeys,!0)}function enableScroll(){window.removeEventListener("wheel",prevent),window.removeEventListener("touchmove",prevent),window.removeEventListener("keydown",blockKeys,!0)}function prevent(e){e.preventDefault()}function blockKeys(e){["ArrowUp","ArrowDown","PageUp","PageDown","Home","End"," "].includes(e.key)&&e.preventDefault()}function setCloneToOriginalRect(e,t){const n=t.getBoundingClientRect();e.style.top=visualViewport.offsetTop+n.top+"px",e.style.left=visualViewport.offsetLeft+n.left+"px",e.style.width=n.width+"px",e.style.height=n.height+"px"}function openPopupable(e){if("open"===e.state)return;e.state="open";const{cloneContainer:t,popup:n,transition:o,group:a,listeners:r}=e;n.classList.add("popupable-active"),updateExpandedSize(),t.removeEventListener("transitionend",o.listener),o.listener=e=>{if(!e||e.target===e.currentTarget){if(t.removeEventListener("transitionend",o.listener),n.classList.add("popupable-open"),a)for(const e of a)e.cloneContainer.style.display=null;for(const e of r)e.target.addEventListener(e.event,e.func,e.args)}},o.duration?t.addEventListener("transitionend",o.listener):o.listener()}function closePopupable(){if(!e||"close"===e.state)return;o++,e.state="close";const{cloneContainer:n,clone:a,original:r,popup:i,transition:l,group:s,listeners:p}=e;if(i.classList.remove("popupable-active"),i.classList.remove("popupable-open"),setCloneToOriginalRect(n,r),s)for(const[e,t]of s.entries())t.clone!==a&&e!==s.currentIndex&&(t.cloneContainer.style.display="none");for(const e of p)e.target.removeEventListener(e.event,e.func);n.removeEventListener("transitionend",l.listener);const c=e;l.listener=o=>{o&&o.target!==o.currentTarget||(n.removeEventListener("transitionend",l.listener),r.classList.remove("popupable-hide"),i.remove(),c===e&&(enableScroll(),t=e,e=null))},l.duration?n.addEventListener("transitionend",l.listener):l.listener()}function updateExpandedSize(){if(!e||"close"===e.state)return;const t=visualViewport?.width||window.innerWidth,n=visualViewport?.height||window.innerHeight,o=visualViewport?.offsetTop||0,a=visualViewport?.offsetLeft||0,r=visualViewport?.scale||1;document.documentElement.style.setProperty("--popupable-view-width",t+"px"),e.popup.style.setProperty("--popupable-vv-width",t+"px"),e.popup.style.setProperty("--popupable-vv-height",n+"px"),e.popup.style.setProperty("--popupable-vv-top",o+"px"),e.popup.style.setProperty("--popupable-vv-left",a+"px"),e.popup.style.setProperty("--popupable-vv-scale",r),e.popup.style.setProperty("--popupable-vv-ui-scale",1/r);const i=parseFloat(getComputedStyle(e.popup).getPropertyValue("--popupable-vv-ui-scale"))||1,l=(parseFloat(getComputedStyle(e.popup).getPropertyValue("--popupable-screen-padding"))||0)/r,s=Math.max(0,t-2*l),p=n-2*l,c=e.popup.querySelector(".popupable-counter"),u=c?c.getBoundingClientRect().height/i:0;let d;d=e.group?e.group:[e];for(const t of d){let c;if(t.maintainAspect){const e=t.original.getBoundingClientRect();c=e.width/e.height}else c=t.cloneLayer?t.cloneLayer.naturalWidth/t.cloneLayer.naturalHeight:t.original.naturalWidth/t.original.naturalHeight;const d=Math.max(0,p),f=t.content?t.content.getBoundingClientRect().height/i:0,m=e.thumbnailsContainer?e.thumbnailsContainer.getBoundingClientRect().height/i:0,b=(e.orderPlacement.counterTop?u:0)+(e.orderPlacement.contentTop?f:0)+(e.orderPlacement.thumbnailsTop?m:0),v=(e.orderPlacement.counterBottom?u:0)+(e.orderPlacement.contentBottom?f:0)+(e.orderPlacement.thumbnailsBottom?m:0),h=Math.max(0,n-b-v-2*l);let g=s,y=g/c;if(y>d&&(y=d,g=y*c),y>h&&(y=h,g=y*c),t.noUpscale){const e=t.cloneLayer||t.original,n=e.naturalWidth,o=e.naturalHeight;if(n&&o){const e=n/r,t=o/r,a=Math.min(1,e/g,t/y);a<1&&(g*=a,y*=a)}}let L=o+l+(d-y)/2;const x=o+n-v-l-y;L=Math.min(L,x),L=Math.max(L,o+b+l),t.cloneContainer.style.top=L+"px",t.cloneContainer.style.left=a+l+(s-g)/2+"px",t.cloneContainer.style.width=g+"px",t.cloneContainer.style.height=y+"px"}if(e.contentContainer){let t;t=e.group?e.group[e.group.currentIndex]:e;const n=t.content.getBoundingClientRect();e.contentContainer.style.height=n.height/i+"px"}}function parsePopupableOrder(e){const t=["counter","image","content","thumbnails"],n=new Set(t),o=[];if(e)for(const t of e.split(",")){const e=t.trim().toLowerCase();e&&n.has(e)&&!o.includes(e)&&o.push(e)}for(const e of t)o.includes(e)||o.push(e);const a=o.indexOf("image");return{top:o.slice(0,a),bottom:o.slice(a+1)}}function cloneElement(e){const t=document.createElement("div");t.className="popupable-clone-container",e.hasAttribute("data-popupable-transparent")&&t.classList.add("popupable-transparent");const n=new Image;n.className="popupable-clone",n.src=e.currentSrc??e.src;const o=getComputedStyle(e);let a,r,i;if(t.style.borderRadius=o.borderRadius,n.style.objectFit=o.objectFit,n.style.objectPosition=o.objectPosition,n.style.imageRendering=o.imageRendering,n.style.background=o.background,t.append(n),e.dataset.popupableSrc&&(a=new Image,a.className="popupable-clone-layer",a.src=e.dataset.popupableSrc,a.style.imageRendering=o.imageRendering,t.append(a),"fill"===n.style.objectFit)){const t=e.getBoundingClientRect();e.naturalWidth&&e.naturalHeight&&Math.abs(t.width/t.height-e.naturalWidth/e.naturalHeight)<.01&&(n.style.objectFit="cover")}if(e.dataset.popupableTitle||e.dataset.popupableDescription){if(r=document.createElement("div"),r.classList="popupable-content",e.dataset.popupableTitle){const t=document.createElement("div");t.className="popupable-title",t.textContent=e.dataset.popupableTitle,r.append(t)}if(e.dataset.popupableDescription){const t=document.createElement("div");t.className="popupable-description",t.textContent=e.dataset.popupableDescription,r.append(t)}}return e.hasAttribute("data-popupable-zoomable")&&(i=!0,t.classList.add("popupable-zoomable")),{id:e.dataset.popupable,original:e,cloneContainer:t,clone:n,cloneLayer:a,maintainAspect:e.hasAttribute("data-popupable-maintain-aspect"),noUpscale:e.hasAttribute("data-popupable-no-upscale"),counter:e.hasAttribute("data-popupable-counter"),thumbnails:e.hasAttribute("data-popupable-thumbnails"),order:parsePopupableOrder(e.dataset.popupableOrder),ready:Promise.all([n,a].filter(Boolean).map(e=>e.complete?Promise.resolve():new Promise(t=>{e.addEventListener("load",t,{once:!0}),e.addEventListener("error",t,{once:!0})}))),content:r,zoomable:i}}let a,r,i,l=0;function handleMove(t){if("open"!==e?.state||!e.group||!a)return;const n=e.group[e.group.currentIndex];n.cloneContainer.parentElement.style.transition="initial",n.cloneContainer.parentElement.style.transform=`translateX(${(t.touches?.[0].clientX??t.clientX)-r}px)`}document.addEventListener("pointerdown",t=>{0===t.button&&("touch"===t.pointerType&&l++,a||(n=t.target),a||"open"!==e?.state||t.target.closest(".popupable-header, .popupable-footer")||(a=!0,r=t.clientX,i=t.clientY))}),document.addEventListener("mousemove",handleMove),document.addEventListener("touchmove",handleMove,{passive:!0}),document.addEventListener("pointerup",async s=>{if(0!==s.button)return;if("touch"===s.pointerType&&(l=Math.max(0,l-1),a&&l>=1))return;if(a){a=!1;const U=e.group?e.group[e.group.currentIndex]:e;U.cloneContainer.parentElement.style.transition=null,U.cloneContainer.parentElement.style.transform=null;const _=s.clientX-r,j=Math.abs(_),K=s.clientY-i,G=Math.abs(K);if("touch"===s.pointerType&&G>56&&G>1.1*j)return void closePopupable();if(e.group&&j>3){const Z=Math.max(0,Math.floor((j-window.innerWidth/2)/window.innerWidth));if(_>32)for(let J=0;J<=Z;J++)e.goPrev();else if(_<-32)for(let Q=0;Q<=Z;Q++)e.goNext();return void(e.blocked=!0)}}const p=s.target.closest(".popupable-viewport")&&!n.closest(".popupable-viewport");if(!(p||s.target==n||n.classList.contains("popupable-clone-container")&&s.target===t?.original))return;const c=(p?n.closest("[data-popupable]"):null)||s.target.closest("[data-popupable]");if(!c){if(p)return void closePopupable();if(s.target.closest(".popupable-container"))return;return void(e&&("zoomed"===e.state?e.unzoom():closePopupable()))}if(s.preventDefault(),e?.original===c&&e.popup&&!e.popup.isConnected&&"close"!==e.state)return;const u=++o;e&&closePopupable(),e={transition:{},listeners:[]};const d=e,f=document.createElement("div");f.className="popupable-clones";const m=cloneElement(c),{cloneContainer:b,clone:v,content:h}=m;let g;if(c.dataset.popupableGroup){const ee=document.querySelectorAll(`[data-popupable-group="${c.dataset.popupableGroup}"]`);if(ee.length){g=[];for(const[te,ne]of ee.entries())if(ne===c)g.push(m),g.currentIndex=te,f.append(b);else{const oe=cloneElement(ne);oe.cloneContainer.style.display="none",oe.cloneContainer.classList.add("popupable-clone-extra"),g.push(oe),f.append(oe.cloneContainer)}}}else f.append(b);const y=document.createElement("div");y.className="popupable-container",m.id&&(y.id=m.id);const L=document.createElement("div");let x,w,C,P,E,I,M,z,k,T;L.className="popupable-viewport",h&&(x=document.createElement("div"),x.classList="popupable-content-container");const X={};if(g){m.counter&&(w=document.createElement("div"),w.className="popupable-header",P=document.createElement("div"),P.className="popupable-counter",w.append(P)),m.thumbnails&&(E=document.createElement("div"),E.className="popupable-thumbnails",I=g.map((e,t)=>{const n=new Image;return n.className="popupable-thumbnail",n.src=e.original.currentSrc??e.original.src,n.dataset.thumbnailIndex=t,E.append(n),n})),L.innerHTML=`\n <div class="popupable-prev-container${g.currentIndex?"":" popupable-disabled"}">\n <div class="popupable-button popupable-nav-button popupable-prev">\n <svg width="24px" height="24px" viewBox="0 -960 960 960" fill="#fff">\n <path d="m313-440 224 224-57 56-320-320 320-320 57 56-224 224h487v80H313Z"/>\n </svg>\n </div>\n </div>\n <div class="popupable-next-container${g.currentIndex===g.length-1?" popupable-disabled":""}">\n <div class="popupable-button popupable-nav-button popupable-next">\n <svg width="24px" height="24px" viewBox="0 -960 960 960" fill="#fff">\n <path d="M647-440H160v-80h487L423-744l57-56 320 320-320 320-57-56 224-224Z"/>\n </svg>\n </div>\n </div>\n `;const ae=L.querySelector(".popupable-next-container"),re=L.querySelector(".popupable-prev-container");let ie,le,se,pe;const ce=!(navigator.maxTouchPoints>0||window.matchMedia("(hover: none)").matches);function A(){ce&&(clearTimeout(ie),pe||(ie=setTimeout(()=>{pe||(ae.classList.add("popupable-nav-inactive"),re.classList.add("popupable-nav-inactive"))},1500)))}function S(){ae.classList.remove("popupable-nav-inactive"),re.classList.remove("popupable-nav-inactive"),A()}async function Y(){const e=g[g.currentIndex];await e.ready,g.currentIndex?re.classList.remove("popupable-disabled"):re.classList.add("popupable-disabled"),g.currentIndex===g.length-1?ae.classList.add("popupable-disabled"):ae.classList.remove("popupable-disabled");for(const[e,t]of g.entries()){const n=e-g.currentIndex;t.cloneContainer.style.setProperty("--popupable-offset-multiplier",n),t.cloneContainer.style.zIndex=-1*Math.abs(n),t.content&&(n?n>0?(t.content.classList.add("popupable-content-after"),t.content.classList.remove("popupable-content-before")):(t.content.classList.add("popupable-content-before"),t.content.classList.remove("popupable-content-after")):(t.content.classList.remove("popupable-content-before"),t.content.classList.remove("popupable-content-after")))}if(e.id?y.id=e.id:y.removeAttribute("id"),P&&(P.textContent=`${g.currentIndex+1} / ${g.length}`),I){for(const[e,t]of I.entries())t.classList.toggle("popupable-thumbnail-active",e===g.currentIndex);const e=I[g.currentIndex];requestAnimationFrame(()=>{if(!e||!E?.isConnected)return;const t=getComputedStyle(E),n=parseFloat(t.paddingLeft)||0,o=parseFloat(t.paddingRight)||0,a=E.scrollLeft+n,r=E.scrollLeft+E.clientWidth-o,i=e.offsetLeft,l=i+e.offsetWidth;let s=E.scrollLeft;i<a?s=Math.max(0,i-n):l>r&&(s=l-E.clientWidth+o),s!==E.scrollLeft&&(M?E.scrollTo({left:s,behavior:"smooth"}):E.scrollLeft=s),M=!0})}updateExpandedSize()}if(z=()=>{g.currentIndex>=g.length-1||(g.currentIndex++,Y())},k=()=>{g.currentIndex<=0||(g.currentIndex--,Y())},Y(),ce&&A(),e.listeners.push({target:ae,event:"click",func:()=>z()},{target:re,event:"click",func:()=>k()},{target:document,event:"keydown",func:t=>{if("zoomed"!==e.state)switch(t.key){case"ArrowRight":case"ArrowDown":case"PageDown":case"d":case"s":z();break;case"ArrowLeft":case"ArrowUp":case"PageUp":case"a":case"w":k();break;case"Home":g.currentIndex=0,Y();break;case"End":g.currentIndex=g.length-1,Y();break;case"0":case"1":case"2":case"3":case"4":case"5":case"6":case"7":case"8":case"9":g.currentIndex=Math.min(Math.max(Number(t.key),1)-1,g.length-1),Y()}}},{target:document,event:"wheel",func:t=>{if("zoomed"===e.state)return;const n=performance.now();n-(T||0)<80||(t.deltaY>50?(T=n,z()):t.deltaY<-50&&(T=n,k()))},args:{passive:!0}}),E){let ue,de,fe,me,be,ve,he,ge,ye;function R(){ye&&(cancelAnimationFrame(ye),ye=null)}function B(){if(R(),Math.abs(ge)<.01)return;let e=performance.now();ye=requestAnimationFrame(function t(n){if(!E.isConnected)return void R();const o=E.scrollWidth-E.clientWidth;if(o<=0)return void R();const a=Math.min(32,n-e);e=n;let r=E.scrollLeft+ge*a;r<0&&(r=0),r>o&&(r=o),E.scrollLeft=r,E.scrollLeft<=.1&&ge<0||E.scrollLeft>=o-.1&&ge>0?R():(ge*=Math.pow(.8,a/16.67),Math.abs(ge)<=.002?R():ye=requestAnimationFrame(t))})}e.listeners.push({target:E,event:"pointerdown",func:e=>{if(0!==e.button)return;const t=E.scrollWidth-E.clientWidth;fe=t>0,R(),ue=!0,de=!1,me=e.clientX,be=E.scrollLeft,ve=E.scrollLeft,he=performance.now(),ge=0,fe&&E.classList.add("popupable-thumbnails-dragging"),E.setPointerCapture(e.pointerId)}},{target:E,event:"pointermove",func:e=>{if(!ue)return;const t=e.clientX-me;Math.abs(t)>3&&(de=!0);const n=performance.now(),o=n-he,a=be-t;if(E.scrollLeft=a,o>0){const e=(E.scrollLeft-ve)/o;ge=.65*ge+.35*e,ve=E.scrollLeft,he=n}}},{target:E,event:"pointerup",func:e=>{if(!ue)return;if(ue=!1,fe&&E.classList.remove("popupable-thumbnails-dragging"),E.hasPointerCapture(e.pointerId)&&E.releasePointerCapture(e.pointerId),de)return performance.now()-he>10&&(ge=0),void B();const t=document.elementFromPoint(e.clientX,e.clientY)?.closest?.(".popupable-thumbnail");t&&(g.currentIndex=Number(t.dataset.thumbnailIndex),Y())}},{target:E,event:"pointercancel",func:e=>{ue&&(ue=!1,fe&&E.classList.remove("popupable-thumbnails-dragging"),E.hasPointerCapture(e.pointerId)&&E.releasePointerCapture(e.pointerId),R())}},{target:E,event:"wheel",func:e=>{e.stopPropagation(),e.preventDefault();const t=E.scrollWidth-E.clientWidth;if(t<=0)return;const n=Math.abs(e.deltaX)>Math.abs(e.deltaY)?e.deltaX:e.deltaY,o=E.scrollLeft<=.1,a=E.scrollLeft>=t-.1;if(o&&n<0||a&&n>0)return;o&&n>0&&ge<0&&(ge=0),a&&n<0&&ge>0&&(ge=0);ge=(ge||0)+.015*n,ye||B()},args:{passive:!1}})}ce&&(e.listeners.push({target:ae,event:"pointerenter",func:()=>{pe=!0,S()}},{target:re,event:"pointerenter",func:()=>{pe=!0,S()}},{target:ae,event:"pointerleave",func:()=>{pe=!1,A()}},{target:re,event:"pointerleave",func:()=>{pe=!1,A()}}),e.listeners.push({target:y,event:"pointermove",func:t=>{if("zoomed"!==e.state)return null==le||null==se?(le=t.clientX,void(se=t.clientY)):void(Math.abs(t.clientX-le)<3&&Math.abs(t.clientY-se)<3||(le=t.clientX,se=t.clientY,S()))},args:{passive:!0}}));for(const Le of g)Le.content&&x&&x.append(Le.content)}else h&&x.append(h);const N={counter:!!P,content:!!x,thumbnails:!!E},F=m.order.top.filter(e=>N[e]),D=m.order.bottom.filter(e=>N[e]);function W(e,t){e&&("counter"===t&&P?(X[e===w?"counterTop":"counterBottom"]=!0,e.append(P)):"content"===t&&x?(X[e===w?"contentTop":"contentBottom"]=!0,e.append(x)):"thumbnails"===t&&E&&(X[e===w?"thumbnailsTop":"thumbnailsBottom"]=!0,e.append(E)))}F.length&&(w=document.createElement("div"),w.className="popupable-header"),D.length&&(C=document.createElement("div"),C.className="popupable-footer");for(const xe of F)W(w,xe);for(const we of D)W(C,we);if(w&&L.append(w),C&&L.append(C),y.append(f,L),Object.assign(d,m,{popup:y,group:g,contentContainer:x,thumbnailsContainer:E,orderPlacement:X,goNext:z,goPrev:k}),await d.ready,u!==o||e!==d||"close"===d.state)return;setCloneToOriginalRect(b,c),document.body.append(y),c.classList.add("popupable-hide"),disableScroll();const H=getComputedStyle(y);d.transition.duration=1e3*parseFloat(H.transitionDuration)+1e3*parseFloat(H.transitionDelay),y._state=d,requestAnimationFrame(()=>{requestAnimationFrame(()=>{openPopupable(y._state)})});let O,V=0;g&&y.addEventListener("dragstart",e=>e.preventDefault());const q=new Map;function $(e,t,n,o,r=[]){if("open"!==e.state)return;let i=0;const l=t.cloneContainer.parentElement,s=l.style.transform;if(s){const e=s.match(/translateX\((-?\d+(?:\.\d+)?)px\)/);e&&(i=Number(e[1])||0)}const p=Math.abs(i)>.5;a=!1,p?(t.cloneContainer.style.translate="0 0",t.cloneContainer.style.transition="translate var(--popupable-switch-duration), transform 0s",l.style.transition=null,l.style.transform=null,t.cloneContainer.style.translate=`${i}px 0`):(l.style.transition=null,l.style.transform=null),e.state="zoomed",y.classList.add("popupable-locked");let c,u,d=o;const f=new Map;let m,b,v,h,g,x,w,C,P,E,I=!1;const M=t.cloneContainer.getBoundingClientRect(),z=n?.clientX??M.left+M.width/2,k=n?.clientY??M.top+M.height/2;c=(z-M.left)*(1-d),u=(k-M.top)*(1-d);const T=()=>t.cloneContainer.style.transform=`translate(${c}px, ${u}px) scale(${d})`;function X(e,n,o){const a=d;var r;if(r=e,d=Math.min(6,Math.max(1,r)),d===a)return!1;const i=t.cloneContainer.getBoundingClientRect(),l=d/a,s=n-i.left,p=o-i.top;return c+=s*(1-l),u+=p*(1-l),!0}function A(){if(1===f.size){const e=f.values().next().value;return m=e.id,b=e.x,v=e.y,h=null,g=null,void(x=null)}if(f.size>=2){m=null;const[e,t]=[...f.values()];return h=(e.x+t.x)/2,g=(e.y+t.y)/2,void(x=Math.hypot(t.x-e.x,t.y-e.y))}m=null,b=null,v=null,h=null,g=null,x=null}if(t.cloneContainer.classList.add("popupable-zoomed"),T(),r.length){p||(t.cloneContainer.style.transition="none"),E=!0;for(const e of r)f.set(e.id,{id:e.id,x:e.x,y:e.y}),y.setPointerCapture(e.id);A()}e.unzoom=()=>{e.state="open",y.classList.remove("popupable-locked");for(const e of f.keys())y.hasPointerCapture(e)&&y.releasePointerCapture(e);f.clear(),t.cloneContainer.classList.remove("popupable-zoomed"),t.cloneContainer.style.transition=null,t.cloneContainer.style.transform=null,t.cloneContainer.style.translate=null;for(const t of e.zoomListeners)t.target.removeEventListener(t.event,t.func)},e.zoomListeners=[{target:y,event:"pointerdown",func:e=>{0===e.button&&(t.cloneContainer.style.transition="none",y.setPointerCapture(e.pointerId),f.set(e.pointerId,{id:e.pointerId,x:e.clientX,y:e.clientY}),1===f.size?(w=e.target,C=e.clientX,P=e.clientY,E=!1):E=!0,A(),e.preventDefault())}},{target:y,event:"pointermove",func:e=>{const t=f.get(e.pointerId);if(t){if(t.x=e.clientX,t.y=e.clientY,!E&&(Math.abs(e.clientX-C)>3||Math.abs(e.clientY-P)>3)&&(E=!0),1===f.size&&m===e.pointerId){const t=e.clientX-b,n=e.clientY-v;if(!t&&!n)return;return c+=t,u+=n,b=e.clientX,v=e.clientY,void T()}if(f.size>=2){const[e,t]=[...f.values()],n=(e.x+t.x)/2,o=(e.y+t.y)/2,a=Math.hypot(t.x-e.x,t.y-e.y);if(!x)return h=n,g=o,void(x=a);c+=n-h,u+=o-g,X(d*(a/x),n,o),I=!0,h=n,g=o,x=a,T()}}}},{target:y,event:"pointerup",func:n=>{if(f.has(n.pointerId)){if(f.delete(n.pointerId),y.hasPointerCapture(n.pointerId)&&y.releasePointerCapture(n.pointerId),I&&d<=1.01&&f.size<2)return e.skipOpenTouchPointerUps=f.size,void e.unzoom();if(!f.size&&!E&&Math.abs(n.clientX-C)<3&&Math.abs(n.clientY-P)<3){if(w?.closest?.(".popupable-clone-container")===t.cloneContainer||(w===y||w===L))return void e.unzoom()}A()}}},{target:y,event:"pointercancel",func:e=>{f.has(e.pointerId)&&(f.delete(e.pointerId),y.hasPointerCapture(e.pointerId)&&y.releasePointerCapture(e.pointerId),A())}},{target:y,event:"wheel",func:e=>{t.cloneContainer.style.transition="none",X(d*Math.exp(.002*-e.deltaY),e.clientX,e.clientY)&&T()},args:{passive:!0}}];for(const t of e.zoomListeners)t.target.addEventListener(t.event,t.func,t.args)}e.listeners.push({target:y,event:"pointerdown",func:e=>{if("open"!==y._state.state||"touch"!==e.pointerType)return;const t=y._state,n=t.group?t.group[t.group.currentIndex]:t;e.target.closest(".popupable-clone-container")===n.cloneContainer&&(q.set(e.pointerId,{id:e.pointerId,x:e.clientX,y:e.clientY}),q.size>=2&&($(t,n,e,1,[...q.values()].slice(0,2)),q.clear(),e.preventDefault()))}},{target:y,event:"pointermove",func:e=>{const t=q.get(e.pointerId);t&&(t.x=e.clientX,t.y=e.clientY)}},{target:y,event:"pointerup",func:e=>{q.delete(e.pointerId)}},{target:y,event:"pointercancel",func:e=>{q.delete(e.pointerId)}}),y.addEventListener("pointerup",t=>{if("zoomed"===y._state.state)return;if("touch"===t.pointerType&&l>1)return;if("touch"===t.pointerType&&(y._state.skipOpenTouchPointerUps||0)>0)return void y._state.skipOpenTouchPointerUps--;const o=performance.now(),a=null!=t.target.closest(".popupable-next-container, .popupable-prev-container");if(O&&o-V<250)return void(V=o);if(a?(O=!0,V=o):(O=!1,V=o),0!==t.button||!((t.target.classList.contains("popupable-clone")||t.target.classList.contains("popupable-clone-layer"))&&n.classList.contains("popupable-clone-container")||t.target==n&&(t.target.closest(".popupable-clone-container")||t.target.classList.contains("popupable-viewport")||t.target.classList.contains("popupable-container"))||t.target.classList.contains("popupable-container")&&n===e.original.parentElement))return;const r=y._state,i=r.group?r.group[r.group.currentIndex]:r;r.blocked&&(r.blocked=!1),"open"!==r.state?(t.stopPropagation(),e!==r&&(closePopupable(),e=r),openPopupable(e)):requestAnimationFrame(()=>{r.blocked?r.blocked=!1:i.zoomable&&(t.target.classList.contains("popupable-clone")||t.target.classList.contains("popupable-clone-layer"))?$(r,i,t,2):closePopupable()})})}),document.addEventListener("pointercancel",e=>{"touch"===e.pointerType&&(l=Math.max(0,l-1))}),document.addEventListener("keydown",t=>{if("Escape"===t.key||"Backspace"===t.key||" "===t.key||"Delete"===t.key){if("zoomed"===e.state)return void e.unzoom();closePopupable()}}),window.addEventListener("resize",updateExpandedSize),visualViewport&&visualViewport.addEventListener("resize",updateExpandedSize)}
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "popupable",
3
+ "version": "1.0.0",
4
+ "description": "A lightweight, zero-dependency lightbox library using modern JavaScript and CSS.",
5
+ "author": "Ewan Howell <ewanhowell5195>",
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "main": "./dist/popupable.min.js",
9
+ "style": "./dist/popupable.min.css",
10
+ "exports": {
11
+ ".": "./dist/popupable.min.js",
12
+ "./styles.css": "./dist/popupable.min.css"
13
+ },
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/ewanhowell5195/popupable.git"
20
+ },
21
+ "bugs": {
22
+ "url": "https://github.com/ewanhowell5195/popupable/issues"
23
+ },
24
+ "homepage": "https://github.com/ewanhowell5195/popupable#readme",
25
+ "scripts": {
26
+ "build": "node build.js"
27
+ },
28
+ "devDependencies": {
29
+ "clean-css": "^5.3.3",
30
+ "terser": "^5.43.1"
31
+ }
32
+ }