PageSourceSearch

https://uvelir.info/static/js/vanilla-plugins.ff007ba2f7ac.js

js uvelir.info collected 2026-10-02 06:18:34 UTC 13,252 bytes, 337 lines download raw bytes

1/* ============================================================
2   vanilla-plugins.js — jQuery-free replacements
3   Replaces: counter, readmore, tabs, lightbox init, carousel init,
4             subscribe modal, masonry (CSS-only, no JS needed)
5   ============================================================ */
6
7document.addEventListener('DOMContentLoaded', function () {
8
9    /* ================================================================
10       LAZY LOAD — swap data-src → src for .lazy images
11       Replaces: plugins.lazyload.js (Canvas theme LazyLoad)
12       ================================================================ */
13    (function () {
14        var lazyEls = document.querySelectorAll('.lazy[data-src]');
15        if (!lazyEls.length) return;
16
17        function loadEl(el) {
18            var src = el.getAttribute('data-src');
19            if (!src) return;
20            if (el.tagName === 'IMG') {
21                el.src = src;
22            } else {
23                el.style.backgroundImage = 'url(' + src + ')';
24            }
25            el.removeAttribute('data-src');
26            el.classList.add('initial');
27        }
28
29        if ('IntersectionObserver' in window) {
30            var io = new IntersectionObserver(function (entries) {
31                entries.forEach(function (entry) {
32                    if (entry.isIntersecting) {
33                        loadEl(entry.target);
34                        io.unobserve(entry.target);
35                    }
36                });
37            }, { rootMargin: '200px' });
38            lazyEls.forEach(function (el) { io.observe(el); });
39        } else {
40            lazyEls.forEach(loadEl);
41        }
42    })();
43
44
45    /* ================================================================
46       COUNTER — animated number increment on scroll
47       Targets: [data-from][data-to] and .counter spans
48       ================================================================ */
49    document.querySelectorAll('[data-from][data-to], .counter[data-comma]').forEach(function (el) {
50        var from = parseInt(el.dataset.from, 10) || 0;
51        var to = parseInt(el.dataset.to, 10) || parseInt(el.textContent, 10) || 0;
52        var speed = parseInt(el.dataset.speed, 10) || 2000;
53        var interval = parseInt(el.dataset.refreshInterval, 10) || 50;
54        var started = false;
55
56        function animate() {
57            if (started) return;
58            started = true;
59            var steps = Math.ceil(speed / interval);
60            var inc = (to - from) / steps;
61            var current = from;
62            var step = 0;
63            var timer = setInterval(function () {
64                step++;
65                current += inc;
66                if (step >= steps) {
67                    current = to;
68                    clearInterval(timer);
69                }
70                el.textContent = Math.round(current).toLocaleString('ru-RU');
71            }, interval);
72        }
73
74        if ('IntersectionObserver' in window) {
75            var observer = new IntersectionObserver(function (entries) {
76                entries.forEach(function (entry) {
77                    if (entry.isIntersecting) {
78                        animate();
79                        observer.unobserve(el);
80                    }
81                });
82            }, { threshold: 0.5 });
83            observer.observe(el);
84        } else {
85            animate();
86        }
87    });
88
89
90    /* ================================================================
91       READMORE — expandable text blocks
92       Targets: [data-readmore]
93       ================================================================ */
94    document.querySelectorAll('[data-readmore]').forEach(function (el) {
95        var triggerOpenHTML = el.dataset.readmoreTriggerOpen || 'Показать';
96        var triggerCloseHTML = el.dataset.readmoreTriggerClose || 'Скрыть';
97        var maxHeight = 100;
98
99        // Measure natural height
100        var naturalHeight = el.scrollHeight;
101        if (naturalHeight <= maxHeight + 50) return;
102
103        el.style.maxHeight = maxHeight + 'px';
104        el.style.overflow = 'hidden';
105        el.style.transition = 'max-height 0.4s ease';
106        el.style.position = 'relative';
107
108        // Find existing trigger or create one
109        var trigger = el.querySelector('.read-more-trigger');
110        if (!trigger) {
111            trigger = document.createElement('a');
112            trigger.href = '#';
113            trigger.className = 'read-more-trigger';
114            el.parentNode.insertBefore(trigger, el.nextSibling);
115        }
116
117        trigger.innerHTML = triggerOpenHTML;
118        trigger.style.display = 'block';
119        trigger.style.textAlign = 'center';
120        trigger.style.padding = '0.5rem';
121        trigger.style.cursor = 'pointer';
122
123        var expanded = false;
124        trigger.addEventListener('click', function (e) {
125            e.preventDefault();
126            expanded = !expanded;
127            if (expanded) {
128                el.style.maxHeight = naturalHeight + 'px';
129                trigger.innerHTML = triggerCloseHTML;
130            } else {
131                el.style.maxHeight = maxHeight + 'px';
132                trigger.innerHTML = triggerOpenHTML;
133            }
134        });
135    });
136
137
138    /* ================================================================
139       TABS — vanilla tab switching
140       Targets: .tabs .tab-nav a + .tab-content
141       ================================================================ */
142    document.querySelectorAll('.tabs').forEach(function (container) {
143        var navLinks = container.querySelectorAll('.tab-nav a');
144        var tabContents = container.querySelectorAll('.tab-content');
145
146        if (!navLinks.length || !tabContents.length) return;
147
148        // Show first tab by default
149        navLinks[0].parentElement.classList.add('ui-tabs-active');
150        tabContents.forEach(function (tc, i) {
151            tc.style.display = i === 0 ? 'block' : 'none';
152        });
153
154        navLinks.forEach(function (link) {
155            link.addEventListener('click', function (e) {
156                e.preventDefault();
157                var target = link.getAttribute('href');
158
159                // Deactivate all
160                navLinks.forEach(function (l) {
161                    l.parentElement.classList.remove('ui-tabs-active');
162                });
163                tabContents.forEach(function (tc) {
164                    tc.style.display = 'none';
165                });
166
167                // Activate current
168                link.parentElement.classList.add('ui-tabs-active');
169                var panel = container.querySelector(target);
170                if (panel) panel.style.display = 'block';
171            });
172        });
173    });
174
175
176    /* ================================================================
177       GLIGHTBOX — compatibility init for data-lightbox attributes
178       Maps Canvas theme data-lightbox to GLightbox data-gallery
179       ================================================================ */
180    if (typeof GLightbox !== 'undefined') {
181        var galleryIndex = 0;
182
183        // Gallery containers: data-lightbox="gallery" with child links
184        document.querySelectorAll('[data-lightbox="gallery"]').forEach(function (container) {
185            galleryIndex++;
186            var name = 'gallery-' + galleryIndex;
187            container.querySelectorAll('a[data-lightbox="gallery-item"]').forEach(function (link) {
188                link.classList.add('glightbox');
189                link.setAttribute('data-gallery', name);
190            });
191        });
192
193        // Standalone lightbox images
194        document.querySelectorAll('a[data-lightbox="image"]').forEach(function (link) {
195            link.classList.add('glightbox');
196        });
197
198        // Initialize
199        GLightbox({
200            selector: '.glightbox',
201            touchNavigation: true,
202            loop: true,
203            closeOnOutsideClick: true,
204        });
205    }
206
207
208    /* ================================================================
209       SPLIDE — carousel init from data attributes
210       Targets: .splide elements
211       ================================================================ */
212    if (typeof Splide !== 'undefined') {
213        document.querySelectorAll('.splide').forEach(function (el) {
214            // Skip thumbnail sliders (they are synced from main)
215            if (el.classList.contains('splide-thumbs')) return;
216
217            var config = {
218                type: el.dataset.type || 'slide',
219                perPage: parseInt(el.dataset.perPage, 10) || 1,
220                gap: el.dataset.gap || '0.5rem',
221                pagination: el.dataset.pagination !== 'false',
222                arrows: el.dataset.arrows !== 'false',
223                rewind: el.dataset.rewind === 'true',
224                autoplay: el.dataset.autoplay === 'true',
225                interval: parseInt(el.dataset.interval, 10) || 5000,
226                speed: parseInt(el.dataset.speed, 10) || 400,
227                breakpoints: {},
228            };
229
230            if (el.dataset.perPageMd) {
231                config.breakpoints[768] = { perPage: parseInt(el.dataset.perPageMd, 10) };
232            }
233            if (el.dataset.perPageSm) {
234                config.breakpoints[480] = { perPage: parseInt(el.dataset.perPageSm, 10) };
235            }
236
237            // Thumbnail sync
238            var thumbId = el.dataset.thumbsTarget;
239            if (thumbId) {
240                var thumbEl = document.querySelector(thumbId);
241                if (thumbEl) {
242                    var mainSplide = new Splide(el, config);
243                    var thumbSplide = new Splide(thumbEl, {
244                        fixedWidth: 100,
245                        fixedHeight: 75,
246                        gap: 8,
247                        rewind: true,
248                        pagination: false,
249                        isNavigation: true,
250                    });
251                    mainSplide.sync(thumbSplide);
252                    mainSplide.mount();
253                    thumbSplide.mount();
254                    return;
255                }
256            }
257
258            new Splide(el, config).mount();
259        });
260    }
261
262
263    /* ================================================================
264       SUBSCRIBE MODAL — Alpine.js modal with cookie-based delay
265       Triggers via custom event listened by Alpine x-data component
266       ================================================================ */
267    var subscribeModal = document.getElementById('subscribeModal');
268    if (subscribeModal) {
269        var cookieName = 'uv_subscribe_shown';
270        var hasCookie = (typeof Cookies !== 'undefined')
271            ? Cookies.get(cookieName)
272            : document.cookie.indexOf(cookieName + '=') !== -1;
273
274        if (!hasCookie) {
275            setTimeout(function () {
276                window.dispatchEvent(new CustomEvent('open-modal', { detail: 'subscribeModal' }));
277                if (typeof Cookies !== 'undefined') {
278                    Cookies.set(cookieName, '1', { expires: 30 });
279                } else {
280                    document.cookie = cookieName + '=1; max-age=' + (30 * 86400) + '; path=/';
281                }
282            }, 60000);
283        }
284    }
285
286
287    /* ================================================================
288       NAVIGATION — search toggle, GoToTop
289       Меню шапки живёт в Alpine-компоненте navMenu() (js/nav-menu.js).
290       ================================================================ */
291    (function () {
292        /* ── Search toggle ── */
293        var searchForm = document.getElementById('uvSearchForm');
294        if (searchForm) {
295            document.querySelectorAll('.uv-search-toggle').forEach(function (btn) {
296                btn.addEventListener('click', function (e) {
297                    e.preventDefault();
298                    searchForm.classList.toggle('show');
299                    if (searchForm.classList.contains('show')) {
300                        searchForm.querySelector('input')?.focus();
301                    }
302                });
303            });
304        }
305
306        /* ── GoToTop button ── */
307        var gotoTop = document.getElementById('gotoTop');
308        if (gotoTop) {
309            var scrollThreshold = 450;
310            gotoTop.style.display = 'none';
311
312            window.addEventListener('scroll', function () {
313                gotoTop.style.display = window.scrollY > scrollThreshold ? 'block' : 'none';
314            }, { passive: true });
315
316            gotoTop.addEventListener('click', function () {
317                window.scrollTo({ top: 0, behavior: 'smooth' });
318            });
319        }
320    })();
321
322
323    /* ================================================================
324       YANDEX BROWSER — review proposal widget
325       ================================================================ */
326    if (navigator.userAgent.includes('YaBrowser')) {
327        var portal = document.getElementById('UvelirPortal');
328        if (portal) {
329            var proposal = document.createElement('div');
330            proposal.id = 'broy';
331            proposal.className = 'yb-proposal';
332            proposal.innerHTML = 'Оставить отзыв о портале Ювелир.INFO';
333            portal.appendChild(proposal);
334        }
335    }
336
337});

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.