PageSourceSearch

https://mvroddom.ru/templates/all/all.js?v1.2

js mvroddom.ru collected 2026-09-28 22:56:08 UTC 19,048 bytes, 400 lines download raw bytes

1(function () {
2  if (customElements.get('site-carousel')) return;
3
4  var BLANK = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==';
5
6  class SiteCarousel extends HTMLElement {
7    connectedCallback() {
8      // --- 1. Считываем слайды из light DOM (то, что положил редактор/XSLT) ---
9      // Поддерживаются: <a><img></a>, <a><video></a>, <video> без ссылки
10      var nodes = Array.prototype.slice.call(this.children).filter(function (el) {
11        var t = el.tagName.toLowerCase();
12        return t === 'a' || t === 'video';
13      });
14
15      this._slides = nodes.map(function (el) {
16        var isLink   = el.tagName.toLowerCase() === 'a';
17        var media    = isLink ? el.querySelector('video, img') : el;
18        if (!media) return null;
19        var isVideo  = media.tagName.toLowerCase() === 'video';
20        var interval = parseInt(el.getAttribute('data-interval') ||
21                                media.getAttribute('data-interval'), 10);
22        return {
23          type:     isVideo ? 'video' : 'image',
24          href:     isLink ? (el.getAttribute('href') || '') : '',
25          target:   isLink ? (el.getAttribute('target') || '_self') : '_self',
26          rel:      isLink ? (el.getAttribute('rel') || '') : '',
27          src:      media.getAttribute('src') || '',
28          poster:   media.getAttribute('poster') || '',
29          thumb:    media.getAttribute('thumb') || media.getAttribute('poster') || '',
30          alt:      media.getAttribute('alt') || media.getAttribute('data-alt') ||
31                    (isLink ? (el.getAttribute('aria-label') || '') : ''),
32          interval: isNaN(interval) ? null : interval
33        };
34      }).filter(function (s) { return s && s.src; });
35
36      if (!this._slides.length) return;
37
38      this._interval = parseInt(this.getAttribute('interval'), 10);
39      if (isNaN(this._interval)) this._interval = 5000;
40      this._perView  = parseInt(this.getAttribute('thumbs'), 10) || 4;
41      this._index    = 0;
42      this._timer    = null;
43      this._videos   = {};   // индекс слайда -> <video> в shadow DOM
44      this._soundOn  = false;
45
46      // уважение к системной настройке "меньше движения"
47      this._reduceMotion = window.matchMedia &&
48        window.matchMedia('(prefers-reduced-motion: reduce)').matches;
49
50      this._render();
51      this._bind();
52      this._updateSound();
53      this._start();
54
55      this.style.setProperty('--sc-hidden', '0'); // флаг, что JS отработал
56    }
57
58    disconnectedCallback() { this._stop(); }
59
60    // ----------------------------------------------------------------- //
61    _render() {
62      var root = this.attachShadow({ mode: 'open' });
63
64      var slidesHtml = this._slides.map(function (s, i) {
65        var tag   = s.href ? 'a' : 'div';
66        var attrs = 'class="slide' + (i === 0 ? ' is-active' : '') + '" '
67                  + 'data-i="' + i + '" aria-hidden="' + (i === 0 ? 'false' : 'true') + '"';
68        if (s.href) {
69          attrs += ' href="' + esc(s.href) + '" target="' + esc(s.target) + '"'
70                 + (s.rel ? ' rel="' + esc(s.rel) + '"' : '');
71        }
72
73        var inner = s.type === 'video'
74          ? '<video src="' + esc(s.src) + '" '
75            + (s.poster ? 'poster="' + esc(s.poster) + '" ' : '')
76            + (s.alt ? 'aria-label="' + esc(s.alt) + '" ' : '')
77            + 'preload="metadata" playsinline muted disablepictureinpicture></video>'
78          : '<img src="' + esc(s.src) + '" alt="' + esc(s.alt) + '" '
79            + 'loading="' + (i === 0 ? 'eager' : 'lazy') + '" draggable="false">';
80
81        return '<' + tag + ' ' + attrs + '>' + inner + '</' + tag + '>';
82      }).join('');
83
84      var thumbsHtml = this._slides.map(function (s, i) {
85        return ''
86          + '<button class="thumb' + (i === 0 ? ' is-active' : '')
87          +   (s.type === 'video' ? ' is-video' : '') + '" '
88          +   'type="button" data-i="' + i + '" '
89          +   'aria-label="Перейти к слайду ' + (i + 1) + '">'
90          +   '<img src="' + esc(s.thumb || BLANK) + '" alt="" loading="lazy" draggable="false">'
91          +   (s.type === 'video'
92                ? '<span class="play" aria-hidden="true">'
93                  + '<svg viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg></span>'
94                : '')
95          + '</button>';
96      }).join('');
97
98      root.innerHTML = this._css(this._perView)
99        + '<div class="sc" tabindex="0" aria-roledescription="карусель" aria-label="Слайдер">'
100        +   '<div class="stage">'
101        +     '<div class="track">' + slidesHtml + '</div>'
102        +     '<button class="nav prev" type="button" aria-label="Предыдущий слайд">'
103        +       '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M15 5l-7 7 7 7"/></svg>'
104        +     '</button>'
105        +     '<button class="nav next" type="button" aria-label="Следующий слайд">'
106        +       '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M9 5l7 7-7 7"/></svg>'
107        +     '</button>'
108        +     '<button class="sound" type="button" aria-label="Включить звук" hidden>'
109        +       '<svg class="ic-off" viewBox="0 0 24 24" aria-hidden="true">'
110        +         '<path d="M4 9v6h4l5 4V5L8 9H4zm12.5 3l2.5-2.5-1-1L15.5 11 13 8.5l-1 1 2.5 2.5L12 14.5l1 1 2.5-2.5 2.5 2.5 1-1L16.5 12z"/>'
111        +       '</svg>'
112        +       '<svg class="ic-on" viewBox="0 0 24 24" aria-hidden="true">'
113        +         '<path d="M4 9v6h4l5 4V5L8 9H4zm12-1.5v9a4.5 4.5 0 0 0 0-9z"/>'
114        +       '</svg>'
115        +     '</button>'
116        +   '</div>'
117        +   '<div class="thumbs" role="tablist" aria-label="Миниатюры">' + thumbsHtml + '</div>'
118        + '</div>';
119
120      this._track    = root.querySelector('.track');
121      this._slideEls = root.querySelectorAll('.slide');
122      this._thumbBar = root.querySelector('.thumbs');
123      this._thumbEls = root.querySelectorAll('.thumb');
124      this._stage    = root.querySelector('.stage');
125      this._soundBtn = root.querySelector('.sound');
126    }
127
128    // ----------------------------------------------------------------- //
129    _bind() {
130      var self = this, root = this.shadowRoot;
131
132      root.querySelector('.prev').addEventListener('click', function () { self.prev(true); });
133      root.querySelector('.next').addEventListener('click', function () { self.next(true); });
134
135      this._soundBtn.addEventListener('click', function (e) {
136        e.preventDefault();
137        self._toggleSound();
138      });
139
140      Array.prototype.forEach.call(this._thumbEls, function (btn) {
141        btn.addEventListener('click', function () {
142          self.go(parseInt(btn.getAttribute('data-i'), 10), true);
143        });
144      });
145
146      // --- видео-слайды ---
147      Array.prototype.forEach.call(this._slideEls, function (el, i) {
148        var v = el.querySelector('video');
149        if (!v) return;
150        self._videos[i] = v;
151
152        v.muted = true;                 // ставим свойством: иначе автозапуск блокируется
153        v.playsInline = true;
154        v.setAttribute('playsinline', '');
155        if (self._slides.length === 1) v.loop = true;
156
157        // клик по видео не должен «проглатываться», если слайд — ссылка: браузер сам откроет <a>
158        v.addEventListener('ended', function () {
159          if (document.hidden || self._slides.length < 2) return;
160          self.next(false);
161        });
162        v.addEventListener('error', function () {
163          // видео не загрузилось — не залипаем на слайде
164          self._timeout(self._slides[i].interval || self._interval);
165        });
166
167        if (!self._slides[i].thumb) self._grabThumb(i, v);
168      });
169
170      // пауза при наведении (на весь компонент: слайд, стрелки, миниатюры)
171      var rootEl = root.querySelector('.sc');
172      rootEl.addEventListener('mouseenter', function () { self._stop(); });
173      rootEl.addEventListener('mouseleave', function () { self._start(); });
174
175      // клавиатура (когда фокус на карусели)
176      rootEl.addEventListener('keydown', function (e) {
177        if (e.key === 'ArrowLeft')  { self.prev(true); e.preventDefault(); }
178        if (e.key === 'ArrowRight') { self.next(true); e.preventDefault(); }
179      });
180
181      // свайп на тач-устройствах
182      var x0 = null;
183      this._stage.addEventListener('touchstart', function (e) {
184        x0 = e.touches[0].clientX; self._stop();
185      }, { passive: true });
186      this._stage.addEventListener('touchend', function (e) {
187        if (x0 === null) return;
188        var dx = e.changedTouches[0].clientX - x0;
189        if (Math.abs(dx) > 40) { dx < 0 ? self.next(true) : self.prev(true); }
190        x0 = null;
191        self._start();
192      }, { passive: true });
193
194      // пауза, когда вкладка не активна
195      document.addEventListener('visibilitychange', function () {
196        document.hidden ? self._stop() : self._start();
197      });
198    }
199
200    // ----------------- публичная навигация ----------------- //
201    next(user) { this.go((this._index + 1) % this._slides.length, user); }
202    prev(user) { this.go((this._index - 1 + this._slides.length) % this._slides.length, user); }
203
204    go(i, user) {
205      if (i === this._index || isNaN(i)) return;
206
207      var prev = this._videos[this._index];
208      if (prev) { prev.pause(); try { prev.currentTime = 0; } catch (e) {} }
209
210      this._slideEls[this._index].classList.remove('is-active');
211      this._slideEls[this._index].setAttribute('aria-hidden', 'true');
212      this._thumbEls[this._index].classList.remove('is-active');
213
214      this._index = i;
215
216      this._slideEls[i].classList.add('is-active');
217      this._slideEls[i].setAttribute('aria-hidden', 'false');
218      this._thumbEls[i].classList.add('is-active');
219      this._scrollThumbIntoView(i);
220      this._updateSound();
221
222      this._start(); // таймер/видео всегда перезапускаются под новый слайд
223    }
224
225    _scrollThumbIntoView(i) {
226      var t = this._thumbEls[i], bar = this._thumbBar;
227      if (!t) return;
228      var bw = bar.getBoundingClientRect(), tr = t.getBoundingClientRect();
229      bar.scrollTo({
230        left: bar.scrollLeft + (tr.left - bw.left) - (bw.width / 2) + (tr.width / 2),
231        behavior: 'smooth'
232      });
233    }
234
235    // ----------------- таймер и воспроизведение ----------------- //
236    _start() {
237      this._stop();
238      if (this._reduceMotion) return;
239
240      var self = this,
241          s    = this._slides[this._index],
242          v    = this._videos[this._index];
243
244      if (v) {
245        v.muted = !this._soundOn;
246        var p = v.play();
247        if (p && p.catch) {
248          p.catch(function () {           // автозапуск заблокирован — крутим по таймеру
249            self._timeout(s.interval || self._interval);
250          });
251        }
252        // если у видео-слайда явно задан data-interval — он работает как ограничитель
253        if (s.interval) this._timeout(s.interval);
254        return;
255      }
256
257      this._timeout(s.interval != null ? s.interval : this._interval);
258    }
259
260    _timeout(ms) {
261      if (!(ms > 0)) return;
262      var self = this;
263      if (this._timer) clearTimeout(this._timer);
264      this._timer = setTimeout(function () { self.next(false); }, ms);
265    }
266
267    _stop() {
268      if (this._timer) { clearTimeout(this._timer); this._timer = null; }
269      var v = this._videos[this._index];
270      if (v) v.pause();
271    }
272
273    // ----------------- звук ----------------- //
274    _toggleSound() {
275      this._soundOn = !this._soundOn;
276      var v = this._videos[this._index];
277      if (v) {
278        v.muted = !this._soundOn;
279        if (this._soundOn) { v.volume = 1; var p = v.play(); if (p && p.catch) p.catch(function () {}); }
280      }
281      this._updateSound();
282    }
283
284    _updateSound() {
285      var isVideo = !!this._videos[this._index];
286      this._soundBtn.hidden = !isVideo;
287      this._soundBtn.classList.toggle('is-on', this._soundOn);
288      this._soundBtn.setAttribute('aria-label', this._soundOn ? 'Выключить звук' : 'Включить звук');
289      if (isVideo) this._videos[this._index].muted = !this._soundOn;
290    }
291
292    // ----------------- миниатюра из первого кадра видео ----------------- //
293    _grabThumb(i, v) {
294      var self = this, done = false;
295
296      function shoot() {
297        if (done || !v.videoWidth || !v.videoHeight) return;
298        done = true;
299        try {
300          var c = document.createElement('canvas');
301          c.width  = 320;
302          c.height = Math.round(320 * v.videoHeight / v.videoWidth);
303          c.getContext('2d').drawImage(v, 0, 0, c.width, c.height);
304          var img = self._thumbEls[i] && self._thumbEls[i].querySelector('img');
305          if (img) img.src = c.toDataURL('image/jpeg', 0.75);
306        } catch (e) { /* канвас «испорчен» кросс-доменным видео — оставляем заглушку */ }
307      }
308
309      v.addEventListener('loadedmetadata', function () {
310        try { if (!v.currentTime) v.currentTime = 0.1; } catch (e) {}
311      });
312      v.addEventListener('seeked', shoot);
313      v.addEventListener('loadeddata', shoot);
314    }
315
316    // ----------------------------------------------------------------- //
317    _css(perView) {
318      // gap = 8px; видимых миниатюр = perView
319      return '<style>'
320      + ':host{all:initial;display:block;contain:content;}'           /* отрезаем наследование извне */
321      + '*,*::before,*::after{box-sizing:border-box;}'
322      + '.sc{font-family:system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;'
323      +     'color:#1a1a1a;line-height:1.4;letter-spacing:normal;outline:none;}'
324      + '.sc:focus-visible{outline:3px solid #2563eb;outline-offset:4px;border-radius:18px;}'
325
326      /* --- сцена --- */
327      + '.stage{position:relative;border-radius:10px;overflow:hidden;'
328      +     'border:2px solid #ced4da;'
329      +     'background:#e9ecef;aspect-ratio:16/9;}'
330      + '.track{position:absolute;inset:0;}'
331      + '.slide{position:absolute;inset:0;display:block;opacity:0;visibility:hidden;'
332      +     'transition:opacity .55s ease;text-decoration:none;}'
333      + '.slide.is-active{opacity:1;visibility:visible;}'
334      + '.slide img,.slide video{display:block;width:100%;height:100%;object-fit:contain;'
335      +     'border:0;border-radius:0;filter:none;max-width:none;}'
336      + '.slide video{background:#000;}'
337
338      /* --- стрелки --- */
339      + '.nav{position:absolute;top:50%;transform:translateY(-50%);z-index:2;'
340      +     'width:44px;height:44px;padding:0;margin:0;border:0;border-radius:50%;cursor:pointer;'
341      +     'display:flex;align-items:center;justify-content:center;letter-spacing:normal;'
342      +     'background:rgba(255,255,255,.85);color:#1a1a1a;backdrop-filter:blur(4px);'
343      +     'box-shadow:0 2px 10px rgba(0,0,0,.25);transition:background .2s,transform .2s;}'
344      + '.nav:hover{background:#fff;transform:translateY(-50%) scale(1.08);}'
345      + '.nav:active{transform:translateY(-50%) scale(.96);}'
346      + '.nav.prev{left:12px;} .nav.next{right:12px;}'
347      + '.nav svg{width:22px;height:22px;fill:none;stroke:currentColor;stroke-width:2.5;'
348      +     'stroke-linecap:round;stroke-linejoin:round;}'
349
350      /* --- кнопка звука (только на видео-слайдах) --- */
351      + '.sound{position:absolute;right:12px;bottom:12px;z-index:3;'
352      +     'width:38px;height:38px;padding:0;margin:0;border:0;border-radius:50%;cursor:pointer;'
353      +     'display:flex;align-items:center;justify-content:center;'
354      +     'background:rgba(255,255,255,.85);color:#1a1a1a;backdrop-filter:blur(4px);'
355      +     'box-shadow:0 2px 10px rgba(0,0,0,.25);transition:background .2s;}'
356      + '.sound:hover{background:#fff;}'
357      + '.sound[hidden]{display:none;}'
358      + '.sound svg{width:20px;height:20px;fill:currentColor;}'
359      + '.sound .ic-on{display:none;}'
360      + '.sound.is-on .ic-on{display:block;} .sound.is-on .ic-off{display:none;}'
361
362      /* --- лента миниатюр --- */
363      + '.thumbs{display:flex;flex-wrap:nowrap;gap:8px;margin-top:8px;'
364      +     'overflow-x:auto;padding-bottom:6px;scrollbar-width:thin;'
365      +     '-webkit-overflow-scrolling:touch;}'
366      + '.thumbs::-webkit-scrollbar{height:6px;}'
367      + '.thumbs::-webkit-scrollbar-thumb{background:rgba(0,0,0,.25);border-radius:3px;}'
368      + '.thumb{position:relative;flex:0 0 calc((100% - ' + ((perView - 1) * 8) + 'px) / ' + perView + ');'
369      +     'padding:0;margin:0;border:2px solid transparent;border-radius:10px;cursor:pointer;'
370      +     'overflow:hidden;background:#e9ecef;opacity:.5;letter-spacing:normal;'
371      +     'transition:opacity .2s,border-color .2s,transform .2s;}'
372      + '.thumb:hover{opacity:.85;transform:translateY(-2px);}'
373      + '.thumb.is-active{opacity:1;border-color:#2563eb;}'
374      + '.thumb img{display:block;width:100%;aspect-ratio:16/9;object-fit:cover;'
375      +     'border:0;border-radius:0;filter:none;max-width:none;}'
376      + '.thumb.is-video img{background:#000;}'
377      + '.thumb .play{position:absolute;inset:0;display:flex;align-items:center;'
378      +     'justify-content:center;pointer-events:none;}'
379      + '.thumb .play svg{width:26px;height:26px;fill:#fff;'
380      +     'filter:drop-shadow(0 1px 3px rgba(0,0,0,.65));}'
381
382      + '@media (prefers-reduced-motion: reduce){'
383      +   '.slide{transition:none;} .nav,.thumb,.sound{transition:none;}}'
384      + '@media (max-width:640px){.nav{display:none;}}'                  /* на мобиле — только свайп */
385      + '</style>';
386    }
387  }
388
389  function esc(s) {
390    return String(s).replace(/&/g,'&amp;').replace(/"/g,'&quot;')
391                    .replace(/</g,'&lt;').replace(/>/g,'&gt;');
392  }
393
394  // прячем исходную light-DOM разметку, как только компонент готов
395  var hideStyle = document.createElement('style');
396  hideStyle.textContent = 'site-carousel > a,site-carousel > video{display:none!important;}';
397  document.head.appendChild(hideStyle);
398
399  customElements.define('site-carousel', SiteCarousel);
400})();

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.