1/* ============================================================ 2 LIGHTBOX.JS â ÐолноÑкÑаннÑй пÑоÑмоÑÑÑик Ñ FLIP-анимаÑией 3 4 ÐÑполÑзование: 5 const lightbox = new Lightbox({ 6 trigger: '.show__gallery-photo', // ÑелекÑÐ¾Ñ ÐºÐ»Ð¸ÐºÐ°Ð±ÐµÐ»ÑнÑÑ ÑоÑо 7 container: document, // где иÑкаÑÑ ÑÑиггеÑÑ 8 title: 'ÐÑевÑаÑение', // обÑий заголовок 9 imageAttr: 'data-src', // пÑÑÑ Ðº full-res 10 captionAttr:'data-caption', // подпиÑÑ (кÑо на ÑоÑо) 11 authorAttr: 'data-author', // авÑÐ¾Ñ ÑоÑо 12 }); 13 14 Ðа каждом ÑлеменÑе-ÑÑиггеÑе: 15 <div class="show__gallery-photo" 16 data-src="path/to/full.jpg" 17 data-caption="Ðнна Ðванова в Ñоли ÐамÑ" 18 data-author="ФоÑо: Ðван ÐеÑÑов"> 19 <img src="path/to/thumb.jpg"> 20 </div> 21 22 ТÑÐ¸Ð³Ð³ÐµÑ Ñам должен ÑодеÑжаÑÑ <img> внÑÑÑи â именно Ñ Ð½ÐµÐ³Ð¾ JS 23 ÑÐ½Ð¸Ð¼ÐµÑ Ð³ÐµÐ¾Ð¼ÐµÑÑÐ¸Ñ Ð´Ð»Ñ FLIP-анимаÑии. 24 25 ÐавиÑимоÑÑи: window.TagankaPage.lockScroll/unlockScroll. 26 27 УпÑавление: 28 â клик по ÑÑиггеÑÑ â оÑкÑÑÐ²Ð°ÐµÑ Ð»Ð°Ð¹ÑÐ±Ð¾ÐºÑ Ð½Ð° ÑÑом ÑоÑо 29 â клик по кÑеÑÑÐ¸ÐºÑ / Esc / клик по backdrop â закÑÑÐ²Ð°ÐµÑ 30 â ÑÑÑелки â â¶ / клавиÑи â/â â пеÑелиÑÑÑвание 31 â гоÑизонÑалÑнÑй Ñвайп â пеÑелиÑÑÑвание 32 ============================================================ */ 33 34(function () { 35 'use strict'; 36 37 const FLIP_DURATION = 350; // Ð¼Ñ 38 const FLIP_EASING = 'cubic-bezier(0.25, 1, 0.5, 1)'; 39 const SWIPE_THRESHOLD = 50; // px 40 const OPEN_PRELOAD_TIMEOUT = 1200; // мÑ: ждÑм full-ÑоÑо пеÑед FLIP, поÑом fallback на thumb 41 const SWITCH_PRELOAD_TIMEOUT = 1200; // мÑ: пÑи ÑÑÑÐµÐ»ÐºÐ°Ñ Ð½Ðµ показÑваем lazy/blur-плейÑÑ Ð¾Ð»Ð´ÐµÑ 42 43 /* ââ SVG иконки (inline, ÑÑÐ¾Ð±Ñ Ð½Ðµ завиÑеÑÑ Ð¾Ñ Ð°ÑÑеÑов) ââ */ 44 const ICON_CLOSE = '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" aria-hidden="true">' + 45 '<line x1="2" y1="2" x2="22" y2="22" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>' + 46 '<line x1="22" y1="2" x2="2" y2="22" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>' + 47 '</svg>'; 48 49 const ICON_PREV = '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">' + 50 '<line x1="14" y1="8" x2="2" y2="8"></line>' + 51 '<polyline points="7 3 2 8 7 13"></polyline>' + 52 '</svg>'; 53 54 const ICON_NEXT = '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">' + 55 '<line x1="2" y1="8" x2="14" y2="8"></line>' + 56 '<polyline points="9 3 14 8 9 13"></polyline>' + 57 '</svg>'; 58 59 60 class Lightbox { 61 constructor(opts) { 62 this._title = opts.title || ''; 63 this._imageAttr = opts.imageAttr || 'data-src'; 64 this._captionAttr = opts.captionAttr || 'data-caption'; 65 this._authorAttr = opts.authorAttr || 'data-author'; 66 this._videoAttr = opts.videoAttr || 'data-video'; 67 68 const root = opts.container || document; 69 this._triggers = Array.prototype.slice.call( 70 root.querySelectorAll(opts.trigger) 71 ); 72 73 if (!this._triggers.length) return; 74 75 this._currentIndex = 0; 76 this._isOpen = false; 77 this._isAnimating = false; 78 this._flybackEl = null; 79 this._closeTimer = null; 80 this._switchTimer = null; 81 this._openToken = 0; 82 this._switchToken = 0; 83 this._preloadCache = Object.create(null); 84 85 this._buildDOM(); 86 this._bindTriggers(); 87 this._bindKeyboard(); 88 this._warmupFullImages(); 89 } 90 91 /* ââ 1. ÐоÑÑÑоение overlay-DOM ââ */ 92 _buildDOM() { 93 const el = document.createElement('div'); 94 el.className = 'lightbox'; 95 el.setAttribute('hidden', ''); 96 el.setAttribute('role', 'dialog'); 97 el.setAttribute('aria-modal', 'true'); 98 el.setAttribute('aria-label', 'ÐÑоÑмоÑÑ ÑоÑогÑаÑии'); 99 100 el.innerHTML = ` 101 <div class="lightbox__backdrop"></div> 102 <div class="lightbox__stage"> 103 <img class="lightbox__image" alt=""> 104 </div> 105 <button type="button" class="lightbox__close" aria-label="ÐакÑÑÑÑ">${ICON_CLOSE}</button> 106 <button type="button" class="lightbox__nav lightbox__nav--prev" aria-label="ÐÑедÑдÑÑее ÑоÑо">${ICON_PREV}</button> 107 <button type="button" class="lightbox__nav lightbox__nav--next" aria-label="СледÑÑÑее ÑоÑо">${ICON_NEXT}</button> 108 <div class="lightbox__caption"> 109 <p class="lightbox__counter"></p> 110 <div class="lightbox__title-block"> 111 <p class="lightbox__title"></p> 112 <p class="lightbox__info"></p> 113 </div> 114 </div> 115 `; 116 117 document.body.appendChild(el); 118 119 this._el = el; 120 this._backdrop = el.querySelector('.lightbox__backdrop'); 121 this._stage = el.querySelector('.lightbox__stage'); 122 this._image = el.querySelector('.lightbox__image'); 123 this._closeBtn = el.querySelector('.lightbox__close'); 124 this._prevBtn = el.querySelector('.lightbox__nav--prev'); 125 this._nextBtn = el.querySelector('.lightbox__nav--next'); 126 this._counterEl = el.querySelector('.lightbox__counter'); 127 this._titleEl = el.querySelector('.lightbox__title'); 128 this._infoEl = el.querySelector('.lightbox__info'); 129 130 this._closeBtn.addEventListener('click', () => this.close()); 131 this._backdrop.addEventListener('click', () => this.close()); 132 this._prevBtn.addEventListener('click', () => this.prev()); 133 this._nextBtn.addEventListener('click', () => this.next()); 134 135 /* iOS Safari: подавлÑем bubbling touch'а до коÑÐ½Ñ Ð»Ð°Ð¹ÑбокÑа 136 (где виÑÐ¸Ñ swipe-обÑабоÑÑик), ÑÑÐ¾Ð±Ñ Ñап по кнопке не ÑÑиÑалÑÑ 137 наÑалом Ñвайпа. Backdrop ÐРвклÑÑаем â на нÑм Ñвайп должен ловиÑÑÑÑ. */
138 [this._closeBtn, this._prevBtn, this._nextBtn].forEach((node) => { 139 node.addEventListener('touchstart', (e) => e.stopPropagation(), { passive: true }); 140 node.addEventListener('touchend', (e) => e.stopPropagation(), { passive: true }); 141 }); 142 143 this._bindSwipe(); 144 } 145 146 /* ââ 2. Ðлик по миниаÑÑÑам ââ */ 147 _bindTriggers() { 148 this._triggers.forEach((t, idx) => { 149 t.style.cursor = 'pointer'; 150 if (!t.hasAttribute('tabindex')) t.setAttribute('tabindex', '0'); 151 if (!t.hasAttribute('role')) t.setAttribute('role', 'button'); 152 if (!t.hasAttribute('aria-label')) { 153 t.setAttribute('aria-label', 'ÐÑкÑÑÑÑ ÑоÑо на веÑÑ ÑкÑан'); 154 } 155 156 t.addEventListener('click', (e) => { 157 e.preventDefault(); 158 this.open(idx); 159 }); 160 161 t.addEventListener('keydown', (e) => { 162 if (e.key === 'Enter' || e.key === ' ') { 163 e.preventDefault(); 164 this.open(idx); 165 } 166 }); 167 }); 168 } 169 170 /* ââ 3. ÐлавиаÑÑÑа ââ */ 171 _bindKeyboard() { 172 this._onKey = (e) => { 173 if (!this._isOpen) return; 174 if (e.key === 'Escape') { e.preventDefault(); this.close(); } 175 else if (e.key === 'ArrowLeft') { e.preventDefault(); this.prev(); } 176 else if (e.key === 'ArrowRight') { e.preventDefault(); this.next(); } 177 }; 178 document.addEventListener('keydown', this._onKey); 179 } 180 181 /* ââ 4. Свайп ââ */ 182 _bindSwipe() { 183 let startX = null; 184 let startY = null; 185 186 /* Swipe ловим на коÑневом .lightbox â он полÑÑÐ°ÐµÑ Ð²Ñе touch. 187 .lightbox__stage Ñ pointer-events: none не полÑÑÐ°ÐµÑ ÑобÑÑиÑ. */ 188 this._el.addEventListener('touchstart', (e) => { 189 if (e.touches.length !== 1) return; 190 startX = e.touches[0].clientX; 191 startY = e.touches[0].clientY; 192 }, { passive: true }); 193 194 this._el.addEventListener('touchend', (e) => { 195 if (startX === null) return; 196 const dx = e.changedTouches[0].clientX - startX; 197 const dy = e.changedTouches[0].clientY - startY; 198 startX = null; 199 startY = null; 200 201 /* ÐгноÑиÑÑем веÑÑикалÑнÑе жеÑÑÑ (ÑкÑолл), 202 ÑеагиÑÑем ÑолÑко на гоÑизонÑалÑнÑе. */ 203 if (Math.abs(dx) < SWIPE_THRESHOLD) return; 204 if (Math.abs(dy) > Math.abs(dx)) return; 205 206 if (dx > 0) this.prev(); 207 else this.next(); 208 }); 209 } 210 211 /* ââ 5. ÐÑкÑÑÑие Ñ FLIP ââ */ 212 async open(index) { 213 if (this._isAnimating || this._isOpen) return; 214 215 const trigger = this._triggers[index]; 216 if (!trigger) return; 217 218 const openToken = ++this._openToken; 219 this._switchToken++; 220 this._currentIndex = index; 221 this._isAnimating = true; 222 223 this._el.classList.remove('is-open', 'is-settled', 'is-loading-full', 'is-closing'); 224 if (this._closeTimer) { 225 clearTimeout(this._closeTimer); 226 this._closeTimer = null; 227 } 228 if (this._flybackEl) { 229 this._flybackEl.remove(); 230 this._flybackEl = null; 231 } 232 233 this._image.style.transition = 'none'; 234 this._image.style.opacity = '1'; 235 this._image.style.visibility = ''; 236 this._image.style.filter = ''; 237 238 const isVideo = this._isVideoTrigger(trigger); 239 const triggerMedia = isVideo 240 ? trigger.querySelector('video') 241 : trigger.querySelector('img'); 242 const fromRect = (triggerMedia || trigger).getBoundingClientRect(); 243 244 /* Ðо ÑÑаÑÑа FLIP гоÑовим именно full-Ñайл, еÑли он доÑÑÑпен. 245 РанÑÑе лайÑÐ±Ð¾ÐºÑ ÑÑаÑÑовал thumb-каÑÑинкой и ÑÑиÑал ÑиналÑнÑй 246 ÑÐ°Ð·Ð¼ÐµÑ Ð¿Ð¾ thumb 4:3. ÐÑли full имел дÑÑгÑÑ Ð¿ÑопоÑÑÐ¸Ñ Ð¸Ð»Ð¸ еÑÑ 247 гÑÑзилÑÑ, внÑÑÑи поÑвлÑлÑÑ blur/lazy-Ñон и визÑалÑнÑй Ñдвиг 248 по вÑÑоÑе. ТепеÑÑ target ÑÑиÑаеÑÑÑ Ð¿Ð¾ ÑеалÑÐ½Ð¾Ð¼Ñ full natural 249 size; thumb â ÑолÑко аваÑийнÑй fallback без blur. */ 250 const media = await this._resolveOpenMedia(trigger, triggerMedia); 251 if (openToken !== this._openToken) { 252 this._isAnimating = false;
253 return; 254 } 255 256 this._isOpen = true; 257 258 /* СкÑÑваем иÑÑ Ð¾Ð´Ð½Ð¸Ðº ÑолÑко когда Ñже гоÑÐ¾Ð²Ñ Ð¾ÑкÑÑÑÑ overlay. 259 Так пÑи медленном full-load полÑзоваÑÐµÐ»Ñ Ð½Ðµ Ð²Ð¸Ð´Ð¸Ñ Ð¿ÑÑÑÑÑ Ð´ÑÑÐºÑ 260 в галеÑее до поÑÐ²Ð»ÐµÐ½Ð¸Ñ Ð»Ð°Ð¹ÑбокÑа. */ 261 trigger.classList.add('lightbox-source--active'); 262 this._pauseTileVideo(trigger); 263 264 this._el.removeAttribute('hidden'); 265 this._image.alt = media.alt || ''; 266 267 if (window.TagankaPage && window.TagankaPage.lockScroll) { 268 window.TagankaPage.lockScroll(); 269 } 270 271 /* iOS Safari поÑле lockScroll Ð¼Ð¾Ð¶ÐµÑ Ð¼ÐµÐ½ÑÑÑ ÑакÑиÑеÑкÑÑ Ð²ÑÑоÑÑ 272 fixed-overlay не в ÑÐ¾Ñ Ð¶Ðµ layout tick. Ðва кадÑа даÑÑ stage 273 ÑÑабилÑнÑй getBoundingClientRect(), поÑÑÐ¾Ð¼Ñ ÑоÑо не âплаваеÑâ 274 вÑÑе/ниже Ð¼ÐµÐ¶Ð´Ñ Ð¾ÑкÑÑÑиÑми. */ 275 await this._nextFrame(); 276 await this._nextFrame(); 277 if (openToken !== this._openToken || !this._isOpen) { 278 this._isAnimating = false; 279 return; 280 } 281 282 void this._el.offsetWidth; 283 284 const target = this._calcTargetRect(media.measureMedia || triggerMedia); 285 286 this._image.style.transition = 'none'; 287 this._image.style.width = fromRect.width + 'px'; 288 this._image.style.height = fromRect.height + 'px'; 289 this._image.style.left = fromRect.left + 'px'; 290 this._image.style.top = fromRect.top + 'px'; 291 this._image.style.opacity = '1'; 292 if (media.displaySrc) { 293 this._image.src = media.displaySrc; 294 } else { 295 this._image.removeAttribute('src'); 296 } 297 this._el.classList.remove('is-loading-full'); 298 299 this._maybeSwapFallbackToFull(media, index, openToken); 300 301 /* Forced reflow â гаÑанÑиÑÑÐµÑ ÑÑо бÑаÑÐ·ÐµÑ Ð·Ð°ÐºÐ¾Ð¼Ð¼Ð¸ÑÐ¸Ñ Ð½Ð°ÑалÑное ÑоÑÑоÑние 302 до пÑÐ¸Ð¼ÐµÐ½ÐµÐ½Ð¸Ñ transition. Ðа iOS Safari двойной rAF не вÑегда ÑÑабаÑÑваеÑ. */ 303 void this._image.offsetWidth; 304 305 this._el.classList.add('is-open'); 306 307 this._image.style.transition = 308 'width ' + FLIP_DURATION + 'ms ' + FLIP_EASING + ', ' + 309 'height ' + FLIP_DURATION + 'ms ' + FLIP_EASING + ', ' + 310 'left ' + FLIP_DURATION + 'ms ' + FLIP_EASING + ', ' + 311 'top ' + FLIP_DURATION + 'ms ' + FLIP_EASING; 312 this._image.style.width = target.w + 'px'; 313 this._image.style.height = target.h + 'px'; 314 this._image.style.left = target.x + 'px'; 315 this._image.style.top = target.y + 'px'; 316 317 const fallbackTimeout = setTimeout(() => { 318 this._image.removeEventListener('transitionend', onEnd); 319 this._finishOpen(); 320 }, FLIP_DURATION + 140); 321 322 const onEnd = (e) => { 323 if (e.propertyName !== 'left') return; 324 this._image.removeEventListener('transitionend', onEnd); 325 clearTimeout(fallbackTimeout); 326 this._finishOpen(); 327 }; 328 this._image.addEventListener('transitionend', onEnd); 329 330 this._renderCaption(); 331 } 332 333 _finishOpen() { 334 if (!this._isOpen) return; 335 336 /* Снимаем ÑолÑко transition, ÐСТÐÐÐЯÐÐ inline width/height/left/top. 337 ÐÑо знаÑÐ¸Ñ ÐºÐ°ÑÑинка оÑÑаÑÑÑÑ Ð² position: absolute Ñ Ñеми же 338 ÑазмеÑами ÑÑо поÑле FLIP. Ðез пÑÑжка в natural-ÑÐ°Ð·Ð¼ÐµÑ ÐºÐ°ÑÑинки. */ 339 this._image.style.transition = ''; 340 this._el.classList.add('is-settled'); 341 this._isAnimating = false; 342 343 if (this._isVideoTrigger(this._triggers[this._currentIndex])) { 344 this._mountVideo(); 345 } 346 } 347 348 /* ЦелевÑе кооÑдинаÑÑ Ð´Ð»Ñ open()/switch(): макÑималÑнÑй 349 пÑÑмоÑголÑник внÑÑÑи безопаÑной Ð·Ð¾Ð½Ñ Ð¿Ð¾ наÑÑÑалÑной пÑопоÑÑии full-медиа. */ 350 _calcTargetRect(srcMedia) {
351 /* Ðе иÑполÑзÑем visualViewport.height и не полагаемÑÑ Ð½Ð° stageRect.height 352 как оÑновной иÑÑоÑник: в Safari они могÑÑ Ð¼ÐµÐ½ÑÑÑÑÑ Ð¿Ñи ÑÑ Ð»Ð¾Ð¿Ñвании 353 адÑеÑной ÑÑÑоки/поÑле lockScroll. documentElement.clientHeight даÑÑ 354 более ÑÑабилÑнÑÑ Ð²ÑÑоÑÑ Ð´Ð»Ñ fixed overlay, поÑÑÐ¾Ð¼Ñ ÑоÑо болÑÑе не 355 âгÑлÑеÑâ ввеÑÑ -вниз Ð¼ÐµÐ¶Ð´Ñ Ð¾ÑкÑÑÑиÑми. */ 356 const stageRect = this._stage 357 ? this._stage.getBoundingClientRect() 358 : { left: 0, top: 0, width: 0, height: 0 }; 359 360 const vw = document.documentElement.clientWidth || stageRect.width || window.innerWidth || 1; 361 const vh = document.documentElement.clientHeight || stageRect.height || window.innerHeight || 1; 362 363 const isMobile = vw < 1024; 364 const maxW = Math.max(1, vw - (isMobile ? 20 : 200)); 365 366 const zoneTop = isMobile ? 64 : 100; 367 const zoneBottom = isMobile ? 140 : 100; 368 const zoneH = Math.max(1, vh - zoneTop - zoneBottom); 369 370 const naturalW = (srcMedia && (srcMedia.naturalWidth || srcMedia.videoWidth || srcMedia.width)) || 4; 371 const naturalH = (srcMedia && (srcMedia.naturalHeight || srcMedia.videoHeight || srcMedia.height)) || 3; 372 const ratio = naturalW > 0 && naturalH > 0 ? naturalW / naturalH : 4 / 3; 373 374 let w = maxW; 375 let h = w / ratio; 376 if (h > zoneH) { 377 h = zoneH; 378 w = h * ratio; 379 } 380 381 const x = (vw - w) / 2; 382 const y = zoneTop + (zoneH - h) / 2; 383 return { x, y, w, h }; 384 } 385 386 /* ââ 6. ÐакÑÑÑие Ñ Ð¿Ð»Ð°Ð²Ð½Ñм fly-back к миниаÑÑÑе ââ */ 387 close() { 388 if (!this._isOpen || this._isAnimating) return; 389 this._openToken++; 390 this._switchToken++; 391 this._closeWithFlyback(); 392 } 393 394 _closeWithFlyback() { 395 const trigger = this._triggers[this._currentIndex]; 396 if (!trigger) { 397 this._isAnimating = true; 398 this._isOpen = false; 399 this._finishClose(null); 400 return; 401 } 402 403 /* Ðидео ÑбиÑаем ÑÑÐ°Ð·Ñ â fly-back леÑÐ¸Ñ Ð¿Ð¾Ñледним кадÑом 404 (this._image), как и пÑи обÑÑном оÑкÑÑÑии. */ 405 this._teardownVideo(); 406 407 /* Ð¡Ð¸Ð½Ñ ÑонизиÑÑем гоÑизонÑалÑнÑй ÑкÑолл до ÑаÑÑÑÑа toRect. 408 ÐнаÑе пÑи закÑÑÑии поÑле пеÑелиÑÑÑÐ²Ð°Ð½Ð¸Ñ ÐºÐ°ÑÑинка Ð¼Ð¾Ð¶ÐµÑ Ð»ÐµÑеÑÑ 409 в кооÑдинаÑÑ ÐºÐ°ÑÑоÑки, коÑоÑÐ°Ñ Ñже ÑаÑÑиÑно ÑÑла за viewport. */ 410 this._scrollIntoViewSync(trigger); 411 412 const triggerMedia = this._getTriggerMedia(trigger); 413 const fromRect = this._image.getBoundingClientRect(); 414 const toRect = (triggerMedia || trigger).getBoundingClientRect(); 415 416 const prefersReducedMotion = window.matchMedia && 417 window.matchMedia('(prefers-reduced-motion: reduce)').matches; 418 419 if (prefersReducedMotion || 420 !fromRect.width || !fromRect.height || 421 !toRect.width || !toRect.height) { 422 this._isAnimating = true; 423 this._isOpen = false; 424 this._finishClose(trigger); 425 return; 426 } 427 428 this._isAnimating = true; 429 this._isOpen = false; 430 this._el.classList.add('is-closing'); 431 432 const flyback = document.createElement('div'); 433 flyback.className = 'lightbox-flyback'; 434 flyback.setAttribute('aria-hidden', 'true'); 435 Object.assign(flyback.style, { 436 left: fromRect.left + 'px', 437 top: fromRect.top + 'px', 438 width: fromRect.width + 'px', 439 height: fromRect.height + 'px' 440 }); 441 442 const fullClone = this._createFlybackMediaClone(this._image, 'full'); 443 const thumbClone = this._createFlybackMediaClone(triggerMedia, 'thumb'); 444 flyback.appendChild(fullClone); 445 flyback.appendChild(thumbClone); 446 document.body.appendChild(flyback); 447 this._flybackEl = flyback; 448 449 /* РеалÑнÑй lightbox-media пÑÑÑем ÑÑазÑ: далÑÑе полÑзоваÑÐµÐ»Ñ Ð²Ð¸Ð´Ð¸Ñ 450 ÑолÑко flyback-клон, в коÑоÑом full contain плавно ÑменÑеÑÑÑ 451 на thumb cover. ÐÑо ÑбиÑÐ°ÐµÑ ÑкаÑок из-за ÑазнÑÑ Ð¿ÑопоÑÑий. */ 452 this._image.style.visibility = 'hidden'; 453 454 this._el.classList.remove('is-settled'); 455 this._el.classList.remove('is-loading-full'); 456 this._el.classList.remove('is-open'); 457 458 /* ÐоммиÑим ÑÑаÑÑовÑÑ Ð³ÐµÐ¾Ð¼ÐµÑÑÐ¸Ñ Ð´Ð¾ назнаÑÐµÐ½Ð¸Ñ transition. */ 459 void flyback.offsetWidth; 460 461 requestAnimationFrame(() => { 462 requestAnimationFrame(() => { 463 if (this._flybackEl !== flyback) return; 464 465 flyback.classList.add('is-ending'); 466 Object.assign(flyback.style, { 467 transition: 468 'left ' + FLIP_DURATION + 'ms ' + FLIP_EASING + ', ' + 469 'top ' + FLIP_DURATION + 'ms ' + FLIP_EASING + ', ' + 470 'width ' + FLIP_DURATION + 'ms ' + FLIP_EASING + ', ' + 471 'height ' + FLIP_DURATION + 'ms ' + FLIP_EASING + ', ' + 472 'opacity 220ms ease', 473 left: toRect.left + 'px', 474 top: toRect.top + 'px', 475 width: toRect.width + 'px', 476 height: toRect.height + 'px' 477 }); 478 }); 479 }); 480 481 let isDone = false;
482 let onEnd = null; 483 484 const done = () => { 485 if (isDone) return; 486 isDone = true; 487 488 if (this._closeTimer) { 489 clearTimeout(this._closeTimer); 490 this._closeTimer = null; 491 } 492 if (onEnd) { 493 flyback.removeEventListener('transitionend', onEnd); 494 } 495 496 this._finishClose(trigger); 497 }; 498 499 onEnd = (e) => { 500 if (e.target !== flyback) return; 501 if (e.propertyName !== 'left' && 502 e.propertyName !== 'top' && 503 e.propertyName !== 'width' && 504 e.propertyName !== 'height') return; 505 done(); 506 }; 507 508 flyback.addEventListener('transitionend', onEnd); 509 this._closeTimer = setTimeout(done, FLIP_DURATION + 120); 510 } 511 512 _getTriggerMedia(trigger) { 513 if (!trigger) return null; 514 return trigger.querySelector('img') || trigger.querySelector('video'); 515 } 516 517 _createFlybackMediaClone(sourceMedia, mode) { 518 let src = ''; 519 520 if (sourceMedia && sourceMedia.tagName === 'VIDEO') { 521 src = this._grabVideoFrame(sourceMedia) || sourceMedia.getAttribute('poster') || ''; 522 } else if (sourceMedia && sourceMedia.tagName === 'IMG') { 523 src = sourceMedia.currentSrc || 524 sourceMedia.getAttribute('src') || 525 sourceMedia.src || 526 ''; 527 } 528 529 const clone = src ? document.createElement('img') : document.createElement('span'); 530 clone.className = 'lightbox-flyback__media lightbox-flyback__media--' + mode; 531 clone.setAttribute('aria-hidden', 'true'); 532 533 if (src) { 534 clone.src = src; 535 clone.alt = ''; 536 clone.draggable = false; 537 clone.decoding = 'async'; 538 } 539 540 return clone; 541 } 542 543 _finishClose(triggerToFocus) { 544 this._openToken += 1; 545 546 if (this._switchTimer) { 547 clearTimeout(this._switchTimer); 548 this._switchTimer = null; 549 } 550 551 if (this._closeTimer) { 552 clearTimeout(this._closeTimer); 553 this._closeTimer = null; 554 } 555 556 if (this._flybackEl) { 557 this._flybackEl.remove(); 558 this._flybackEl = null; 559 } 560 561 this._teardownVideo(); 562 563 this._triggers.forEach((t) => { 564 t.classList.remove('lightbox-source--active'); 565 }); 566 567 this._el.classList.remove('is-open', 'is-settled', 'is-loading-full', 'is-closing'); 568 this._el.setAttribute('hidden', ''); 569 570 this._image.style.transition = ''; 571 this._image.style.width = ''; 572 this._image.style.height = ''; 573 this._image.style.left = ''; 574 this._image.style.top = ''; 575 this._image.style.opacity = ''; 576 this._image.style.visibility = ''; 577 this._image.removeAttribute('src'); 578 this._image.alt = ''; 579 580 this._isAnimating = false; 581 this._isOpen = false; 582 583 if (window.TagankaPage && window.TagankaPage.unlockScroll) { 584 window.TagankaPage.unlockScroll(); 585 } 586 587 if (triggerToFocus && triggerToFocus.focus) { 588 try { 589 triggerToFocus.focus({ preventScroll: true }); 590 } catch (e) { 591 triggerToFocus.focus(); 592 } 593 } 594 } 595 596 /* ââ 7. ÐеÑелиÑÑÑвание ââ */ 597 prev() { 598 if (this._isAnimating) return; 599 const newIndex = (this._currentIndex - 1 + this._triggers.length) % this._triggers.length; 600 this._switchTo(newIndex); 601 } 602 603 next() { 604 if (this._isAnimating) return; 605 const newIndex = (this._currentIndex + 1) % this._triggers.length; 606 this._switchTo(newIndex); 607 } 608 609 /* ÐÑоÑÑое пеÑеклÑÑение Ñ fade: full-ÑоÑо ÑнаÑала гоÑовиÑÑÑ, 610 поÑом невидимо менÑеÑÑÑ src + геомеÑÑиÑ. Так не поÑвлÑеÑÑÑ 611 blur/lazy-Ñон и не видно ÑкаÑка пÑи ÑазнÑÑ Ð¿ÑопоÑÑиÑÑ ÑоÑо. */ 612 _switchTo(newIndex) { 613 if (this._isAnimating) return; 614 615 const oldTrigger = this._triggers[this._currentIndex]; 616 const newTrigger = this._triggers[newIndex]; 617 if (!newTrigger) return; 618 619 const switchToken = ++this._switchToken; 620 this._isAnimating = true; 621 622 this._teardownVideo(); 623 624 if (oldTrigger) oldTrigger.classList.remove('lightbox-source--active'); 625 newTrigger.classList.add('lightbox-source--active'); 626 this._pauseTileVideo(newTrigger); 627 628 this._currentIndex = newIndex; 629 this._scrollIntoViewSync(newTrigger); 630 631 const triggerMedia = this._isVideoTrigger(newTrigger) 632 ? newTrigger.querySelector('video') 633 : newTrigger.querySelector('img'); 634 635 /* Ðе гаÑим ÑÑаÑое ÑоÑо до подгоÑовки нового: инаÑе пÑи медленном 636 full-load полÑзоваÑÐµÐ»Ñ Ð²Ð¸Ð´Ð¸Ñ ÑÑÑнÑй Ñон/плейÑÑ Ð¾Ð»Ð´ÐµÑ. */ 637 this._resolveOpenMedia(newTrigger, triggerMedia, SWITCH_PRELOAD_TIMEOUT) 638 .then((media) => { 639 if (!this._isOpen || this._currentIndex !== newIndex || this._switchToken !== switchToken) { 640 this._isAnimating = false;
641 return; 642 } 643 644 this._image.style.transition = 'opacity ' + (FLIP_DURATION / 2) + 'ms ease'; 645 this._image.style.opacity = '0'; 646 647 setTimeout(() => { 648 if (!this._isOpen || this._currentIndex !== newIndex || this._switchToken !== switchToken) { 649 this._isAnimating = false; 650 return; 651 } 652 653 const target = this._calcTargetRect(media.measureMedia || triggerMedia); 654 655 this._image.style.transition = 'none'; 656 this._image.style.width = target.w + 'px'; 657 this._image.style.height = target.h + 'px'; 658 this._image.style.left = target.x + 'px'; 659 this._image.style.top = target.y + 'px'; 660 this._image.src = media.displaySrc || ''; 661 this._image.alt = media.alt || ''; 662 this._image.style.opacity = '0'; 663 this._el.classList.remove('is-loading-full'); 664 this._renderCaption(); 665 666 this._maybeSwapFallbackToFull(media, newIndex, this._openToken); 667 668 void this._image.offsetWidth; 669 670 this._image.style.transition = 'opacity ' + (FLIP_DURATION / 2) + 'ms ease'; 671 this._image.style.opacity = '1'; 672 673 if (this._isVideoTrigger(newTrigger)) { 674 this._mountVideo(); 675 } 676 677 setTimeout(() => { 678 if (this._switchToken !== switchToken) return; 679 this._image.style.transition = ''; 680 this._isAnimating = false; 681 }, (FLIP_DURATION / 2) + 40); 682 }, FLIP_DURATION / 2); 683 }) 684 .catch(() => { 685 this._isAnimating = false; 686 }); 687 } 688 689 690 691/* ÐгновеннÑй scrollIntoView ÑеÑез scrollLeft ÑодиÑелÑ. 692 Ðа iOS Safari наÑивнÑй scrollIntoView({ behavior: 'instant' }) 693 Ð¼Ð¾Ð¶ÐµÑ Ð¸Ð³Ð½Ð¾ÑиÑоваÑÑÑÑ. ÐдÑм по ÑодиÑелÑм ввеÑÑ Ð¸ Ð´Ð»Ñ ÐºÐ°Ð¶Ð´Ð¾Ð³Ð¾ 694 horizontal-scroll-конÑейнеÑа вÑÑиÑлÑем нÑжнÑй scrollLeft. */ 695 _scrollIntoViewSync(el) { 696 let parent = el.parentElement; 697 while (parent && parent !== document.body) { 698 const cs = window.getComputedStyle(parent); 699 const overflowX = cs.overflowX; 700 if (overflowX === 'auto' || overflowX === 'scroll') { 701 const elRect = el.getBoundingClientRect(); 702 const parRect = parent.getBoundingClientRect(); 703 /* ÐÑли ÑÐ»ÐµÐ¼ÐµÐ½Ñ ÑпÑава за Ñамкой â доÑкÑоллиÑÑ Ð²Ð¿Ñаво */ 704 if (elRect.right > parRect.right) { 705 parent.scrollLeft += (elRect.right - parRect.right); 706 } 707 /* ÐÑли Ñлева â доÑкÑоллиÑÑ Ð²Ð»ÐµÐ²Ð¾ */ 708 else if (elRect.left < parRect.left) { 709 parent.scrollLeft -= (parRect.left - elRect.left); 710 } 711 } 712 parent = parent.parentElement; 713 } 714 } 715 716 717 _nextFrame() { 718 return new Promise((resolve) => requestAnimationFrame(resolve)); 719 } 720 721 _getMediaSrc(media) { 722 if (!media) return ''; 723 if (media.tagName === 'IMG') { 724 return media.currentSrc || media.getAttribute('src') || media.src || ''; 725 } 726 if (media.tagName === 'VIDEO') { 727 return this._grabVideoFrame(media) || media.getAttribute('poster') || ''; 728 } 729 return ''; 730 } 731 732 _loadImage(src) { 733 if (!src) return Promise.resolve(null);
734 if (this._preloadCache[src]) return this._preloadCache[src]; 735 736 this._preloadCache[src] = new Promise((resolve) => { 737 const img = new Image(); 738 let done = false; 739 740 const finish = (value) => { 741 if (done) return; 742 done = true; 743 resolve(value); 744 }; 745 746 img.decoding = 'async'; 747 if ('fetchPriority' in img) { 748 img.fetchPriority = 'high'; 749 } 750 751 img.onload = () => { 752 if (img.decode) { 753 img.decode().then(() => finish(img)).catch(() => finish(img)); 754 } else { 755 finish(img); 756 } 757 }; 758 img.onerror = () => finish(null); 759 img.src = src; 760 761 if (img.complete && img.naturalWidth) { 762 if (img.decode) { 763 img.decode().then(() => finish(img)).catch(() => finish(img)); 764 } else { 765 finish(img); 766 } 767 } 768 }); 769 770 return this._preloadCache[src]; 771 } 772 773 _withTimeout(promise, timeoutMs) { 774 return new Promise((resolve) => { 775 let settled = false; 776 const timer = setTimeout(() => { 777 if (settled) return; 778 settled = true; 779 resolve(null); 780 }, timeoutMs); 781 782 promise.then((value) => { 783 if (settled) return; 784 settled = true; 785 clearTimeout(timer); 786 resolve(value); 787 }).catch(() => { 788 if (settled) return; 789 settled = true; 790 clearTimeout(timer); 791 resolve(null); 792 }); 793 }); 794 } 795 796 async _resolveOpenMedia(trigger, triggerMedia, timeoutMs) { 797 const isVideo = this._isVideoTrigger(trigger); 798 799 if (isVideo) { 800 const frameSrc = this._grabVideoFrame(triggerMedia); 801 return { 802 displaySrc: frameSrc, 803 measureMedia: triggerMedia, 804 alt: '', 805 fullReady: true, 806 fullSrc: frameSrc, 807 pendingFullPromise: null 808 }; 809 } 810 811 const thumbSrc = this._getMediaSrc(triggerMedia); 812 const fullSrc = trigger.getAttribute(this._imageAttr) || thumbSrc; 813 const alt = (triggerMedia && triggerMedia.alt) || ''; 814 815 if (!fullSrc || fullSrc === thumbSrc) { 816 return { 817 displaySrc: thumbSrc, 818 measureMedia: triggerMedia, 819 alt, 820 fullReady: true, 821 fullSrc: thumbSrc, 822 pendingFullPromise: null 823 }; 824 } 825 826 const pendingFullPromise = this._loadImage(fullSrc); 827 const fullImg = await this._withTimeout(pendingFullPromise, timeoutMs || OPEN_PRELOAD_TIMEOUT); 828 829 if (fullImg && fullImg.naturalWidth && fullImg.naturalHeight) { 830 return { 831 displaySrc: fullSrc, 832 measureMedia: fullImg, 833 alt, 834 fullReady: true, 835 fullSrc, 836 pendingFullPromise 837 }; 838 } 839 840 return { 841 displaySrc: thumbSrc, 842 measureMedia: triggerMedia, 843 alt, 844 fullReady: false, 845 fullSrc, 846 pendingFullPromise 847 }; 848 } 849 850 _maybeSwapFallbackToFull(media, index, openToken) { 851 if (!media || media.fullReady || !media.pendingFullPromise || !media.fullSrc) return; 852 853 media.pendingFullPromise.then((img) => { 854 if (!img || !img.naturalWidth || !img.naturalHeight) return; 855 if (!this._isOpen || this._currentIndex !== index || this._openToken !== openToken) return; 856 857 /* ÐÑли full пÑиÑÑл позже fallback'а и Ñ Ð½ÐµÐ³Ð¾ дÑÑÐ³Ð°Ñ Ð¿ÑопоÑÑиÑ, 858 не подменÑем его в ÑÑаÑÑÑ 4:3-ÑамкÑ. Ðменно ÑÐ°ÐºÐ°Ñ Ð¿Ð¾Ð·Ð´Ð½ÑÑ 859 подмена давала оÑÑÑение, ÑÑо ÑоÑо âплаваеÑâ внÑÑÑи лайÑбокÑа. 860 Full Ñже окажеÑÑÑ Ð² cache и оÑкÑоеÑÑÑ ÐºÐ¾ÑÑекÑно Ñо ÑледÑÑÑего Ñаза. */ 861 const oldW = media.measureMedia && (media.measureMedia.naturalWidth || media.measureMedia.videoWidth || media.measureMedia.width); 862 const oldH = media.measureMedia && (media.measureMedia.naturalHeight || media.measureMedia.videoHeight || media.measureMedia.height); 863 const oldRatio = oldW && oldH ? oldW / oldH : 4 / 3;
864 const newRatio = img.naturalWidth / img.naturalHeight; 865 if (Math.abs(oldRatio - newRatio) > 0.01) return; 866 867 this._image.src = media.fullSrc; 868 }).catch(() => {}); 869 } 870 871 _warmupFullImages() { 872 const run = () => { 873 this._triggers.forEach((trigger) => { 874 if (this._isVideoTrigger(trigger)) return; 875 const fullSrc = trigger.getAttribute(this._imageAttr); 876 if (fullSrc) this._loadImage(fullSrc); 877 }); 878 }; 879 880 if ('requestIdleCallback' in window) { 881 window.requestIdleCallback(run, { timeout: 1600 }); 882 } else { 883 setTimeout(run, 600); 884 } 885 } 886 887 888 /* ââ Ðидео-Ñлайд ââ 889 ТÑÐ¸Ð³Ð³ÐµÑ = видео, еÑли на нÑм еÑÑÑ data-video. FLIP леÑÐ¸Ñ 890 кадÑом (this._image), как Ñ ÑоÑо â open()/close()/_switchTo 891 не менÑÑÑÑÑ. ÐоÑле settled повеÑÑ ÐºÐ°Ð´Ñа монÑиÑÑеÑÑÑ 892 <video controls autoplay>. */ 893 _isVideoTrigger(trigger) { 894 return !!(trigger && trigger.getAttribute(this._videoAttr)); 895 } 896 897 /* ÐаÑза плиÑоÑного <video> ÑÑиггеÑа, пока он оÑкÑÑÑ Ð² лайÑбокÑе. 898 visibility:hidden Ð¾Ñ lightbox-source--active ÐРоÑÑÐ°Ð½Ð°Ð²Ð»Ð¸Ð²Ð°ÐµÑ 899 воÑпÑоизведение (извеÑÑнÑй пÑинÑип пÑоекÑа) â глÑÑим Ñ
899вно, 900 ÑÑÐ¾Ð±Ñ Ð½Ðµ бÑло двойного звÑка ни в одном бÑаÑзеÑе. */ 901 _pauseTileVideo(trigger) { 902 if (!trigger) return; 903 const tv = trigger.querySelector('video'); 904 if (tv && !tv.paused) { 905 try { tv.pause(); } catch (e) { /* noop */ } 906 } 907 } 908 909 /* Снимаем ÑекÑÑий ÐºÐ°Ð´Ñ <video> плиÑки в <canvas> â data-URL. 910 ÐÑо «обложка» Ð´Ð»Ñ FLIP â оÑделÑной каÑÑинки неÑ. 911 ÐÑли ÐºÐ°Ð´Ñ ÐµÑÑ Ð½Ðµ гоÑов (metadata не загÑÑжена) â веÑнÑÑÑÑ 912 пÑÑÑÐ°Ñ ÑÑÑока, FLIP ÑÑаÑÑÑÐµÑ Ð±ÐµÐ· кадÑа (Ñон ÑÑÑнÑй), 913 заÑем видео подмениÑ. */ 914 _grabVideoFrame(video) { 915 if (!video) return ''; 916 const w = video.videoWidth; 917 const h = video.videoHeight; 918 if (!w || !h) return ''; 919 try { 920 const canvas = document.createElement('canvas'); 921 canvas.width = w; 922 canvas.height = h; 923 canvas.getContext('2d').drawImage(video, 0, 0, w, h); 924 return canvas.toDataURL('image/jpeg', 0.85); 925 } catch (e) { 926 /* CORS-tainted (видео Ñ Ð´ÑÑгого домена) â ÐºÐ°Ð´Ñ ÑнÑÑÑ Ð½ÐµÐ»ÑзÑ. 927 FLIP ÑÑаÑÑÑÐµÑ Ð±ÐµÐ· кадÑа, видео Ð¿Ð¾Ð´Ð¼ÐµÐ½Ð¸Ñ Ð¿Ð¾Ñле settled. */ 928 return ''; 929 } 930 } 931 932 /* <video controls autoplay> повеÑÑ settled-кадÑа, в ÑÐµÑ Ð¶Ðµ 933 inline-кооÑдинаÑÐ°Ñ ÑÑо this._image. Со звÑком. Ðо оконÑании â 934 ÑÑоп-ÐºÐ°Ð´Ñ (loop вÑклÑÑен, currentTime ÐÐ ÑбÑаÑÑваем). */ 935 _mountVideo() { 936 const trigger = this._triggers[this._currentIndex]; 937 if (!this._isVideoTrigger(trigger)) return; 938 939 const src = trigger.getAttribute(this._videoAttr); 940 if (!src) return; 941 942 this._teardownVideo(); 943 944 const v = document.createElement('video'); 945 v.className = 'lightbox__video'; 946 v.preload = 'auto'; 947 948 /* iOS Safari ÑÐ²Ð¾Ð´Ð¸Ñ Ð²Ð¸Ð´ÐµÐ¾ в наÑивнÑй полноÑкÑаннÑй Ð¿Ð»ÐµÐµÑ Ð¿Ñи 949 play(), еÑли playsinline не вÑÑÑавлен ÐÐÐ HTML-ÐТРÐÐУТ. 950 DOM-ÑвойÑÑво v.playsInline на iOS ненадÑжно (ÑÐ¾Ñ Ð¶Ðµ клаÑÑ 951 бага, ÑÑо v.hidden на SVG) â ÑÑавим именно аÑÑибÑÑÑ. 952 webkit-playsinline нÑжен Ð´Ð»Ñ ÑÑаÑÑÑ iOS. controls Ñоже 953 аÑÑибÑÑом â Ð´Ð»Ñ ÐµÐ´Ð¸Ð½Ð¾Ð¾Ð±ÑазиÑ. */ 954 v.setAttribute('playsinline', ''); 955 v.setAttribute('webkit-playsinline', ''); 956 v.setAttribute('controls', ''); 957 v.playsInline = true; // дÑблиÑÑем ÑвойÑÑвом â не меÑÐ°ÐµÑ 958 v.controls = true; 959 960 /* src назнаÑаем ÐÐСÐÐ playsinline: на iOS иÑÑоÑник, заданнÑй 961 до аÑÑибÑÑа inline, в ÑаÑÑи веÑÑий вÑÑ Ñавно оÑкÑÑваеÑÑÑ 962 ÑÑллÑкÑином. */ 963 v.src = src; 964 965 /* СовмеÑаем геомеÑÑÐ¸Ñ Ñ ÐºÐ°Ð´Ñом: Ñе же inline left/top/width/height, 966 ÑÑо JS вÑÑÑавил на this._image поÑле FLIP. */ 967 v.style.left = this._image.style.left; 968 v.style.top = this._image.style.top; 969 v.style.width = this._image.style.width; 970 v.style.height = this._image.style.height; 971 972 this._stage.appendChild(v); 973 this._video = v; 974 975 /* iOS Safari: touch по наÑивнÑм конÑÑолам (ÑкÑаббинг, гÑомкоÑÑÑ) 976 не должен ÑÑиÑаÑÑÑÑ Ñвайпом лайÑбокÑа. ÐаÑим bubbling до 977 коÑнÑ, где виÑÐ¸Ñ swipe-обÑабоÑÑик. */ 978 v.addEventListener('touchstart', (e) => e.stopPropagation(), { passive: true }); 979 v.addEventListener('touchend', (e) => e.stopPropagation(), { passive: true }); 980 981 /* Fade-in повеÑÑ ÐºÐ°Ð´Ñа, когда гоÑов пеÑвÑй кадÑ. 982 loadeddata надÑжнее canplay на iOS. Fallback по ÑаймеÑÑ. */ 983 const reveal = () => { 984 if (this._video !== v) return; 985 v.classList.add('is-ready'); 986 }; 987 v.addEventListener('loadeddata', reveal, { once: true }); 988 this._videoRevealTimer = setTimeout(reveal, 600); 989 990 /* ÐвÑозапÑÑк Ñо звÑком. ÐÑли бÑаÑÐ·ÐµÑ Ð·Ð°Ð±Ð»Ð¾ÐºÐ¸Ñовал autoplay 991 Ñ Ð°Ñдио (полиÑика) â пÑобÑем без звÑка; полÑзоваÑÐµÐ»Ñ 992 вклÑÑÐ¸Ñ Ð·Ð²Ñк наÑивнÑм конÑÑолом. */ 993 v.play().catch(() => { 994 if (this._video !== v) return; 995 v.muted = true; 996 v.play().catch(() => { /* полÑзоваÑÐµÐ»Ñ Ð·Ð°Ð¿ÑÑÑÐ¸Ñ Ñам ÑеÑез controls */ }); 997 }); 998 } 999 1000 _teardownVideo() { 1001 if (this._videoRevealTimer) { 1002 clearTimeout(this._videoRevealTimer); 1003 this._videoRevealTimer = null; 1004 } 1005 if (this._video) { 1006 try { 1007 this._video.pause(); 1008 this._video.removeAttribute('src'); 1009 this._video.load(); 1010 } catch (e) { /* noop */ } 1011 if (this._video.parentNode) { 1012 this._video.parentNode.removeChild(this._video); 1013 } 1014 this._video = null; 1015 } 1016 } 1017 1018 1019 /* ââ 8. ÐодпиÑÑ ââ */ 1020 _renderCaption() { 1021 const trigger = this._triggers[this._currentIndex]; 1022 const total = this._triggers.length; 1023 1024 this._counterEl.textContent = (this._currentIndex + 1) + ' / ' + total; 1025 this._titleEl.textContent = this._title; 1026 1027 const caption = trigger.getAttribute(this._captionAttr) || ''; 1028 const author = trigger.getAttribute(this._authorAttr) || '';
1029 const info = [caption, author].filter(Boolean).join(' · '); 1030 this._infoEl.textContent = info; 1031 } 1032 1033 destroy() { 1034 this._teardownVideo(); 1035 if (this._closeTimer) { 1036 clearTimeout(this._closeTimer); 1037 this._closeTimer = null; 1038 } 1039 if (this._switchTimer) { 1040 clearTimeout(this._switchTimer); 1041 this._switchTimer = null; 1042 } 1043 if (this._flybackEl) { 1044 this._flybackEl.remove(); 1045 this._flybackEl = null; 1046 } 1047 document.removeEventListener('keydown', this._onKey); 1048 if (this._el && this._el.parentNode) { 1049 this._el.parentNode.removeChild(this._el); 1050 } 1051 } 1052 } 1053 1054 window.Lightbox = Lightbox; 1055})();
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.