1/** 2 * AMA Lightbox 3 * --------------------------------------------------------------------------- 4 * Zero-dependency lightbox with no per-image markup requirements. 5 * 6 * Trigger rule: an <a> whose href points at an image file, wrapping a single 7 * <img> directly, located inside one of CONFIG.scopes. That is exactly what 8 * the block editor emits for "Link to â Media file", so editors get lightboxes 9 * without touching anything. 10 * 11 * Grouping: all qualifying links inside the same scope element form a gallery 12 * (arrows, keyboard, swipe). Override with data-lightbox-group="name" to group 13 * links across scopes, or data-lightbox-group="none" to isolate one. 14 * 15 * Captions: data-caption on the link, else a sibling <figcaption>. 16 * 17 * Labels come from window.amaLightboxL10n (wp_localize_script) so they run 18 * through the theme's trans() layer. 19 * 20 * Public API: window.amaLightbox.open(linkEl) / .close() 21 */ 22(function () { 23 'use strict'; 24 25 var CONFIG = { 26 scopes: ['.editor-content', '.item__content-editor', '.is-item__media'], 27 imageExt: /\.(jpe?g|png|gif|webp|avif|svg)(\?.*)?$/i, 28 swipeThreshold: 50, // px before a drag counts as a swipe 29 lockClass: 'ama-lightbox-lock' 30 }; 31 32 var L10N = window.amaLightboxL10n || {}; 33 34 function t(key, fallback) { 35 return L10N[key] || fallback; 36 } 37 38 var dialog = null; 39 var imgEl = null; 40 var captionEl = null; 41 var counterEl = null; 42 var prevBtn = null; 43 var nextBtn = null; 44 var frameEl = null; 45 var group = []; 46 var index = 0; 47 var lastFocus = null; 48 var supportsModal = typeof HTMLDialogElement === 'function' && 49 typeof HTMLDialogElement.prototype.showModal === 'function'; 50 51 function scopeSelector() { 52 return CONFIG.scopes.join(','); 53 } 54 55 /* An <a> that links straight to an image file and wraps nothing but one 56 <img>
56. Anything else â a link with text, a link to a PDF, a link around 57 a card â is left alone. */ 58 function isImageLink(a) { 59 if (!a || a.tagName !== 'A') { 60 return false; 61 } 62 63 var href = a.getAttribute('href') || ''; 64 65 if (!CONFIG.imageExt.test(href)) { 66 return false; 67 } 68 69 if (a.children.length !== 1) { 70 return false; 71 } 72 73 return a.firstElementChild && a.firstElementChild.tagName === 'IMG'; 74 } 75 76 function inScope(a) { 77 return a.closest(scopeSelector()); 78 } 79 80 function collect(a) { 81 var name = a.dataset.lightboxGroup; 82 83 if (name === 'none') { 84 return [a]; 85 } 86 87 var pool; 88 89 if (name) { 90 pool = document.querySelectorAll('[data-lightbox-group="' + name + '"]'); 91 } else { 92 var scope = inScope(a); 93 pool = scope ? scope.querySelectorAll('a[href]') : [a]; 94 } 95 96 var links = Array.prototype.filter.call(pool, isImageLink); 97 98 return links.length ? links : [a]; 99 } 100 101 function caption(a) { 102 if (a.dataset.caption) { 103 return a.dataset.caption; 104 } 105 106 var figure = a.closest('figure'); 107 var cap = figure ? figure.querySelector('figcaption') : null; 108 109 return cap ? cap.textContent.trim() : ''; 110 } 111 112 function button(className, label, path) { 113 var btn = document.createElement('button'); 114 115 btn.type = 'button'; 116 btn.className = className; 117 btn.setAttribute('aria-label', label); 118 btn.innerHTML = '<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">' + 119 '<path d="' + path + '"/></svg>'; 120 121 return btn; 122 } 123 124 function build() { 125 if (dialog) { 126 return; 127 } 128 129 dialog = document.createElement(supportsModal ? 'dialog' : 'div'); 130 dialog.className = 'ama-lightbox'; 131
132 if (!supportsModal) { 133 dialog.setAttribute('role', 'dialog'); 134 dialog.setAttribute('aria-modal', 'true'); 135 dialog.hidden = true; 136 } 137 138 frameEl = document.createElement('div'); 139 frameEl.className = 'ama-lightbox__frame'; 140 141 imgEl = document.createElement('img'); 142 imgEl.className = 'ama-lightbox__img'; 143 imgEl.alt = ''; 144 145 captionEl = document.createElement('p'); 146 captionEl.className = 'ama-lightbox__caption'; 147 148 counterEl = document.createElement('p'); 149 counterEl.className = 'ama-lightbox__counter'; 150 151 var closeBtn = button('ama-lightbox__btn ama-lightbox__close', 152 t('close', 'Close'), 'M6 6l12 12M18 6L6 18'); 153 prevBtn = button('ama-lightbox__btn ama-lightbox__prev', 154 t('prev', 'Previous'), 'M15 5l-7 7 7 7'); 155 nextBtn = button('ama-lightbox__btn ama-lightbox__next', 156 t('next', 'Next'), 'M9 5l7 7-7 7'); 157 158 frameEl.appendChild(imgEl); 159 frameEl.appendChild(captionEl); 160 161 dialog.appendChild(closeBtn); 162 dialog.appendChild(prevBtn); 163 dialog.appendChild(nextBtn); 164 dialog.appendChild(frameEl); 165 dialog.appendChild(counterEl); 166 167 document.body.appendChild(dialog); 168 169 closeBtn.addEventListener('click', close); 170 prevBtn.addEventListener('click', function () { step(-1); }); 171 nextBtn.addEventListener('click', function () { step(1); }); 172 173 /* Clicking the empty area around the image closes. */ 174 dialog.addEventListener('click', function (e) { 175 if (e.target === dialog || e.target === frameEl) { 176 close(); 177 } 178 }); 179 180 dialog.addEventListener('cancel', function (e) { 181 e.preventDefault(); 182 close(); 183 }); 184 185 bindSwipe(); 186 } 187 188 function show(i) { 189 var total = group.length; 190 191 index = (i + total) % total; 192 193 var link = group[index]; 194 var text = caption(link); 195 var thumb = link.querySelector('img'); 196 197 dialog.classList.add('is-loading'); 198 199 var loader = new Image(); 200 201 loader.onload = function () { 202 if (group[index] !== link) { 203 return; // navigated on while this was loading 204 } 205 206 imgEl.src = loader.src; 207 imgEl.alt = thumb ? thumb.alt : ''; 208 dialog.classList.remove('is-loading'); 209 preloadNeighbours(); 210 }; 211 loader.onerror = function () { 212 dialog.classList.remove('is-loading'); 213 }; 214 loader.src = link.href; 215 216 captionEl.textContent = text; 217 captionEl.hidden = !text; 218 219 counterEl.textContent = total > 1 ? (index + 1) + ' / ' + total : ''; 220 counterEl.hidden = total < 2; 221 prevBtn.hidden = total < 2; 222 nextBtn.hidden = total < 2; 223 } 224 225 function preloadNeighbours() { 226 if (group.length < 2) { 227 return; 228 } 229 230 [-1, 1].forEach(function (offset) { 231 var next = group[(index + offset + group.length) % group.length]; 232 var pre = new Image(); 233 pre.src = next.href; 234 }); 235 } 236 237 function step(delta) { 238 if (group.length > 1) { 239 show(index + delta); 240 } 241 } 242 243 function open(link) { 244 build(); 245 246 lastFocus = document.activeElement; 247 group = collect(link); 248 index = Math.max(0, group.indexOf(link)); 249 250 document.documentElement.classList.add(CONFIG.lockClass); 251 252 if (supportsModal) { 253 dialog.showModal(); 254 } else { 255 dialog.hidden = false; 256 dialog.classList.add('is-open'); 257 } 258 259 show(index); 260 dialog.querySelector('.ama-lightbox__close').focus(); 261 } 262 263 function close() { 264 if (!dialog) { 265 return; 266 } 267 268 document.documentElement.classList.remove(CONFIG.lockClass); 269 270 if (supportsModal) { 271 if (dialog.open) { 272 dialog.close(); 273 } 274 } else { 275 dialog.hidden = true; 276 dialog.classList.remove('is-open'); 277 } 278 279 imgEl.removeAttribute('src'); 280 group = []; 281 282 if (lastFocus && typeof lastFocus.focus === 'function') { 283 lastFocus.focus(); 284 } 285 lastFocus = null; 286 } 287 288 function isOpen() { 289 return dialog && (supportsModal ? dialog.open : !dialog.hidden); 290 } 291 292 /* Horizontal swipe changes image, downward swipe closes. */ 293 function bindSwipe() { 294 var startX = 0; 295 var startY = 0; 296 var tracking = false; 297 298 frameEl.addEventListener('touchstart', function (e) { 299 if (e.touches.length !== 1) { 300 tracking = false; 301 return; 302 } 303 tracking = true; 304 startX = e.touches[0].clientX; 305 startY = e.touches[0].clientY; 306 }, { passive: true }); 307 308 frameEl.addEventListener('touchend', function (e) { 309 if (!tracking) { 310 return; 311 } 312 tracking = false;
313 314 var touch = e.changedTouches[0]; 315 var dx = touch.clientX - startX; 316 var dy = touch.clientY - startY; 317 318 if (Math.abs(dx) > Math.abs(dy)) { 319 if (Math.abs(dx) > CONFIG.swipeThreshold) { 320 step(dx < 0 ? 1 : -1); 321 } 322 return; 323 } 324 325 if (dy > CONFIG.swipeThreshold) { 326 close(); 327 } 328 }); 329 } 330 331 document.addEventListener('click', function (e) { 332 /* Let modified clicks open in a new tab as the visitor expects. */ 333 if (e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) { 334 return; 335 } 336 337 var link = e.target.closest ? e.target.closest('a[href]') : null; 338 339 if (!link || !isImageLink(link) || !inScope(link)) { 340 return; 341 } 342 343 e.preventDefault(); 344 open(link); 345 }); 346 347 document.addEventListener('keydown', function (e) { 348 if (!isOpen()) { 349 return; 350 } 351 352 if (e.key === 'ArrowLeft') { 353 e.preventDefault(); 354 step(-1); 355 } else if (e.key === 'ArrowRight') { 356 e.preventDefault(); 357 step(1); 358 } else if (e.key === 'Escape' && !supportsModal) { 359 e.preventDefault(); 360 close(); 361 } 362 }); 363 364 window.amaLightbox = { open: open, close: close, config: CONFIG }; 365})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.