PageSourceSearch

https://3dnews.ru/assets/js/hires-lightbox.js?v=4

js 3dnews.ru collected 2026-09-24 08:29:04 UTC 7,695 bytes, 130 lines download raw bytes

1// hires-lightbox.js — просмотрщик хайрезов и галерей для публикаций 3dnews (PhotoSwipe v5, ленивая загрузка).
2// Прогрессивное улучшение: без модуля / при сбое v5 — <a href> открывает исходник как раньше.
3// Галереи (.adaptive-gallery, формат get_hgallery_new): data-size уже в разметке → прелоад не нужен.
4// Легаси-форматы .gallery-am-block (montage) и .gallery-box (2013) НЕ перехватываем — там свои вьюеры.
5(function () {
6  var IMG_RE = /\.(jpe?g|png|gif|webp|avif)(\?|#|$)/i;
7  var BASE = '/assets/js/photoswipe5/';
8  var BODY_SEL = '.entry-body, [itemprop="articleBody"]';
9  var pswPromise = null, cssPromise = null, busy = false;
10
11  function ensureCss() {                                  // грузим css и резолвимся по его onload
12    if (cssPromise) return cssPromise;
13    var st = document.createElement('style');             // ТЗ: одиночные без кнопок → зум скрыт; у галерей (решение DesT 2026-08-27) зум/капшен/счётчик остаются
14    st.textContent =
15      '.pswp.pswp--hires .pswp__button--zoom{display:none}' +
16      '.pswp.pswp--hires.pswp--gal .pswp__button--zoom{display:block}' +
17      '.pswp__gal-caption{position:absolute;left:0;right:0;bottom:0;padding:12px 20px;color:#fff;font-size:15px;line-height:1.35;text-align:center;text-shadow:0 1px 2px #000;pointer-events:none}';
18    document.head.appendChild(st);
19    cssPromise = new Promise(function (res) {
20      var l = document.createElement('link'); l.rel = 'stylesheet'; l.href = BASE + 'photoswipe.css';
21      l.onload = l.onerror = function () { res(); };      // не блокируем даже если css не долетел
22      document.head.appendChild(l);
23    });
24    return cssPromise;
25  }
26  function loadPsw() {
27    if (!pswPromise) pswPromise = import(BASE + 'photoswipe.esm.min.js').then(function (m) { return m.default; })
28      ['catch'](function (e) { pswPromise = null; throw e; });   // не кешируем провал импорта — транзиентный сбой не убивает лайтбокс на сессию
29    return pswPromise;
30  }
31  // fallbackHref: если v5/css не загрузятся или init кинет — уходим на исходник (не мёртвый клик)
32  function openSingle(item, fallbackHref) {
33    Promise.all([ ensureCss(), loadPsw() ]).then(function (r) {   // ЖДЁМ CSS до init — иначе первый показ без стилей: картинки вываливаются в поток статьи
34      new r[1]({
35        dataSource: [item], index: 0, mainClass: 'pswp--hires',
36        bgOpacity: 0.9, showHideAnimationType: 'fade', clickToCloseNonZoomable: true
37      }).init();
38      busy = false;
39    })['catch'](function () { busy = false; if (fallbackHref) window.location.href = fallbackHref; });
40  }
41  // Галерея: items = [{src,width,height,msrc,title}], стрелки/зум из коробки, капшен и счётчик — свои элементы
42  function openGallery(items, index, fallbackHref) {
43    Promise.all([ ensureCss(), loadPsw() ]).then(function (r) {
44      var PhotoSwipe = r[1];
45      var pswp = new PhotoSwipe({
46        dataSource: items, index: index, mainClass: 'pswp--hires pswp--gal',
47        bgOpacity: 0.9, showHideAnimationType: 'fade', clickToCloseNonZoomable: true
48      });
49      var captionEl = null;                               // счётчик — ВСТРОЕННЫЙ v5 (.pswp__counter), свой НЕ добавлять (2026-08-27: двоилось)
50      function updateMeta() {
51        if (captionEl) {
52          var t = items[pswp.currIndex] && items[pswp.currIndex].title || '';
53          captionEl.textContent = t;
54          captionEl.style.display = t ? '' : 'none';
55        }
56      }
57      pswp.on('uiRegister', function () {
58        pswp.ui.registerElement({
59          name: 'gal-caption', className: 'pswp__gal-caption', appendTo: 'root',
60          onInit: function (el) { captionEl = el; updateMeta(); }
61        });
62      });
63      pswp.on('change', updateMeta);
64      pswp.init();
65      busy = false;
66    })['catch'](function () { busy = false; if (fallbackHref) window.location.href = fallbackHref; });
67  }
68  function preloadOpen(src, msrc, fallbackHref) {
69    document.body.style.cursor = 'progress';
70    var full = new Image();
71    full.onload = function () { document.body.style.cursor = ''; openSingle({ src: src, width: full.naturalWidth, height: full.naturalHeight, msrc: msrc }, fallbackHref); };
72    full.onerror = function () { document.body.style.cursor = ''; busy = false; if (fallbackHref) window.location.href = fallbackHref; };
73    full.src = src;
74  }
75  // Собрать items галереи из разметки: все <a data-size> (включая скрытые превью n>3)
76  function galleryItems(gal) {
77    var anchors = gal.querySelectorAll('a[data-size]'), items = [];
78    for (var i = 0; i < anchors.length; i++) {
79      var a = anchors[i], m = /(\d+)\s*x\s*(\d+)/i.exec(a.getAttribute('data-size') || '');
80      if (!m) continue;
81      var img = a.querySelector('img'), fig = a.querySelector('figure');
82      items.push({
83        src: a.href, width: +m[1], height: +m[2],
84        msrc: img ? (img.currentSrc || img.src) : undefined,
85        title: fig ? fig.textContent.replace(/^\s+|\s+$/g, '') : ''
86      });
87    }
88    return items;
89  }
90  function onClick(e) {
91    if (busy) return;
92    var t = e.target;
93    if (!t || !t.closest) return;
94    if (!t.closest(BODY_SEL)) return;                     // только тело публикации (3dnews/download .entry-body, servernews .pubtext[itemprop=articleBody])
95    if (t.closest('.gallery-am-block, .gallery-box')) return;  // легаси-форматы: свои вьюеры, не перехватываем
96
97    // --- галерея формата A ---
98    var gal = t.closest('.adaptive-gallery');
99    if (gal) {
100      var items = galleryItems(gal);
101      if (!items.length) return;
102      var index = 0, fallback = items[0].src;
103      if (!t.closest('.openGallery')) {                   // "Смотреть все изображения" → всегда с первого
104        var a = t.closest('a[data-size]');
105        if (a) {
106          var anchors = gal.querySelectorAll('a[data-size]');
107          for (var i = 0; i < anchors.length; i++) if (anchors[i] === a) { index = i; break; }
108          fallback = a.href;
109        }
110      }
111      e.preventDefault(); busy = true; openGallery(items, index, fallback); return;
112    }
113
114    // --- одиночная картинка ---
115    var img = t.closest('img');
116    if (!img) return;
117    var msrc = img.currentSrc || img.src;
118    var a = img.closest('a');
119    if (a) {
120      var href = a.getAttribute('href') || '';
121      if (!IMG_RE.test(href)) return;                     // ссылка не на картинку — не мешаем навигации
122      e.preventDefault(); busy = true; preloadOpen(a.href, msrc, a.href); return;
123    }
124    e.preventDefault(); busy = true;                      // голая <img>
125    if (img.complete && img.naturalWidth) openSingle({ src: img.src, width: img.naturalWidth, height: img.naturalHeight, msrc: msrc }, null);
126    else preloadOpen(img.src, msrc, null);
127  }
128  // document-level делегирование: покрывает AJAX-подгрузку (uncut/infinity) без перепривязки
129  document.addEventListener('click', onClick);
130})();

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.