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.